threejs-onBeforeCompile解析


前言

threejs中提供了shaderMaterial和RawShaderMaterial两种材质,开发者可以自定义顶点着色器和片元着色器,这种做法相当于从头开始写shader,但是在某些场景中,我们希望在threejs自带的材质的基础上增加其它的显示效果,典型的如对导入的三维白膜加入特殊的可视化效果,或者保留某些特殊的特效,threejs Material类的onBeforeCompile方法为我们提供了添加shader的手段。


onBeforeCompile用法

从网上找到一份开源的gltf数据,将其导入,默认材质是MeshStandardMaterial,为它赋予一个默认颜色。

const gltfLoader = new GLTFLoader()
  gltfLoader.load(city, (gltf) => { // gltfLoader
    gltf.scene.traverse((meshItem) => {
      if (item.material) { // 去除非Mesh的Object3D对象
        item.material.color =  new THREE.Color("#00094d")
        console.log(meshItem);
        handleDealyMaterial(meshItem);
      }
    });
    scene.add(gltf.scene);
  });
  });

.onBeforeCompile会自动获取材质的shader,参考官网的例子,打印shader查看其数据结构,发现有很多属性,主要用到了其中的vertexShader,fragmentShader,uniforms三项,与自定义着色器材质一样:

const handleDealyMaterial= (mesh) => {
  mesh.material.onBeforeCompile = (shader) => {
    console.log('shader------', shader);
    console.log('vertexShader------', shader.vertexShader);
    console.log('fragmentShader------', shader.fragmentShader);
  };
}

在这里插入图片描述
在这里插入图片描述

由上图可知顶点着色器和片元着色器都是以字符串的形式存储的,打印后如下,由于截图太长就将它们放在代码块中。

vertexShader------
 #define STANDARD
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
	varying vec3 vNormal;
	#ifdef USE_TANGENT
		varying vec3 vTangent;
		varying vec3 vBitangent;
	#endif
#endif
#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
	#include <uv_vertex>
	#include <uv2_vertex>
	#include <color_vertex>
	#include <beginnormal_vertex>
	#include <morphnormal_vertex>
	#include <skinbase_vertex>
	#include <skinnormal_vertex>
	#include <defaultnormal_vertex>
#ifndef FLAT_SHADED
	vNormal = normalize( transformedNormal );
	#ifdef USE_TANGENT
		vTangent = normalize( transformedTangent );
		vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );
	#endif
#endif
	#include <begin_vertex>
	#include <morphtarget_vertex>
	#include <skinning_vertex>
	#include <displacementmap_vertex>
	#include <project_vertex>
	#include <logdepthbuf_vertex>
	#include <clipping_planes_vertex>
	vViewPosition = - mvPosition.xyz;
	#include <worldpos_vertex>
	#include <shadowmap_vertex>
	#include <fog_vertex>
}

fragmentShader------ 
#define STANDARD
#ifdef PHYSICAL
	#define REFLECTIVITY
	#define CLEARCOAT
	#define TRANSMISSION
#endif
uniform vec3 diffuse;
uniform vec3 emissive;
uniform float roughness;
uniform float metalness;
uniform float opacity;
#ifdef TRANSMISSION
	uniform float transmission;
#endif
#ifdef REFLECTIVITY
	uniform float reflectivity;
#endif
#ifdef CLEARCOAT
	uniform float clearcoat;
	uniform float clearcoatRoughness;
#endif
#ifdef USE_SHEEN
	uniform vec3 sheen;
#endif
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
	varying vec3 vNormal;
	#ifdef USE_TANGENT
		varying vec3 vTangent;
		varying vec3 vBitangent;
	#endif
#endif
#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <transmissionmap_pars_fragment>
#include <bsdfs>
#include <cube_uv_reflection_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_physical_pars_fragment>
#include <fog_pars_fragment>
#include <lights_pars_begin>
#include <lights_physical_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <clearcoat_pars_fragment>
#include <roughnessmap_pars_fragment>
#include <metalnessmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
	#include <clipping_planes_fragment>
	vec4 diffuseColor = vec4( diffuse, opacity );
	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
	vec3 totalEmissiveRadiance = emissive;
	#ifdef TRANSMISSION
		float totalTransmission = transmission;
	#endif
	#include <logdepthbuf_fragment>
	#include <map_fragment>
	#include <color_fragment>
	#include <alphamap_fragment>
	#include <alphatest_fragment>
	#include <roughnessmap_fragment>
	#include <metalnessmap_fragment>
	#include <normal_fragment_begin>
	#include <normal_fragment_maps>
	#include <clearcoat_normal_fragment_begin>
	#include <clearcoat_normal_fragment_maps>
	#include <emissivemap_fragment>
	#include <transmissionmap_fragment>
	#include <lights_physical_fragment>
	#include <lights_fragment_begin>
	#include <lights_fragment_maps>
	#include <lights_fragment_end>
	#include <aomap_fragment>
	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
	#ifdef TRANSMISSION
		diffuseColor.a *= mix( saturate( 1. - totalTransmission + linearToRelativeLuminance( reflectedLight.directSpecular + reflectedLight.indirectSpecular ) ), 1.0, metalness );
	#endif
	gl_FragColor = vec4( outgoingLight, diffuseColor.a );
	#include <tonemapping_fragment>
	#include <encodings_fragment>
	#include <fog_fragment>
	#include <premultiplied_alpha_fragment>
	#include <dithering_fragment>
}

