3D网页小实验——将txt配置文本转化为3D陈列室

设计目标:借鉴前辈编程者的经验将简单的配置文本转化为3D场景,并根据配置文件在场景中加入图片和可播放的视频,最终形成可浏览的3D陈列室。

一、使用效果

1、txt配置文件:

(博客园的富文本编辑器会改变txt文本的排版,所以用图片方式呈现文本)

第一行表示陈列室的每一层前后最多有5个房间,左右最多有8个房间,接下来是第一层的地图:“0”表示普通房间,“+、-、|”表示连接房间的通道,“#”表示地面有洞的房间可用来连接下一层,“^”表示顶部有洞的房间用来连接上一层。“//source”后面是本层的资源,用竖线分隔的参数依次表示前后位置、左右位置、资源“贴在”哪面墙上、资源类型、资源url,比如“//source:2|4|z|mp4|big_buck_bunny.mp4”即表示在第二行第四列的房间的z面(前面)贴上url为big_buck_bunny.mp4的mp4视频。再下面则是-1层的地图。

2、显示效果

房间的整体效果如下:

渲染出了配置文件中设置的房间布局,可以通过修改代码替换默认的草地和线框纹理。场景默认使用Babylon.js的自由相机进行控制,按“v”键可以启用fps式控制,鼠标移动直接控制视角,wasd控制前后左右,c和空格控制升降,同时启用相机与墙壁的碰撞检测阻止穿墙,再次按v键则可关闭fps控制。

进入第二层中间的房间可以看到房间两侧的视频,点击即可播放:

进入第二层右侧的房间,可以看到相邻的小房间融合为一个大房间:

可以通过https://ljzc002.github.io/Txt2room/HTML/PAGE/room.html查看在线实例,代码没有进行编译可以直接在线调试,在https://github.com/ljzc002/ljzc002.github.io/tree/master/Txt2room查看项目代码。

二、代码实现

1、建立房间零件的源网格(master mesh)

为了提高渲染效率,这里并没有为每个房间建立独立的mesh对象,而是将房间拆解为基础的组成零件,对零件建立源网格,然后用WebGL的instance技术批量生成源网格的实例。

以下是生成预制件源网格的方法:

1 var size_per_u=3;//1纹理坐标长度对应场景的3单位长度
 2 var size_per_v=3;
 3 var positions=[];
 4 var uvs=[];
 5 var normals=[];
 6 var indices=[];
 7 function initMeshClass()
 8 {//plan的基础状态是一个位于原点,面向z轴负方向的平面
 9     add_plan2({x:-4.5,y:4.5,z:0},{x:1.5,y:4.5,z:0},{x:1.5,y:1.5,z:0},{x:-4.5,y:1.5,z:0},0);
10     add_plan2({x:1.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-1.5,z:0},{x:1.5,y:-1.5,z:0},4,6/size_per_u);
11     add_plan2({x:-1.5,y:-1.5,z:0},{x:4.5,y:-1.5,z:0},{x:4.5,y:-4.5,z:0},{x:-1.5,y:-4.5,z:0},8,3/size_per_u,6/size_per_v);
12     add_plan2({x:-4.5,y:1.5,z:0},{x:-1.5,y:1.5,z:0},{x:-1.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},12,0,3/size_per_v);
13     var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_hole",mat_grass);
14     mesh.setEnabled(false);//令源网格不显示
15     // 很奇怪如果不对长通道设置mesh.setEnabled(false);则实例无法正常显示,但其他类的实例则没有这种问题。
16     //mesh.setEnabled(true);//默认就是这个
17     obj_meshclass["hole"]=mesh;//带有洞的墙壁
18
19     positions=[];//新建式清空,理论上不影响引用的数据
20     uvs=[];
21     normals=[];
22     indices=[];
23     add_plan2({x:-4.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},0);
24     var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_wall",mat_grass);
25     mesh.setEnabled(false);
26     obj_meshclass["wall"]=mesh;//墙壁
27
28     positions=[];
29     uvs=[];
30     normals=[];
31     indices=[];
32     add_plan2({x:-1.5,y:1.5,z:0},{x:1.5,y:1.5,z:0},{x:1.5,y:-1.5,z:0},{x:-1.5,y:-1.5,z:0},0);
33     var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_smallwall",mat_grass);
34     mesh.setEnabled(false);
35     obj_meshclass["smallwall"]=mesh;//小块墙壁
36
37     positions=[];
38     uvs=[];
39     normals=[];
40     indices=[];
41     add_plan2({x:-1.5,y:1.5,z:-4.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:-4.5},0);
42     add_plan2({x:1.5,y:1.5,z:-4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:-4.5},4);
43     add_plan2({x:1.5,y:-1.5,z:-4.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:-4.5},8);
44     add_plan2({x:-1.5,y:-1.5,z:-4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:-4.5},12);
45     var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_channel",mat_frame);
46     //mesh.setEnabled(false);
47     // 很奇怪如果不对长通道设置mesh.setEnabled(false);则实例无法正常显示,但其他类的实例则没有这种问题。
48     mesh.setEnabled(false);
49     obj_meshclass["channel"]=mesh;//长通道
50
51     positions=[];
52     uvs=[];
53     normals=[];
54     indices=[];
55     add_plan2({x:-1.5,y:1.5,z:1.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:1.5},0);
56     add_plan2({x:1.5,y:1.5,z:1.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:1.5},4);
57     add_plan2({x:1.5,y:-1.5,z:1.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:1.5},8);
58     add_plan2({x:-1.5,y:-1.5,z:1.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:1.5},12);
59     var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_shortchannel",mat_grass);
60     mesh.setEnabled(false);
61     obj_meshclass["shortchannel"]=mesh;//短通道
62 }

