前言

  最近制作three.js的项目,场景可能需要大量的花草树木进行装饰,但是如果采用模型去修饰的话,工作量很大,而且加载负荷也很重,如何才能在性能和效果上面做好平衡,这就是核心问题。

Maya给我的灵感

  显然用实体的模型是不现实的,一棵还算客观的树至少也要有上千个面,然而当这个面数乘以几百之后,无谓的负荷将会让场景卡到无法动弹。

  因此要用面片贴图去实现树的效果,而Maya自带的Paint Effect效果就提供了很好的参考。

操作截图

  用paint effect的笔刷在视图中画一笔,就可以生成一堆面片。
操作截图

  需要注意的是paint effect并不是多边形,它不能直接导出,只能用maya 软件渲染器去渲染,因此需要将它转成多边形。
  转成多边形后按6键可以在视图中看到树木面片。
操作截图

  奇迹般的效果出来了,当你转动视图的时候,面片会自动朝向摄像机。
  遗憾的是,当你删除了转化历史之后,所有的效果就消失了,所以当然这种效果也是不可能通过模型导出来的。

操作截图

  重点是这里提供了操作思路。只要面片是朝向摄像机的,就可以实现不错的伪3D效果。另外也可以在这里获取Maya的树木贴图,省得网上找了。

Three.js的sprite实现

  如何在Three.js实现图片永远朝向摄像机呢,我首先想到了sprite,以前做Maya的粒子效果的时候有接触过这种形式的贴图效果,印象深刻。

  首先的了解sprite在Three.js怎么用 - 链接

  看过之后其实很简单,链接图片,添加材质,即可生成。和生成方块的流程差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//核心代码
var treeMap = textureLoader.load("oakTree.png");//加载树的贴图