以上即为MeshStandardMaterial所对应的顶点着色器和片元着色器,那么他们在threejs底层是如何调用的呢。
首先,threejs存储了一个名为shaderChunck的映射,为不同的材质赋予不同的shader。
在这里插入图片描述

shaderChunck中的每对key-val都存储了一段着色器字符串。它们可以是单一的着色器代码,也可能是包括了其它的引用。
在这里插入图片描述
在这里插入图片描述
或者
在这里插入图片描述
在这里插入图片描述

使用时,使用replace方法将要新增的代码加进去,同字符串的replace方法一样

 shader.fragmentShader = shader.fragmentShader.replace(
    "#include <common>",
    `
      #include <common>
      // 自定义shader
    `
  );

使用示例

更改白膜

将原本的gltf模型根据高度设置一个渐变色,过程参考之前的文章,需要注意以下几点:

  • varying声明的变量需要在顶点着色器和片元着色器中同时声明,将顶点着色器的信息传达给片元。varying的传递在main函数外,变量传递在main函数中
  • uiform变量将外部的变量(js中定义的)传递给片元着色器,同样是在main函数外接收uiform变量,main函数中使用变量
  • 使用replace方法时,最好把新加入的着色器放在最后面,防止新增的shader被抵消(个人踩坑经历)

在handleDealyMaterial中加入以下函数,根据打印的着色器shader决定在何处指向replace方法,代码如下:

const changeShaderByHeight = (shader, mesh) => {
  // 计算几何的边界框,更新boundingBox属性
  mesh.geometry.computeBoundingBox();
  // console.log(mesh.geometry.boundingBox); => Box3包围盒
  const { min, max } = mesh.geometry.boundingBox;
  // 物体的高度范围
  const uHeight = max.y - min.y;
  shader.uniforms.uHeight = {
    value: uHeight,
  };
  // 顶部颜色
  shader.uniforms.uTopColor = {
    value: new THREE.Color("#eca729"),
  };
  
  // 顶点着色器传递模型坐标给片元坐标系
  shader.vertexShader = shader.vertexShader.replace(
    "#include <common>",
    `
      #include <common>
      varying vec3 vPosition;
      `
  );
  // 在顶点着色器的主函数中为将要传递给片元着色器的varying变量赋值
  // position 是threejs底层定义好的模型坐标
  shader.vertexShader = shader.vertexShader.replace(
    "#include <fog_vertex>",
    `
      #include <fog_vertex>
      vPosition = position;
  `
  );

  // 片元着色器接收: 1 外部传入的uniform变量;2 来自顶点着色器的varying变量
  shader.fragmentShader = shader.fragmentShader.replace(
    "#include <common>",
    `
      #include <common>
      uniform vec3 uTopColor;
      uniform float uHeight;
      varying vec3 vPosition;
    `
  );
  // 在片元着色器的主函数中
  shader.fragmentShader = shader.fragmentShader.replace(
    "#include <dithering_fragment>",
    `
      #include <dithering_fragment>
      vec4 distGradColor = gl_FragColor;
      // 设置渐变色比例
      float gradMix = (vPosition.y+uHeight/2.0)/uHeight;
      // 设置渐变效果 mix(a,b,r) = (1-r)*a + br
      vec3 gradMixColor = mix(distGradColor.xyz,uTopColor,gradMix);
      // 片元赋色
      gl_FragColor = vec4(gradMixColor,1);
      // next
      `
  );
}

修改前后:
在这里插入图片描述
在这里插入图片描述

添加扩散效果

1 使用shaderMaterial实现

使用shaderMaterial主要是通过定义CylinderBufferGeometry几何体,然后根据时间不断改变它的scale,光圈根据距离和高度修改透明度。这里把它封装为一个类,将scene、光圈半径、高度、位置传入构造器,使用requestAnimationFrame方法进行动画渲染。
类lightRing:

import * as THREE from 'three'