其中add_plan2方法用来根据四个给定的顶点生成平整的四边形顶点数据:

1 //平面四个顶点的坐标(从左上角开始顺时针排列),第一个顶点的插入索引,uv纹理的坐标的偏移量
 2 function add_plan2(v1,v2,v3,v4,index,offsetu,offsetv)
 3 {
 4     positions.push(v1.x);
 5     positions.push(v1.y);
 6     positions.push(v1.z);
 7     positions.push(v2.x);
 8     positions.push(v2.y);
 9     positions.push(v2.z);
10     positions.push(v3.x);
11     positions.push(v3.y);
12     positions.push(v3.z);
13     positions.push(v4.x);
14     positions.push(v4.y);
15     positions.push(v4.z);
16     //使用和Babylon.js条带网格相同的顶点顺序
17     indices.push(index+3);
18     indices.push(index+2);
19     indices.push(index);
20     indices.push(index+1);
21     indices.push(index);
22     indices.push(index+2);
23     //根据顶点位置计算平整纹理坐标
24     //1234对应abcd
25     var vab=v3subtract(v2,v1);
26     var lab=v3length(vab);
27     var vac=v3subtract(v3,v1);
28     var lac=v3length(vac);
29     var vad=v3subtract(v4,v1);
30     var lad=v3length(vad);
31
32     var BAC=Math.acos((vab.x*vac.x+vab.y*vac.y+vab.z*vac.z)/(lab*lac));
33     var BAD=Math.acos((vab.x*vad.x+vab.y*vad.y+vab.z*vad.z)/(lab*lad));
34     if(!offsetu)
35     {
36         offsetu=0;
37     }
38     if(!offsetv)
39     {
40         offsetv=0;
41     }
42     uvs.push(offsetu);
43     uvs.push(offsetv);
44     uvs.push(offsetu+lab/size_per_u);
45     uvs.push(offsetv);
46     uvs.push(offsetu+(lac*Math.cos(BAC)/size_per_u));
47     uvs.push(offsetv+(lac*Math.sin(BAC)/size_per_v));
48     uvs.push(offsetu+(lad*Math.cos(BAD)/size_per_u));
49     uvs.push(offsetv+(lad*Math.sin(BAD)/size_per_v));
50 }
51 function v3subtract(v1,v2)//向量相减
52 {
53     return {x:(v1.x-v2.x),y:(v1.y-v2.y),z:(v1.z-v2.z)}
54 }
55 function v3length(v)//计算向量长度
56 {
57     return Math.pow(v.x*v.x+v.y*v.y+v.z*v.z,0.5)
58 }

计算平整纹理坐标使用了向量点乘的性质:vab.x*vac.x+vab.y*vac.y+vab.z*vac.z=vab.vac=lab*lac*cosCAB

这使得我们可以根据三角形三个顶点的坐标计算出其中两个向量的夹角,进而在这两个向量确定的平面中计算出三个顶点的纹理坐标。

可以在https://forum.babylonjs.com/t/which-way-should-i-choose-to-make-a-custom-mesh-from-ribbon/10793查看一些关于为什么要进行纹理平整的讨论。

vertexData2Mesh方法用来将生成的顶点数据转化为网格,

1 function vertexData2Mesh(positions, indices, normals, uvs,name,material)
 2 {
 3     var vertexData= new BABYLON.VertexData();//顶点数据对象
 4     BABYLON.VertexData.ComputeNormals(positions, indices, normals);//计算法线
 5     BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs);
 6     vertexData.indices = indices.concat();//索引
 7     vertexData.positions = positions.concat();
 8     vertexData.normals = normals.concat();//position改变法线也要改变!!!!
 9     vertexData.uvs = uvs.concat();
10     var mesh=new BABYLON.Mesh(name,scene);
11     vertexData.applyToMesh(mesh, true);
12     mesh.vertexData=vertexData;
13     mesh.material=material;
14     mesh.renderingGroupId=2;
15     return mesh;
16 }

最后mesh.setEnabled(false)用来隐藏源网格。

2、读取配置文本,提取房间信息

