Harlotte 发表于 2024-5-12 20:55:03

一个包含声音、烟、车轮转动等的JS代码案例分享(全部注释)

本帖最后由 Harlotte 于 2024-5-12 21:06 编辑

首先感谢zbx1425的技术支持!

通过一段时间的研究和钻研,我终于完成了对列车js代码的开发与实验,现除了声音部分由于nte函数问题(?)还无法实现,其他均已实现。
写在前面,JS一定会对流畅度有影响,这十分正常,请有心理准备。目前这种大杂烩的做法会使延迟异常的高,不建议直接搬走使用,尽量将其分为不同JS。(各功能分开的我过两天整理好了会发,可以再等等)。

var rms = ModelManager.loadPartedRawModel(Resources.manager(), Resources.idRelative("df5g.obj"), null);//抄的
var models = uploadPartedModels(rms);//抄的
var yanrawModels = Array.from(ModelManager.loadPartedRawModel(Resources.manager(), Resources.idRelative("df5g_yan.obj"), null).values());
var suo = 1;//初始化缩放
var yanclass = 30;//烟大小等级数量
var yanmaxscale = 4;//烟最大缩放
var yanarrayrms = yanrawModels.copy();//存储烟模型
var yanarray = new Array();//烟模型文件(加载后
for (let i = 0; i < yanclass; i++) {//缩放并上传不同大小的烟模型
    yanarray = {model:new Array()};//向这个元素其添加一个数组
    for(let j=0;j<4;j++){//这里我的df5g_yan中含有4种烟,所以为j<4
      yanarrayrms = yanrawModels.copy();//复制到临时rawmodel
      yanarrayrms.applyScale(suo,suo,suo);//进行缩放
      yanarrayrms.applyUVMirror(false, true);//UV翻转(取决于模型文件)
      yanarrayrms.sourceLocation = null;//可以理解为断开原数据连接,否则多个缩放大小会指向统一份数据
      yanarray.model = ModelManager.uploadVertArrays(yanarrayrms)//添加数据到数组中的数组 大的为缩放值 小的为模型类型
    }
    suo = suo +yanmaxscale/yanclass;//缩放大小更新
}
var r=0;//初始化摇晃幅度
var pid=1.12; //轮子直径
var d=-0.446434+0.01; //轮子距原点纵轴距离
var s1=3.1358; //轮子距原点横距离1
var s2=1.80722; //轮子距原点横距离2
var pit = 1;//初始化音高(速度)
var yao=0.1;//摇晃幅度限制
var yao1=0.01;//车速对摇晃幅度的影响幅度
var yc = new Vector3f(0.336062 , 3.49877 + 0.15 , 6.31814 );//烟囱位置
var yantimelimit = 6;//烟囱持续时间限制
var yanverticalspeedlimit = 1;//烟囱上升速度系数 m/s
var yanshuatimelimit = 0.08;//烟刷新时间
var yannumlimit = 0 ;//初始化烟数量限制
var kongzu =0.5; //空气阻力系数m/s


function create(ctx, state, train) {
    yannumlimit = yantimelimit * (1 / yanshuatimelimit) + 20;//烟数量限制
    state.r = 0.0;//初始化轮子旋转角度
    state.speed = 0;//初始化列车速度
    state.number = new Array();//声音下一次播放时间记录
    state.yan = new Array();//烟数据
    state.ynum = 0;//烟现在数量
    state.time = 0;//初始化时间
    state.yantime = 0;//初始化烟刷新时间
    for (let j = 0; j < yannumlimit; j++) {//初始化烟数据
      state.yan = {zhe:0,runing:0,time:0,speed:train.speed(),model:1,lastpos:new Vector3f(0,0,0),lastrot:new Vector3f(0,0,0),vect:new Vector3f(0,0,0),rot:new Vector3f(0,0,0)};
    }
}