export default class LightRing {
  /**
   * @param {*} scene threejs 场景
   * @param {*} radius 光圈半径
   * @param {*} height 光圈高度
   * @param {*} position 中心坐标
   */
  constructor(scene, radius, height = 1, position) {
    this.cylinderGeom = new THREE.CylinderBufferGeometry(
      radius, radius, height, 64
    )
    
    this.scene = scene;
    // 默认增加半径
    this.cylinderRadius = 0;

    // 光圈中心坐标
    this.x = position.x;
    this.y = height / 2;
    this.z = position.z;

    // 设置材质
    this.material = new THREE.ShaderMaterial({
      vertexShader: `
      varying vec3 vPosition;

      void main(){
          vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
          gl_Position = projectionMatrix * viewPosition;
          vPosition = position;
      }
      `,
      fragmentShader: `
      varying vec3 vPosition;
      uniform float uHeight;
      void main(){
        float gradMix = (vPosition.y+uHeight/2.0)/uHeight;
        gl_FragColor = vec4(1,0,0,1.0-gradMix);
      }
      `,
      transparent: true,
      side: THREE.DoubleSide,
    })
    // 设置模型、位置
    this.lightRingMesh = new THREE.Mesh(this.cylinderGeom, this.material);
    this.lightRingMesh.position.set(this.x, this.y, this.z);

    this.lightRingMesh.geometry.computeBoundingBox();

    const { min, max } = this.lightRingMesh.geometry.boundingBox;
    //  设置物体高差
    let uHeight = max.y - min.y;
    this.material.uniforms.uHeight = {
      value: uHeight,
    };

    scene.add(this.lightRingMesh)
    // 存储计时器
    this.timer = null;

    this.render()
  }

  // 动画函数
  animate() {
    // 每帧都让它的半径增加0.005
    this.cylinderRadius += 0.005;
    // 当半径大于1时,重新开始
    if (this.cylinderRadius > 1) {
      this.cylinderRadius = 0;
    }
    if (this.lightRingMesh) { // 进行缩放半径扩大,y轴不变
      this.lightRingMesh.scale.set(this.cylinderRadius, 1, this.cylinderRadius);
    }
  }

  // 渲染入口
  render() {
    this.timer = requestAnimationFrame(() => { this.render() })
    this.animate()
  }

  // 移除光墙
  remove() {
    this.scene.remove(this.lightRingMesh);
    this.cylinderGeom.dispose();
    this.material.dispose();
  }
}

效果:
在这里插入图片描述

2 使用onBeforeCompile方法

直接上代码,把addLightRing方法放在handleDealMaterial中即可,代码如下:

const handleDealMaterial = (mesh) => {
  mesh.material.onBeforeCompile = (shader) => {
    console.log('shader------', shader);
    console.log('vertexShader------', shader.vertexShader);
    console.log('fragmentShader------', shader.fragmentShader);
	...
    changeShaderByHeight(shader, mesh);
    addLightRing(shader);
    ...
  };
}

...
const addLightRing = (shader, center = new THREE.Vector2(0, 0)) => {
  // 设置扩散的中心点
  shader.uniforms.u_Center = { value: center };
  // 扩散的时间
  shader.uniforms.u_Time = { value: 0 };
  // 设置条带的宽度
  shader.uniforms.u_Width = { value: 1000 };
  // 设置半径
  shader.uniforms.R = { value: 0 };
  // main函数外接收外部变量
  shader.fragmentShader = shader.fragmentShader.replace(
    "#include <common>",
    `
      #include <common>
      uniform vec2 u_Center;
      uniform float u_Time;
      uniform float u_Width;
      uniform float R;
    `
  );
  // 效果一 圆环扩散
  shader.fragmentShader = shader.fragmentShader.replace(
    "// next",
    `
    float radius = distance(vPosition.xz,u_Center);
    //  扩散范围的函数
    float spreadIndex = -(radius-u_Time) * (radius-u_Time) + radius;
    float a = pow(spreadIndex / radius, 2.0); 
    if(spreadIndex > 0.0) {
        gl_FragColor = mix(gl_FragColor,vec4(1,0,0,1),a);
    }
    // next
    `
  );
  // 效果二 圆形扩散,内部透明度渐变
  // shader.fragmentShader = shader.fragmentShader.replace(
  //   "// next",
  //   `
  //   float R_ = R;
  //   float radius = distance(vPosition.xz,u_Center);
 
  //   if(radius < R_) {
  //     float a = radius /  R_;
  //     a = pow(a , 2.0);
  //     gl_FragColor = mix(gl_FragColor,vec4(1,0,0,1),a);
  //   }

  //   // next
  //   `
  // );

// 这里使用了gsap这个轻量库
  gsap.to(shader.uniforms.u_Time, {
    value: 1000,  // 从初始值到该值为界限范围
    duration: 5,  // 5s一个周期
    ease: "power1.out", // 扩散速率函数
    repeat: -1, // 不断重复这个效果
  });
  gsap.to(shader.uniforms.R, {
    value: 500,
    duration: 5,
    ease: "power1.out",
    repeat: -1,
  });
}

效果一
在这里插入图片描述
效果二

在这里插入图片描述

参考

老陈打码threejs
https://blog.csdn.net/qw8704149/article/details/117869186

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值