1 var str=newland.importString("06.txt");
 2         //console.log(str);
 3         var arr=str.split("\r\n")//限于window操作系统下??
 4         var len=arr.length;
 5         for(var i=0;i<len;i++)//对于每一行
 6         {
 7             var line=arr[i];
 8             if(line.substring(0,2)=="//")
 9             {
10                 var arr2=line.substring(2).split("@");
11                 var len2=arr2.length;
12                 for(var j=0;j<len2;j++)
13                 {
14                     var obj=arr2[j];
15                     var arr3=obj.split(":");
16                     var ptype=arr3[0];
17                     var pvalue=arr3[1];
18                     if(ptype=="seg_z")
19                     {
20                         seg_z=parseInt(pvalue);
21                     }
22                     else if(ptype=="seg_x")
23                     {
24                         seg_x=parseInt(pvalue);
25                     }
26                     else if(ptype=="floor")//进入了一层
27                     {
28                         i=handleFloor(pvalue,arr,i+1);
29                     }
30                 }
31             }
32         }

其中importString是一个读取服务端文本文件的方法,其代码如下:

1 newland.importString=function(url)
2 {
3     var xhr=new XMLHttpRequest;
4     xhr.open("GET",url,false);//第三个参数表示是同步加载
5     xhr.send(null);
6     var data=xhr.responseText;
7     return data;
8 }

读入后一行行遍历文本,发现“//floor”则开始处理这一层的房间数据:

1 function handleFloor(int_floor,arr,index)
 2 {
 3     var floor=obj_building[int_floor];//在obj_building中保存所有房间信息
 4     if(!floor)
 5     {
 6         obj_building[int_floor]={};
 7         floor=obj_building[int_floor];
 8     }
 9     var len=arr.length;
10     var count=0;
11     //继续读txt文本
12     for(var i=index;i<len;i++)
13     {
14         var line=arr[i];
15         count++;
16         if(count<=seg_z)
17         {
18
19             if(!floor[count+""])
20             {
21                 floor[count+""]={}
22             }
23
24             for(var j=0;j<seg_x;j++)
25             {
26                 if(line[j])
27                 {
28
29                     floor[count+""][j+1+""]={type:line[j],arr_source:[]};//这个“数组”都是从一开始的
30                     //addRoom(count-1,j);//行、列,规划完毕后统一添加渲染
31                 }
32             }
33         }
34         else
35         {
36             if(line.substring(0,7)=="//floor")//查找到另一层
37             {
38                 return (index+count-2);
39             }
40             else if(line.substring(0,8)=="//source")//为这个房间设置资源
41             {
42                 //var arr2=line.split(":")[1].split("|");
43                 var arr2=line.substring(line.search(":")+1).split("|");
44                 if(floor[arr2[0]]&&floor[arr2[0]][arr2[1]])
45                 {
46                     var arr_source=floor[arr2[0]][arr2[1]].arr_source;//这个房间的资源列表
47                     var obj={};
48                     obj.sourceSide=arr2[2];
49                     obj.sourceType=arr2[3];
50                     obj.sourceUrl=arr2[4];
51                     arr_source.push(obj);
52                 }
53
54             }
55         }
56
57     }
58     return (len);//查找到文件末尾
59 }

经过以上处理,配置文件中的房间信息都被提取到obj_building中。

3、根据房间信息排列源网格的实例,并放置资源。

代码如下:(有一定冗余)

1 function handleBuilding()
  2 {
  3     var len=0;
  4     for(var key in obj_building)
  5     {
  6         len++;//总层数
  7     }
  8     for(var key in obj_building)//对于每一层
  9     {
 10         var int_key=parseInt(key);
 11         var floor=obj_building[key];
 12         //寻找这一层的上下两层,这里假设obj_building是没有顺序的
 13         var int_key_shang=int_key,int_key_xia=int_key;
 14         var floor_shang=null,floor_xia=null;
 15         //for(var i=int_key;i<)
 16         for(var key2 in obj_building)
 17         {
 18             var int_key2=parseInt(key2);
 19             if((int_key2>int_key)&&(int_key_shang==int_key||int_key_shang>int_key2))
 20             {
 21                 int_key_shang=int_key2;
 22                 floor_shang=obj_building[key2];
 23             }
 24             if((int_key2<int_key)&&(int_key_xia==int_key||int_key_xia<int_key2))
 25             {
 26                 int_key_shang=int_key2;
 27                 floor_xia=obj_building[key2];
 28             }
 29         }
 30         for(var i=1;i<=seg_z;i++)//对于本层的每一行房间
 31         {
 32             var row=floor[i+""];
 33             if(row)//如果有这一行
 34             {
 35                 for(var j=0;j<=seg_x;j++)//对于本行的每一个房间
 36                 {
 37                     var room=row[j+""];
 38                     //根据房间的类型不同决定是否要查看其周围的房间
 39                     if(room)
 40                     {//@@@@普通房间,要考虑前后左右的四个房间状态,要考虑资源放置
 41                         if(room.type=="0"||room.type=="#"||room.type=="^")
 42                         {
 43                             //room.arr_source=[];
 44                             //考虑前面
 45                             if(floor[i-1+""]&&floor[i-1+""][j+""])
 46                             {
 47                                 var room2=floor[i-1+""][j+""];
 48                                 if(!room2)//如果没有东西,就是普通墙壁
 49                                 {
 50                                     //网格类型,实例名字,位置,姿态
 51                                     drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez)
 52                                         ,new BABYLON.Vector3(0,0,0))
 53                                 }
 54
 55                                 else if(room2.type=="|"||room2.type=="+")
 56                                 {
 57                                     drawMesh("hole","hole_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez)
 58                                         ,new BABYLON.Vector3(0,0,0))
 59                                 }
 60                                 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁
 61                                 {
 62
 63                                 }
 64                                 else//默认绘制墙壁
 65                                 {
 66                                     drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez)
 67                                         ,new BABYLON.Vector3(0,0,0))
 68                                 }
 69                             }
 70                             else//默认绘制墙壁
 71                             {
 72                                 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez)
 73                                     ,new BABYLON.Vector3(0,0,0))
 74                             }
 75                             //后面
 76                             if(floor[i+1+""]&&floor[i+1+""][j+""])
 77                             {
 78                                 var room2=floor[i+1+""][j+""];
 79                                 if(!room2)//如果没有东西,就是普通墙壁
 80                                 {
 81                                     //网格类型,实例名字,位置,姿态
 82                                     drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez)
 83                                         ,new BABYLON.Vector3(0,0,0))
 84                                 }
 85
 86                                 else if(room2.type=="|"||room2.type=="+")
 87                                 {
 88                                     drawMesh("hole","hole_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez)
 89                                         ,new BABYLON.Vector3(0,0,0))
 90                                 }
 91                                 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁
 92                                 {
 93
 94                                 }
 95                                 else//默认绘制墙壁
 96                                 {
 97                                     drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez)
 98                                         ,new BABYLON.Vector3(0,0,0))
 99                                 }