function render(ctx, state, train) {
    //ctx.setDebugInfo("yanm=",yanarray.model);
    pit = 1.0 + train.speed() / 2;//更新音高(速度)
    if(r==0&&grnn(0,800)-train.speed()/10<5&&train.speed()>0){//当抖动归零且随机数为目标以内且列车在运动时:
      r=grn(0,yao+train.speed()*yao1);//随机抖动幅度
    }else if(r>0.1){//当角度为正且它在归零范围外时
      r=r-Math.abs(grnn(0,0.15));//减少随机数
    }else if(r<-0.1){//当角度为负且它在归零范围外时
      r=r+Math.abs(grnn(0,0.15));//增加随机数
    }else{//当他在归零范围内时
      r=0;//归零
    };
    if(train.speed()-state.speed>=0){//如果列车加速或保持速度则车轮转动
      if(train.isReversed()){//如果它是倒车的
          state.r = state.r - train.speed() / ( pid * Math.PI ) * 360 * Timing.delta() * 20; //轮子角度向后转
      }else{//否则如果它前进
          state.r = state.r + train.speed() / ( pid * Math.PI ) * 360 * Timing.delta() * 20;//向前转
      }
    }else{//否则如果其减速
      state.r = state.r;//保持不动
      sound(ctx,"mtr:df5g/shache",i,0,0,0,pit,state.number,16,0.05);//播放刹车声
    };
    let mat = new Matrices();//创建矩阵
    let mat2 = new Matrices();//创建矩阵   
    mat.rotateZ(dtrd(r));//第一个矩阵应用全部抖动
    mat2.rotateZ(dtrd(r)/5);//第二个应用部分抖动
    for (let i = 0; i < train.trainCars(); i++) {//重复加载车辆
      ctx.drawCarModel(models["bogie_frame"], i, mat2);//转向架 应用部分抖动
      ctx.drawCarModel(models["bogie_frame_1"], i, mat2);//同上
      ctx.drawCarModel(models["coupler"], i, mat);//其余应用全部抖动
      ctx.drawCarModel(models["decorations"], i, mat);
      ctx.drawCarModel(models["doors"], i, mat);
      ctx.drawCarModel(models["frame"], i, mat);
      ctx.drawCarModel(models["shell"], i, mat);
      if(train.isOnRoute()){//如果它已经出库
            if(train.isReversed()){//如果它前进
                ctx.drawCarModel(models["light_2"], i, mat);//后退组灯灭
                ctx.drawCarModel(models["light_11"], i, mat);//前进组灯亮
            }else{//否则它后退
                ctx.drawCarModel(models["light_1"], i, mat);//前进组灯亮
                ctx.drawCarModel(models["light_21"], i, mat);//后退组灯灭
            };
      }else{//否则它没出库
            ctx.drawCarModel(models["light_1"], i, mat);//前进组灯灭
            ctx.drawCarModel(models["light_2"], i, mat);//后退组灯灭
      }

    };
    for (let i = 0; i < train.trainCars(); i++) {//加载轮子
      let mat33 = new Matrices();
      mat33.translate(0,d,0);//向下平移
      mat33.translate(0,0,s1);//向前平移
      mat33.pushPose();//存储平移
      mat33.rotateX(dtrd(state.r));//旋转
      ctx.drawCarModel(models["wheel"], i, mat33);//绘制
      mat33.popPose();//还原平移
      mat33.translate(0,0,s2);//继续向前
      mat33.pushPose();//。。。。。。
      mat33.rotateX(dtrd(state.r));
      ctx.drawCarModel(models["wheel"], i, mat33);
      mat33.popPose();
      mat33.translate(0,0,s2);
      mat33.pushPose();
      mat33.rotateX(dtrd(state.r));
      ctx.drawCarModel(models["wheel"], i, mat33);
      mat33 = new Matrices();
      mat33.translate(0,d,0);
      mat33.translate(0,0,-s1);
      mat33.pushPose();
      mat33.rotateX(dtrd(state.r));
      ctx.drawCarModel(models["wheel"], i, mat33);
      mat33.popPose();
      mat33.translate(0,0,-s2);
      mat33.pushPose();
      mat33.rotateX(dtrd(state.r));
      ctx.drawCarModel(models["wheel"], i, mat33);
      mat33.popPose();
      mat33.translate(0,0,-s2);
      mat33.pushPose();
      mat33.rotateX(dtrd(state.r));
      ctx.drawCarModel(models["wheel"], i, mat33);
    };
    ctx.setDebugInfo("r=",state.r);
    ctx.setDebugInfo("dtrd(r)=",dtrd(state.r));
    let gmk = getCurrentTrackModelKey(ctx,state,train);//获取当前轨道的自定义轨道名称   
    if(train.isOnRoute()){//如果工作
      for (let i = 0; i < train.trainCars(); i++) {
            sound(ctx,"mtr:df5g_engine2",i,0,0,0,pit,state.number,16,1);//播放常见的声音,最后一个是轮播时常,单位是秒,推荐是音频的一半左右
            sound(ctx,"mtr:df5g_engine",i,0,0,0,pit,state.number,16,1);//同上
            if(gmk.indexOf("horn")){
                sound(ctx,"mtr:df5g_horn",i,0,0,0,pit,state.number,16,5);//播放鸣笛声,单位是秒,推荐是5秒左右,可以根据需要调整 我这里是因为音频不合适循环播放,实际音频在1s左右
            }
      }
    }
    //yanshuatimelimit = (yannumlimit - 20)/yantimelimit;
    for(i = 0; i < train.trainCars(); i++){
      if(train.isOnRoute()){//是否出库
            if(state.yantime < state.time){//是否到达刷新时间
                state.yan.runing = 1;//是否显示
                state.yan.time = 0;//记录时间
                state.yan.speed = train.speed();//记录速度
                state.yan.lastpos = train.lastCarPosition;//记录位置
                state.yan.lastrot = train.lastCarRotation;//记录旋转
                state.yan.vect = getWorldPositionFromTrainLocalPosition(yc,train.lastCarPosition,train.lastCarRotation);//记录世界位置
                state.yan.rot = new Vector3f(grn(0,2*Math.PI),grn(0,2*Math.PI),grn(0,2*Math.PI));//随机旋转
                state.yan.model = Math.floor(grnn(0,4));//随机模型
                if(train.isReversed()){//记录向前向后惯性方向
                  state.yan.zhe = 1;
                }else{
                  state.yan.zhe = 0;
                }
                state.yantime = state.yantime + yanshuatimelimit;//增加刷新时间
                state.ynum = state.ynum + 1;//计数器加一
                if(state.ynum >= yannumlimit){//如果超限(前面20个已经显示完毕) 计数器归零
                  state.ynum = 0;
                }
            }
      }
      for (let j = 0; j < yannumlimit; j++) {
            //以下判断烟是否显示
            if(state.yan.runing == 0){//如果不显示 跳过
                continue;
            }
            if(state.yan.time > yantimelimit){//如果超过时间 将其改为不显示并跳过
                runing = 0;
                continue;
            }

            //以下绘制烟
            let mat9 = new Matrices();//一个临时刷新矩阵
            let mat10 = getTrainLocalPositionFromWorldPosition(state.yan.vect,train.lastCarPosition,train.lastCarRotation);//将渲染位置转换为列车坐标系
            let mat11 = vectadd(state.yan.rot , vectMirrorFlip(train.lastCarRotation));//计算旋转
            mat9.translate(mat10.x(),mat10.y(),mat10.z());//移动
            mat9.rotateX(mat11.x());//旋转
            mat9.rotateY(mat11.y());
            mat9.rotateZ(mat11.z());
            let yansuo1 = positive(Math.floor(state.yan.time / yantimelimit * yanclass +grn(0,0.1)) -1);//计算烟的大小模型
            ctx.drawCarModel(yanarray.model.model], i, mat9);//绘制烟

            //以下为烟添加变换
            state.yan.rot = vectadd(state.yan.rot,new Vector3f(grn(0,2*Math.PI),grn(0,2*Math.PI),grn(0,2*Math.PI)));//增加随机旋转
            let vectyy = new Vector3f(0,yanverticalspeedlimit * Timing.delta(),0);//垂直坐标增加量
            let vectzz = new Vector3f(0,0,0);//初始化水平(列车z轴)增加量
            state.yan.speed = positive(state.yan.speed - kongzu * Timing.delta());//速度(惯性)减少空气阻力
            if(state.yan.zhe == 1){//如果已经折返 则惯性为-速度
                vectzz = new Vector3f(0,0,-state.yan.speed * 20 * Timing.delta());
            }else{//否则为+速度
                vectzz = new Vector3f(0,0,state.yan.speed * 20 * Timing.delta());
            }
            state.yan.vect = vectadd(state.yan.vect,vectadd(vectyy,getWorldPositionFromTrainLocalPositionOnly(vectzz,state.yan.lastpos,state.yan.lastrot)));//增加位置
            state.yan.time = state.yan.time + Timing.delta();//增加时间
      }
    }
    state.speed = train.speed();//更新速度
    state.time = state.time + Timing.delta();//更新时间
}
function uploadPartedModels(rawModels) {//直接搬过来的,上传模型
    let result = {};
    for (it = rawModels.entrySet().iterator(); it.hasNext(); ) {
      entry = it.next();
      entry.getValue().applyUVMirror(false, true);
      result = ModelManager.uploadVertArrays(entry.getValue());
    }
    return result;
}
function grn(min, max) {//随机小数正负
    if(Math.random()>=0.5){
      returnMath.random()*(max-min)+min;
    }else{
      return0-Math.random()*(max-min)+min;
    }
}
function grnn(min, max) {//随机小数
      returnMath.random()*(max-min)+min;
}
function getCurrentTrackModelKey(ctx, state, train) {//获取当前轨道的自定义轨道名称,让ai写的
    // 获取列车从车库开出的距离
    let railProgress = train.railProgress();
    // 获取当前轨道的索引
    let currentRailIndex = train.getRailIndex(railProgress, true);
    // 检查当前轨道索引是否有效
    if (currentRailIndex >= 0 && currentRailIndex < train.path().size()) {
      // 获取当前路径数据对象
      let currentPathData = train.path().get(currentRailIndex);
      // 获取当前轨道使用的自定义轨道名称
      let trackModelKey = currentPathData.rail.getModelKey();
      return trackModelKey;
    }
    return null; // 或者是一个默认值
}
function dtrd(degrees) {//角度转弧度
    return degrees * Math.PI / 180;
}
function rdtd(radians) { // 弧度转角度
    return radians * 180 / Math.PI;
}
function sound(ctx,name2,i,xx,yy,zz,pit,nu,ll,long){//播放声音pit是音高(速度) nu是一个数组,用来记录播放时间,ll是音频响度,long是循环播放时间
    if(Timing.elapsed()>nu){//如果时间超过了播放时间
      ctx.playCarSound(Resources.id(name2) , i , xx , yy , zz , ll , pit);//播放
      nu = Timing.elapsed()+long;   //更新播放时间
    }
}
function getWorldPositionFromTrainLocalPosition(localPosition , wordpost , wordrotate){//转换列车局部坐标到世界坐标
    let mat = new Matrix4f();//新建一个临时矩阵,将它想想为一个原点位于列车位置,xyz轴与世界标系相等的坐标系

    mat.rotateX(wordrotate.x());//可以理解为旋转这个平行于世界坐标系的坐标系,让它与列车的xyz坐标平行,与其重叠
    mat.rotateY(wordrotate.y());
    mat.rotateZ(wordrotate.z());

    mat.translate(localPosition.x(),localPosition.y(),localPosition.z());//在列车坐标系中移动
   
    return vectadd(wordpost,mat.getTranslationPart())//计算还原到原来的世界坐标系的位置并于世界坐标相加
}
function getWorldPositionFromTrainLocalPositionOnly(localPosition , wordpost , wordrotate){//转换列车局部坐标到世界坐标
    let mat = new Matrix4f();

    mat.rotateX(wordrotate.x());
    mat.rotateY(wordrotate.y());
    mat.rotateZ(wordrotate.z());

    mat.translate(localPosition.x(),localPosition.y(),localPosition.z());
   
    return mat.getTranslationPart();//以上于上一个函数相同 但没有增加世界坐标
}
function getTrainLocalPositionFromWorldPosition(localPosition , wordpost , wordrotate){//转换世界坐标到列车局部坐标
    let mat = new Matrix4f();//新建一个临时矩阵,将它想想为一个位于列车原点位置、xyz轴与列车坐标系相等的坐标系

    mat.rotateX(-wordrotate.x());//旋转世界坐标将其与列车坐标xyz轴平行
    mat.rotateY(-wordrotate.y());
    mat.rotateZ(-wordrotate.z());

    mat.translate(vectsub(localPosition,wordpost));//移动列车相对于世界坐标的位置 两坐标相减即可看作一个带有方向的距离
   
    return mat.getTranslationPart();
}
function vectMirrorFlip(vect){//镜像翻转
    return new Vector3f(-vect.x(),-vect.y(),-vect.z());
}
function vectadd(v1,v2){//向量相加
    return new Vector3f(v1.x()+v2.x(),v1.y()+v2.y(),v1.z()+v2.z());
}
function vectsub(v1,v2){//向量相减
    return new Vector3f(v1.x()-v2.x(),v1.y()-v2.y(),v1.z()-v2.z());
}
function positive(num){//一个简单的取整函数
    if(num>0){//如果大于0
      return num;//返回原值
    }else{//否则小于0
      return 0;//返回0
    }
}


