【threejs开发随笔】利用shaderMaterial制作草地
0.前言
最近的一个threejs项目需要草地的效果,遂参考各路大神的方法制作了一个
1.al-ro
2.spacejack
大体的思路是用shaderMaterial实现顶点动画,模拟草叶随风摇摆的样子,利用InstancedMesh来大规模产生草叶。利用math库里的MeshSurfaceSampler对象来吧草铺到指定mesh上去。
点击查看演示示例
点击下载示例文件
1.先说用法
用法很简单,引入makeGrass对象,然后new一个,传入场景、需要生成草的mesh以及生成草的数量:
const grasses = new makeGrass(
this.scene, // 场景
model.scene.getObjectByName('平面') as Mesh, // 需要生草的mesh,类型需为Mesh
250000 // 生草数量
)
然后在渲染循环中执行makeGrass实例的公共方法update()
...
this.renderer.setAnimationLoop(() => { this.renderLoop() }) 执行渲染循环方法。
...
// 渲染循环
private renderLoop () {
this.renderer.render(this.scene, this.camera)
this.grasses?.update()
this.controls?.update()
}
2. 再说实现
首先我们新建一个makeGrass.ts文件,导入所需依赖
import { Clock, DoubleSide, InstancedMesh, Mesh, Object3D, PlaneGeometry, Scene, ShaderMaterial, Vector3 } from "three"
import { MeshSurfaceSampler } from "three/examples/jsm/math/MeshSurfaceSampler"
新建一个makeGrass类
export default class makeGrass {
private scene:Scene
private mesh:Mesh
private leavesMaterial: ShaderMaterial
private clock = new Clock
private sampler!:MeshSurfaceSampler
private grassesAmount!:number
constructor(scene:Scene,mesh:Mesh,grassesAmount:number,) {
this.scene = scene
this.mesh = mesh
}
}
写一个方法用来初始化草叶的shader材质并返回
private initleavesMaterial() {
const vertexShader = `
varying vec2 vUv;
uniform float time;
// 噪波
float N (vec2 st) {
return fract( sin( dot( st.xy, vec2(12.9898,78.233 ) ) ) * 43758.5453123);
}
float smoothNoise( vec2 ip ){
vec2 lv = fract( ip );
vec2 id = floor( ip );
lv = lv * lv * ( 3. - 2. * lv );
float bl = N( id );
float br = N( id + vec2( 1, 0 ));
float b = mix( bl, br, lv.x );
float tl = N( id + vec2( 0, 1 ));
float tr = N( id + vec2( 1, 1 ));
float t = mix( tl, tr, lv.x );
return mix( b, t, lv.y );
}
void main() {
vUv = uv;
float t = time * 2.;
// 顶点位置
vec4 mvPosition = vec4( position, 1.0 );
#ifdef USE_INSTANCING
mvPosition = instanceMatrix * mvPosition;
#endif
// 移动
float noise = smoothNoise(mvPosition.xz * 0.5 + vec2(0., t));
noise = pow(noise * 0.5 + 0.5, 2.) * 2.;
// 叶片顶部晃动力度.
float dispPower = 1. - cos( uv.y * 3.1416 * 0.5 );
float displacement = noise * ( 0.3 * dispPower );
mvPosition.z -= displacement;
//
vec4 modelViewPosition = modelViewMatrix * mvPosition;
gl_Position = projectionMatrix * modelViewPosition;
}
`;
const fragmentShader = `
varying vec2 vUv;
void main() {
vec3 baseColor = vec3( 0.41, 1.0, 0.5 );
float clarity = ( vUv.y * 0.5 ) + 0.5;
gl_FragColor = vec4( baseColor * clarity, 1 );
}
`;
const uniforms = {
time: {
value: 0
}
}
const leavesMaterial = new ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
side: DoubleSide
});
return leavesMaterial
}
在构造函数中调用initleavesMaterial方法并赋值给类实例leavesMaterial
this.leavesMaterial = this.initleavesMaterial()
初始化网格表面取样器,用来在需要生成草的mesh上随机取点,通过取到的点的位置来生成草
private initSampler(){
this.sampler = new MeshSurfaceSampler(this.mesh)
.setWeightAttribute(null)
.build()
}
在构造函数中调用initSampler方法
this.initSampler()
写一个生成草的方法makegrasses
private makegrasses(grassesAmount:number){
const instanceNumber = grassesAmount;
const dummy = new Object3D();
const geometry = new PlaneGeometry( 0.1, 1, 1, 1 );
geometry.translate( 0, 0.5, 0 ); // 吧草叶的最低点设置到0.
const instancedMesh = new InstancedMesh( geometry, this.leavesMaterial, instanceNumber );
this.scene.add(instancedMesh)
const _position = new Vector3();
const_normal = new Vector3();
for ( let i=0 ; i<instanceNumber ; i++ ) {
this.sampler.sample(_position,_normal)
_normal.add(_position)
dummy.position.set(
_position.x,
_position.y,
_position.z
)
dummy.scale.setScalar( 0.2 + Math.random() *0.6 );
dummy.rotation.y = Math.random()* Math.PI;
dummy.updateMatrix();
instancedMesh.setMatrixAt( i, dummy.matrix );
}
}
这里的草叶模型我们实例化一个简单的PlaneGeometry,heightSegments参数控制草叶的高度分段,分段越多草叶在摇摆的时候的弧度越精细。而分段越多场景的总面数也就越多,出于性能考虑我分段只给了1,这样草叶是笔直的在晃动。
最后我们写一个公共方法update,用来刷新草叶动画。
public update(){
this.leavesMaterial.uniforms.time.value = this.clock.getElapsedTime();
this.leavesMaterial.uniformsNeedUpdate = true;
}