100                             }
101                             else//默认绘制墙壁
102                             {
103                                 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez)
104                                     ,new BABYLON.Vector3(0,0,0))
105                             }
106                             //左边
107                             if(floor[i+""][j-1+""])
108                             {
109                                 var room2=floor[i+""][j-1+""];
110                                 if(!room2)//如果没有东西,就是普通墙壁
111                                 {
112                                     //网格类型,实例名字,位置,姿态
113                                     drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez)
114                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
115                                 }
116
117                                 else if(room2.type=="-"||room2.type=="+")
118                                 {
119                                     drawMesh("hole","hole_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez)
120                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
121                                 }
122                                 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁
123                                 {
124
125                                 }
126                                 else//默认绘制墙壁
127                                 {
128                                     drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez)
129                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
130                                 }
131                             }
132                             else//默认绘制墙壁
133                             {
134                                 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez)
135                                     ,new BABYLON.Vector3(0,Math.PI/2,0))
136                             }
137                             //右边
138                             if(floor[i+""][j+1+""])
139                             {
140                                 var room2=floor[i+""][j+1+""];
141                                 if(!room2)//如果没有东西,就是普通墙壁
142                                 {
143                                     //网格类型,实例名字,位置,姿态
144                                     drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez)
145                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
146                                 }
147
148                                 else if(room2.type=="-"||room2.type=="+")
149                                 {
150                                     drawMesh("hole","hole_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez)
151                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
152                                 }
153                                 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁
154                                 {
155
156                                 }
157                                 else//默认绘制墙壁
158                                 {
159                                     drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez)
160                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
161                                 }
162                             }
163                             else//默认绘制墙壁
164                             {
165                                 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez)
166                                     ,new BABYLON.Vector3(0,Math.PI/2,0))
167                             }
168                             //上面
169                             if(room.type=="^")
170                             {
171                                 drawMesh("hole","hole_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez)
172                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
173                                 //还要负责向上连接
174                                 if(floor_shang)
175                                 {
176                                     for(var k=int_key+1;k<int_key_shang;k++)
177                                     {
178                                         drawMesh("channel","channel_^_"+k+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(k)*sizey,(-i)*sizez)
179                                             ,new BABYLON.Vector3(Math.PI/2,0,0))
180                                     }
181                                 }
182                                 //暂时不设置弹射器,使用失重模式
183                             }
184                             else
185                             {
186                                 drawMesh("wall","wall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez)
187                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
188                             }
189                             //下面
190                             if(room.type=="#")
191                             {
192                                 drawMesh("hole","hole_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez)
193                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
194                             }
195                             else
196                             {
197                                 drawMesh("wall","wall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez)
198                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
199                             }
200                             //翻转方向会影响碰撞检测吗?
201                             //最后处理资源
202                         }
203                         //@@@@表示通道的三种符号,要考虑其前后左右的位置
204                         else if(room.type=="-"||room.type=="+"||room.type=="|")
205                         {
206                             if(room.type=="-")
207                             {//横向长通道
208                                 drawMesh("channel","channel_-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez)
209                                     ,new BABYLON.Vector3(0,Math.PI/2,0))
210                             }
211                             else if(room.type=="|")
212                             {//纵向长通道
213                                 drawMesh("channel","channel_|_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez)
214                                     ,new BABYLON.Vector3(0,0,0))
215                             }
216                             else
217                             {//十字连接件
218                                 //考虑前面
219                                 if(floor[i-1+""]&&floor[i-1+""][j+""])
220                                 {
221                                     var room2=floor[i-1+""][j+""];
222                                     if(!room2)//如果没有东西,就是普通墙壁
223                                     {
224                                         //网格类型,实例名字,位置,姿态
225                                         drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez)
226                                             ,new BABYLON.Vector3(0,0,0))
227                                     }
228
229                                     else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^")
230                                     {//短通道自带位移
231                                         drawMesh("shortchannel","shortchannel_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez)
232                                             ,new BABYLON.Vector3(0,0,0))
233                                     }
234                                     else//默认绘制小型墙壁
235                                     {
236                                         drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez)
237                                             ,new BABYLON.Vector3(0,0,0))
238                                     }
239                                 }
240                                 else//默认绘制小型墙壁
241                                 {
242                                     drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez)
243                                         ,new BABYLON.Vector3(0,0,0))
244                                 }
245                                 //后面
246                                 if(floor[i+1+""]&&floor[i+1+""][j+""])
247                                 {
248                                     var room2=floor[i+1+""][j+""];
249                                     if(!room2)//如果没有东西,就是普通墙壁
250                                     {
251                                         //网格类型,实例名字,位置,姿态
252                                         drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez)
253                                             ,new BABYLON.Vector3(0,0,0))
254                                     }
255
256                                     else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^")
257                                     {
258                                         drawMesh("shortchannel","shortchannel_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez)
259                                             ,new BABYLON.Vector3(0,Math.PI,0))
260                                     }
261                                     else//默认绘制小型墙壁
262                                     {
263                                         drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez)
264                                             ,new BABYLON.Vector3(0,0,0))
265                                     }
266                                 }
267                                 else//默认绘制小型墙壁
268                                 {
269                                     drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez)
270                                         ,new BABYLON.Vector3(0,0,0))
271                                 }
272                                 //左边
273                                 if(floor[i+""][j-1+""])
274                                 {
275                                     var room2=floor[i+""][j-1+""];
276                                     if(!room2)//如果没有东西,就是小型墙壁
277                                     {
278                                         //网格类型,实例名字,位置,姿态
279                                         drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez)
280                                             ,new BABYLON.Vector3(0,Math.PI/2,0))
281                                     }
282
283                                     else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^")
284                                     {
285                                         drawMesh("shortchannel","shortchannel_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez)
286                                             ,new BABYLON.Vector3(0,-Math.PI/2,0))
287                                     }
288                                     else//默认绘制小型墙壁
289                                     {
290                                         drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez)
291                                             ,new BABYLON.Vector3(0,Math.PI/2,0))
292                                     }
293                                 }
294                                 else//默认绘制小型墙壁
295                                 {
296                                     drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez)
297                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
298                                 }
299                                 //右边
300                                 if(floor[i+""][j+1+""])
301                                 {
302                                     var room2=floor[i+""][j+1+""];
303                                     if(!room2)//如果没有东西,就是普通墙壁
304                                     {
305                                         //网格类型,实例名字,位置,姿态
306                                         drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez)
307                                             ,new BABYLON.Vector3(0,Math.PI/2,0))
308                                     }
309
310                                     else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^")
311                                     {
312                                         drawMesh("shortchannel","shortchannel_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez)
313                                             ,new BABYLON.Vector3(0,Math.PI/2,0))
314                                     }
315                                     else//默认绘制墙壁
316                                     {
317                                         drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez)
318                                             ,new BABYLON.Vector3(0,Math.PI/2,0))
319                                     }
320                                 }
321                                 else//默认绘制墙壁
322                                 {
323                                     drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez)
324                                         ,new BABYLON.Vector3(0,Math.PI/2,0))
325                                 }
326                                 //上面
327                                 drawMesh("smallwall","smallwall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5/3)*sizey,(-i)*sizez)
328                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
329                                 //下面
330                                 drawMesh("smallwall","smallwall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5/3)*sizey,(-i)*sizez)
331                                     ,new BABYLON.Vector3(Math.PI/2,0,0))
332
333                             }
334                         }
335                         //如果这个房间有资源
336                         if(room.arr_source)
337                         {
338                             var arr_source=room.arr_source;
339                             var len=arr_source.length;
340                             for(var k=0;k<len;k++)
341                             {
342                                 var source=arr_source[k];
343                                 if(source.sourceType=="mp4"||source.sourceType=="jpg"||source.sourceType=="png")
344                                 {
345                                     var mesh_plan=new BABYLON.MeshBuilder.CreatePlane(source.sourceType+"_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j,
346                                         {height:4.5,width:8},scene);//建立一个平面网格用来展示资源
347                                     var pos={x:0,y:0,z:0},rot=new BABYLON.Vector3(0,0,0);
348                                     if(source.sourceSide=="z")//根据资源所在的墙壁不同调整资源网格的位置和姿态
349                                     {
350                                         pos.z=0.4
351                                     }else if(source.sourceSide=="z-")
352                                     {
353                                         pos.z=-0.4;
354                                         rot.y=Math.PI;
355                                     }
356                                     else if(source.sourceSide=="x")
357                                     {
358                                         pos.x=0.4;
359                                         rot.y=-Math.PI/2;
360                                     }
361                                     else if(source.sourceSide=="x-")
362                                     {
363                                         pos.x=-0.4;
364                                         rot.y=Math.PI/2;
365                                     }
366                                     else if(source.sourceSide=="y")
367                                     {
368                                         pos.y=0.4;
369                                         rot.x=Math.PI/2;
370                                     }
371                                     else if(source.sourceSide=="z-")
372                                     {
373                                         pos.y=-0.4;
374                                         rot.x=-Math.PI/2;
375                                     }
376                                     mesh_plan.position=new BABYLON.Vector3((j+pos.x)*sizex,(int_key+pos.y)*sizey,(-i+pos.z)*sizez);
377                                     mesh_plan.rotation=rot;
378                                     mesh_plan.renderingGroupId=2;
379                                     if(source.sourceType=="jpg"||source.sourceType=="png")
380                                     {
381                                         var materialf = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene);
382
383                                         materialf.diffuseTexture = new BABYLON.Texture(source.sourceUrl, scene);
384                                         materialf.diffuseTexture.hasAlpha = false;
385                                         materialf.backFaceCulling = true;
386                                         materialf.freeze();
387                                         mesh_plan.material =materialf;
388                                     }
389                                     else if(source.sourceType=="mp4")
390                                     {
391                                         var mat = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene);
392                                         //从Chrome 66开始为了避免标签产生随机噪音禁止没有交互前使用js播放视频,所以后面要监听点击启动播放
393                                         var videoTexture = new BABYLON.VideoTexture("video_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, [source.sourceUrl], scene, true, false);
394                                         //videoTexture.video.autoplay=false;//这两个设置
395                                         //videoTexture.video.muted=true;不起作用
396                                         mat.diffuseTexture = videoTexture;//Babylon.js视频纹理
397                                         mat.emissiveColor=new BABYLON.Color3(1,1,1);
398                                         //监听到交互需求
399                                         // videoTexture.onUserActionRequestedObservable.add(() => {
400                                         //     scene.onPointerDown = function (evt) {
401                                         //         if(evt.pickInfo.pickedMesh == mesh_plan)
402                                         //         {
403                                         //             if(videoTexture.video.paused)
404                                         //             {
405                                         //                 videoTexture.video.play();
406                                         //             }
407                                         //             else
408                                         //             {
409                                         //                 videoTexture.video.pause();
410                                         //             }
411                                         //         }
412                                         //
413                                         //     }
414                                         // });
415                                         //mat.emissiveTexture= videoTexture;
416                                         mesh_plan.material =mat;
417                                         obj_videos[mesh_plan.name]=videoTexture;
418                                         if(false)
419                                         {
420                                             scene.onPointerDown = function (evt) {//这个evt是dom的,不会有pickInfo!!
421                                                 if(evt.pickInfo&&(evt.pickInfo.pickedMesh == mesh_plan))
422                                                 {
423                                                     if(videoTexture.video.paused)
424                                                     {
425                                                         videoTexture.video.play();
426                                                     }
427                                                     else
428                                                     {
429                                                         videoTexture.video.pause();
430                                                     }
431                                                 }
432                                             }
433                                         }
434
435                                     }
436                                 }
437                             }
438                         }
439
440                     }
441                 }
442             }
443         }
444     }
445 }

