【threejs开发随笔】利用shaderMaterial制作草地

【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,这样草叶是笔直的在晃动。
heightSegments = 10heightSegments = 1

最后我们写一个公共方法update,用来刷新草叶动画。

  public update(){
    this.leavesMaterial.uniforms.time.value = this.clock.getElapsedTime();
    this.leavesMaterial.uniformsNeedUpdate = true;
  }
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值