var geometry = new THREE.PlaneBufferGeometry( 50, 50, 32 );
var material = new THREE.MeshLambertMaterial( {color: 0x333333, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
plane.rotation.x = Math.PI/2;//旋转地面
scene.add( plane );//添加地面

spriteMaterial = new THREE.SpriteMaterial( { side:THREE.DoubleSide,map:treeMap,transparent:true,alphaTest:0.20 } );
var sprite = new THREE.Sprite( spriteMaterial );
sprite.scale.set(10,10,1);
sprite.position.set(0,5,0);
scene.add(sprite);//添加树sprite

这个效果还是有不少遗憾

  • 不够立体。
  • 和Maya的不一样,Maya的效果只在水平方向指向摄像机,sprite是无时无刻都指向摄像机,看起来就很奇怪。
  • 定位麻烦

进阶解决方案

  既然如此,那就考虑如何实现Maya的效果吧。所以后面我摈弃了sprite的使用,还是改为使用多边形面片。

  多边形面片的好处是可以实现模型定位。

  后面我尝试将Maya的模型面片导出,看看实现的效果。

  我在代码上实现了效果,但是透明上出了大问题。

操作截图

透明遮挡问题的解决方案

  在Three.js的编辑器可以更加透彻方便地看到这里的问题。

操作截图

  这里并不是完全看不到透明背后的部分,只是渲染顺序不同导致透明的部分渲染后看不到背后的物体,这种情况下有些透明的面片先渲染,背后的物体还没有出来,透明部分后面的模型就看不到了。(如下图)

操作截图

  这种问题怎么解决呢?!Three.js的新版编辑器提供了render order选项,通过调节渲染顺序可以实现正确的渲染效果

操作截图

  这样子的话岂不是每个物体都要去手动调整,即便是用代码智能实现也要去研究物体到摄像机的距离,进而分配render order,这个工作量得干死人的。
  当时真的慌得一匹。

  我记得之前在弄房产信息Demo的时候也有遇到过类似的问题,当时网上找到的信息是去调整depthwrite的材质属性。
  于是我赶紧在编辑器里面测试,但是开了之后效果惊为天人,透明完全错乱了。

操作截图

  我算是想尽办法了,不知道该怎么解决,回到编辑器看一下吧,在材质属性上看到alphaTest属性,想着也和透明有关系,就试着调了一下。

操作截图

  没想到问题就解决了,加上alphaTest的数值之后,就不用考虑渲染顺序的问题了。

操作截图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//核心代码

//加载模型
var path = 'model/obj/treeSprite.obj';
var mtl = 'model/obj/treeSprite.mtl';
var model_source_1= new app.getModel(path,mtl);
scene.add(model_source_1);

var treeMap = textureLoader.load("oakTree.png");//加载树的贴图

//在manager中模型加载完成之后执行,如果直接执行的话,模型还没加载,会找不到模型的子对象
for(let num in model_source_1.children[0].children){

//将模型的材质替换 开启双面、贴图贴上树、开启透明
model_source_1.children[0].children[num].material = new THREE.MeshBasicMaterial({side:THREE.DoubleSide,map:treeMap,transparent:true});
//关闭depthWrite 会让透明物体互相穿透 效果很魔性
// model_source_1.children[0].children[num].material.depthWrite = false;

//开启alphaTest解决透明遮挡问题
model_source_1.children[0].children[num].material.alphaTest = 0.2;
}

立体效果制作

  上面的实现效果,模型始终还是个面片呀,怎么才能实现立体的效果呢?
  先复制几个面片看看效果吧。
  这次就尝试用Three.js的面片来实现,不用考虑模型导入那么麻烦。

操作截图

  这个效果并不理想,一看就知道是几个面片组成的树(:з」∠)
  这时我想到了depthWrite的魔性效果,可能穿插的透明感觉正好可以混淆这里的立体效果

操作截图

  的确开了之后,效果确实不错,只是能感觉到有一个面片在旋转,

  现在要考虑怎么实现在镜头移动的时候,树可以保持望向镜头,这样看起来就不会那么奇怪。
  怎么实现类似于Maya的面片效果呢?
  网上搜了一下还真找到了方案

  两个链接的核心处理就是 mesh.lookAt( camera.position );
  但是我弄上去之后的效果就和sprite实现的效果是一样的了。

  为了解决这个问题,我又去查了lookAt函数的用法。

操作截图

  给lookat的Y轴方向传入0参数就好了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//核心代码
var geometry = new THREE.PlaneBufferGeometry( 50, 50, 1 );
var material = new THREE.MeshLambertMaterial( {color: 0x333333, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
plane.rotation.x = Math.PI/2;
scene.add( plane );//生成地板


var geometry = new THREE.PlaneBufferGeometry( 10, 10, 1 );
var material = new THREE.MeshLambertMaterial({side:THREE.DoubleSide,map:treeMap,transparent:true,alphaTest:0.2});

material.depthWrite = false;

let treeOrigin = new THREE.Mesh( geometry, material ); //生成原始的面片模型

//批量复制面片
let amount =6;

for(let i = 0;i<amount;i++){

let tree = treeOrigin.clone();

tree.rotation.y = Math.PI/amount*i;//根据复制的数量自动旋转
//弧度转角度 Math.PI 为 180 度
//绕面片中心旋转,只需要转半圈 180 度

treeGrp.add(tree);

}

treeGrp.position.y = 5;

scene.add( treeGrp );

function update(){//这里只是演示,update函数当然也包括其他的代码
treeGrp.lookAt( camera.position.x,0,camera.position.z );
}

顶部贴图

  现在效果已经算是比较完美了,但是移动到顶部的时候,还是会暴露物体是由面片构成的。
  如此就需要有一张树顶部的贴图了,同时又要求颜色基本保持一致,那就只能在原先的树贴图上用图章进行处理了。
  弄好之后将水平贴到树的中心。
  并且给它加入朝向摄像机的效果。

最终效果

  将顶部贴图的透明度调节为0.5,这样不会显得违和,效果非常棒。

总结

  这是我搭建出来的面数压到最低的生成效果,一棵树用了7个面片,总计14个三角形,另外还用了两张贴图。
  用种方法搭建树林都毫无压力,缺点就是不能支持阴影,如果需要点击处理的话也会很麻烦而已。