drawMesh方法用来在指定位置生成指定源网格的实例:

1 function drawMesh(type,name,pos,rot)
2     {
3         var instance=obj_meshclass[type].createInstance(name);
4         instance.position=pos;
5         instance.rotation=rot;
6     }

在完成零件组装后,再根据资源信息向设定的位置添加资源。

4、运动控制与碰撞检测

监听操作者的鼠标和键盘操作:

1 var node_temp;
 2 function InitMouse()
 3 {
 4     canvas.addEventListener("blur",function(evt){//监听失去焦点
 5         releaseKeyStateOut();
 6     })
 7     canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说
 8         releaseKeyStateIn();
 9     })
10
11     //scene.onPointerPick=onMouseClick;//如果不attachControl onPointerPick不会被触发,并且onPointerPick必须pick到mesh上才会被触发
12     canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!!
13         onMouseClick(evt);//
14     }, false);
15     canvas.addEventListener("dblclick", function(evt) {//是否要用到鼠标双击??
16         onMouseDblClick(evt);//
17     }, false);
18     scene.onPointerMove=onMouseMove;//Babylon.js的事件监听属性
19     scene.onPointerDown=onMouseDown;
20     scene.onPointerUp=onMouseUp;
21     scene.onKeyDown=onKeyDown;
22     scene.onKeyUp=onKeyUp;
23     node_temp=new BABYLON.TransformNode("node_temp",scene);//用来提取相机的姿态矩阵
24     node_temp.rotation=camera0.rotation;
25 }