无偿分享!还请多多支持!!有什么问题请不吝赐教!!!

Aphrodite
2024年5月12日

ShentongMetro 发表于 2024-5-13 18:01:13

巨!
话说一直有些小问题,就是说我是要所有组件都必须先移动再旋转之类的才行吗?还是存在类似枢轴之类的东西,计算机图形不太懂()

KKWing22 发表于 2024-5-13 18:15:48

甚至正好300行

Jeffreyg1228 发表于 2024-5-14 10:14:36

复制代码时存在乱码,还是希望论坛改进一下防复制功能 (https://www.mtrbbs.top/thread-6671-1-1.html)

复制结果节选:

var rms = ModelManager.loadPartedRawModel(Resources.manager(), Resources.idRelative("df5g.obj"), null);//抄的
]+ q# o- e: T5 v& Y& E% Gvar models = uploadPartedModels(rms);//抄的
5 F" @3 O, s3 I- m: B9 ~7 c7 }var yanrawModels = Array.from(ModelManager.loadPartedRawModel(Resources.manager(), Resources.idRelative("df5g_yan.obj"), null).values());7 y% ~, K# q0 g/ R, }
var suo = 1;//初始化缩放; s; b7 p+ ]" A8 T# c$ L' T
var yanclass = 30;//烟大小等级数量) H% p8 A1 f" k# B