鼠标点击控制视频播放:

1 function onMouseDblClick(evt)
 2 {
 3     var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0);
 4     if(pickInfo.hit)
 5     {
 6         var mesh = pickInfo.pickedMesh;
 7         if(mesh.name.split("_")[0]=="mp4")//重放视频
 8         {
 9             if(obj_videos[mesh.name])
10             {
11                 var videoTexture=obj_videos[mesh.name];
12
13                     videoTexture.video.currentTime =0;
14
15             }
16         }
17     }
18 }
19 function onMouseClick(evt)
20 {
21     var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0);
22     if(pickInfo.hit)
23     {
24         var mesh = pickInfo.pickedMesh;
25         if(mesh.name.split("_")[0]=="mp4")//启停视频
26         {
27             if(obj_videos[mesh.name])
28             {
29                 var videoTexture=obj_videos[mesh.name];
30                 if(videoTexture.video.paused)
31                 {
32                     videoTexture.video.play();
33                 }
34                 else
35                 {
36                     videoTexture.video.pause();
37                 }
38             }
39         }
40     }
41
42 }

在fps模式下通过鼠标移动控制相机视角

1 var lastPointerX,lastPointerY;
 2 var flag_view="free"//free表示默认的自由移动状态,locked表示锁定鼠标的fps模式状态
 3 var flag_locked;
 4 var obj_keystate=[];
 5 function onMouseMove(evt)
 6 {
 7
 8     if(flag_view=="locked")
 9     {
10         evt.preventDefault();
11         //绕y轴的旋转角度是根据x坐标计算的
12         var rad_y=((scene.pointerX-lastPointerX)/window.innerWidth)*(Math.PI/1);//将鼠标位置的变化转化为相机视角的变化
13         var rad_x=((scene.pointerY-lastPointerY)/window.innerHeight)*(Math.PI/1);
14         camera0.rotation.y+=rad_y;
15         camera0.rotation.x+=rad_x;
16     }
17     lastPointerX=scene.pointerX;
18     lastPointerY=scene.pointerY;
19 }
20 function onMouseDown(evt)
21 {
22     if(flag_view=="locked") {
23         evt.preventDefault();
24     }
25 }
26 function onMouseUp(evt)
27 {
28     if(flag_view=="locked") {
29         evt.preventDefault();
30     }
31 }

记录键盘按键状态

1 function onKeyDown(event)
 2 {
 3     if(flag_view=="locked") {
 4         event.preventDefault();
 5         var key = event.key;
 6         obj_keystate[key] = 1;//1表示按下
 7     }
 8 }
 9 function onKeyUp(event)
10 {
11     var key = event.key;
12     if(key=="v"||key=="Escape")//按v键开闭fps模式
13     {
14         event.preventDefault();
15         if(flag_view=="locked")
16         {
17             flag_view="free";
18             document.exitPointerLock();
19         }
20         else if(flag_view=="free")
21         {
22             flag_view="locked";
23             canvas.requestPointerLock();
24         }
25     }
26     if(flag_view=="locked") {
27         event.preventDefault();
28
29         obj_keystate[key] = 0;
30     }
31 }

接下来在渲染循环中根据控制输入确定相机的位移:

var flag_speed=1;
                //var m_view=camera0.getViewMatrix();
                //var m_view=camera0.getProjectionMatrix();
                var m_view=node_temp.getWorldMatrix();
                //只检测其运行方向?-》相对论问题!《-先假设直接外围环境不移动
                if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!!
                {
                    flag_speed=5;//加速移动
                }
                var delta=engine.getDeltaTime();//两渲染帧之间的时间间隔(毫秒)
                //console.log(delta);
                flag_speed=flag_speed*engine.getDeltaTime()/10;
                var v_temp=new BABYLON.Vector3(0,0,0);
                if(obj_keystate["w"]==1)
                {
                    v_temp.z+=0.1*flag_speed;

                }
                if(obj_keystate["s"]==1)
                {
                    v_temp.z-=0.1*flag_speed;
                }
                if(obj_keystate["d"]==1)
                {
                    v_temp.x+=0.05*flag_speed;
                }
                if(obj_keystate["a"]==1)
                {
                    v_temp.x-=0.05*flag_speed;
                }
                if(obj_keystate[" "]==1)
                {
                    v_temp.y+=0.05*flag_speed;
                }
                if(obj_keystate["c"]==1)
                {
                    v_temp.y-=0.05*flag_speed;
                }

                //camera0.position=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,camera0.getWorldMatrix()).subtract(camera0.position));
                //engine.getDeltaTime()
          
                var pos_temp=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,m_view));

根据按键状态和两帧之间的时间计算出相机在这一帧内的位移向量,需要注意的是这个位移向量以相机的局部坐标系为参考,为了在世界坐标系中使用它,建立了一个node_temp节点专门用来保存相机的姿态矩阵,对位移向量施加这个矩阵变化将它转化为世界坐标系中的位移矩阵。

接下来使用射线进行简单的碰撞检测:

1 var direction=pos_temp.subtract(pos_last);//pos_last是上一帧的相机位置,取新位置向量减旧位置向量的结果为物体的运动方向
 2                 //var direction=BABYLON.Vector3.TransformCoordinates(v_temp,m_view);//一次性计算的好处是只需绘制一条射线,缺点是容易射空
 3                 var ray = new BABYLON.Ray(camera0.position, direction, 1);//从camera0.position位置向direction方向,绘制长度为1的'射线’
 4                 var arr=scene.multiPickWithRay(ray);
 5                 arr.sort(sort_compare)//按距离从近到远排序
 6                 var len=arr.length;
 7
 8                 var flag_hit=false;
 9                 for(var k=0;k<len;k++)//对于这条射线击中的每个三角形
10                     {
11                         var hit=arr[k];
12                         var mesh=hit.pickedMesh;
13                         var distance=hit.distance;
14                         if(mesh||mesh.name)//暂不限制mesh种类
15                         {
16                             console.log(mesh.name);
17                             flag_hit=true;
18                             break;
19                         }
20                     }
21                 if(!flag_hit)//如果没有被阻拦,则替换位置
22                 {
23                     camera0.position=pos_temp;
24                 }
25                 else
26                 {
27                     camera0.position=pos_last;//回溯的太远了
28                 }

以上渲染循环中的运动控制代码主要在fps模式下生效,尝试通过在检测到碰撞时调用camera0.position=pos_last;来阻止自由相机穿墙,但效果并不好。

三、总结:

编程结果基本达到设计目标,但在代码冗余、功能细节调试方面尚有不足,接下来可以考虑向程序中添加模型资源作为'雕塑’展示、添加更多类型的零件、添加重力效果、添加WebSocket交互等。

(0)

相关推荐

  • 一个原生JavaScript动画库原型

    设计目标:简单易用,不依赖其他库,对旧版浏览器具有一定兼容性,功能可扩展. 动画调用: 1 <!DOCTYPE html> 2 <html lang="en"&g ...

  • element.ui 省市级联选择器

    原始数据 const obj= { "aa": { "110000": "北京市", "120000": "天 ...

  • 网页小实验——在平面空间建立大量“可思考的”对象

    实验目标:建立大量对象(万级),为每个对象设置自身逻辑,并实现对象之间的交互,以原生DOM为渲染方式.主干在于对象逻辑,可根据需求换用其他渲染方式. 一.html舞台: 1 <!DOCTYPE ...

  • (原创)家庭实验室:小实验——做手机3D全息投影

    小实验--做手机3D全息投影 日期:2017.10.21 星期六 实验人:王佳卉 实验材料:矿泉水瓶.壁纸刀. 实验方法与步骤: 1.用壁纸刀将瓶子切下瓶子前端,并去掉瓶盖. 2.3D投影器就做好了. ...

  • (原创)家庭实验室:小实验——玩转3D素描图

    玩转3D素描图 日期:2017.09.11  星期一 实验人:张嘉宁 实验材料:A4纸.铅笔.彩笔.刻度尺 实验方法与步骤: 1.先在纸上画出一个手的模型(技术不好的可以把手放在纸上拓一个手印下来.) ...

  • Z145.全息3D影盒||亲子科学小实验

    "趣味科学实验"公众号ID:qwkxsy是公益性质的平台,方便广大的青少年交流和学习科学实验.方便家长和孩子一起做一些简单有趣的科学实验,收集幼儿园,小学.初中和高中的科学实验视频 ...

  • 来个小实验,看看你是不是“良性手抖”

    很多手抖病人,在这里给大家讲解一下这个疾病,我们平时的手抖在医学上称为震颤.什么样的手抖是不正常的?我们可以做个小实验来判断.把双手伸直,与肩膀高度保持一致,不要借助任何辅助,在手上面放一张纸,手会有 ...

  • Z172.新能源(盐水)小车||亲子科学小实验

    [实验展示] [实验认知]    新能源小车,就是用盐水进行发电前行的小车,属于绿色能源小车,目前还不能实际应用.通过化学反应(在正负极发生不同的氧化还原反应)使闭合电路中产生电子流,从而产生电流. ...

  • Z171.自制显微镜||亲子科学小实验

    [实验展示] [实验认知]      放大镜是凸透镜的一种,以前人们利用放大镜到的比较小的物体.但是为了看到更小的物体,后利用2个放大镜,将物体的像再次放大,这样就可以 物体了.科学家们通过显微镜首次 ...

  • 【优秀作文】奇妙的小实验

    奇妙的小实验 北京市西城区志成小学三年级四班  王一珑 指导老师  何娜 科学老师曾经对我们说过:"生活中处处有科学!"我对这句话一直都是一知半解,可没想到,语文何老师却让我明白了 ...

  • 家庭化学小实验-脱水水果

    每一种新鲜的水果都含有水分,把水分从水果中分离出来的过程叫做脱水.这是食品经历物理变化的一个例子,也是食品保存的方法之一.物理方法通常是可逆的,这一点与化学变化不同.在这个实验中,你将测试并观察水果脱 ...