Harlotte 发表于 2024-5-14 20:12:07

ShentongMetro 发表于 2024-5-13 18:01
巨!
话说一直有些小问题,就是说我是要所有组件都必须先移动再旋转之类的才行吗?还是存在类似枢轴之类的 ...

简单来说是这样的 复杂来说.......我也说不太明白,你可以试着听听 反正理是这么个理

zbx文档里只是简单的来说是靠后的操作离实际结果更近,但其实我想不会有编译器真是这么写的不按照顺序来操作。而且这种说法下Matrix4f.getTranslationPart()这一函数是无法被解释的。在Matrix4f和Matrices中 旋转不是独立于平移的另一个数据,我想他们是高度相关的。
我对平移和旋转的操作就是他们旋转时会在当前位置旋转坐标轴,然后实际上在这之后的操作都会以新的坐标轴为基础来进行操作。这也就可以理解像在以上代码中关键的列车坐标与世界坐标变换的相关内容了。

Zbx1425 发表于 2024-5-15 23:28:57

Harlotte 发表于 2024-5-14 20:12
简单来说是这样的 复杂来说.......我也说不太明白,你可以试着听听 反正理是这么个理

zbx文档里只是简单 ...
这个东西本质上是 4x4 的三维变换矩阵,所以确实是你说的 “平移和旋转是高度相关的”;或者说都是在变换后的坐标系上再做变换,这在数学上是矩阵乘法的形式,对应的行和列都会参与运算。
这里的 getRotationPart 和 getTranslationPart 做的是在取它的几个向量元素,形式上是最终的相对于最初坐标系的偏移量之类?并不是简单的“所有平移加起来,所有旋转加起来”之类的东西。
您有兴趣的话可以看线性代数上的相关内容,我文档里肯定得照顾对线性代数了解不多的读者,所以那么写了,实话说我的遣词造句水平也不怎样……

总之,最终的形式是类似于
[最终坐标] = [变换1] * [变换2] * [变换3] * [原始坐标]
在实现上是按照乘法结合律把所有变换结合起来,成为了 [最终坐标] = [总变换矩阵] * [原始坐标] 的形式,而调用变换函数的时候就是给总变换矩阵上再右乘一个新的变换(矩阵乘法不符合交换律)
所以这个过程是矩阵乘法自然地产生的,所以我会说 “靠后的操作离原始状态更近”,也没有“故意写的不按顺序来操作”这回事。其实左乘而不是右乘就达到相反的顺序了;只不过大多按右乘设计,因为尤其是这样设计让父子关系(下级元素需要继承上级元素的变换)做起来很方便。
更进一步来说,右乘达到了这种效果其实本质上是因为我把原始坐标乘在了右边,如果把它乘在左边上述的东西就正相反了,不过 Minecraft 原版(以及大多人)都是做的原始坐标乘在右边。

我线性代数也不怎么好,有说错的地方请指正。

页: [1]
查看完整版本: 一个包含声音、烟、车轮转动等的JS代码案例分享(全部注释)