介绍

到现在为止,我们都在创建新的着色器材质,但是如果我们想要修改一个Three.js内置的材质呢?或许我们对MeshStandardMaterial的处理结果感到满意,但是希望往里边添加顶点动画。

如果重写整个MeshStandardMaterial,那么处理灯光、环境贴图、基于物理的渲染、所有类型的纹理等将花费太多时间。

相反,我们将从MeshStandardMaterial开始,并尝试将自己的代码集成到其着色器中。
有两种方法可以做到:

  1. 通过在编译着色器前触发Three.js钩子,可以让我们处理着色器并注入自己的代码。
  2. 重新创建一个全新材质,使用与Three.js写的代码相同的参数,然后再加上我们自己的参数。

我们会用第一个方法,将以一种有趣的方式扭曲模型顶点,但材质的所有基本特征仍在工作,如阴影、纹理、法线贴图等。

设置

我们将使用于three.js学习笔记(十三)——真实渲染一样的设置,只不过将模型替换为一个头部模型,它只有一个网格和真实纹理,可以与我们下边的扭曲动画相处得来。
在这里插入图片描述
在加载模型前已经创建好了带有贴图和法向贴图的标准网格材质MeshStandardMaterial,然后在模型的唯一网格上使用该材质,该网格最终被添加到场景中。

下边的代码大都跟材质相关。

材质钩子

我们有MeshStandardMaterial,但想修改它的着色器。

要想修改一种材质,必须先访问其原始着色器,为此我们可以使用材质的onBeforeCompile方法,为其指定一个函数,该函数会在编译着色器程序前执行回调,此函数使用shader源码作为参数,用于修改内置材质:

material.onBeforeCompile = (shader) =>
{
    console.log(shader)
    console.log(shader.uniforms)
    console.log(shader.vertexShader)
    console.log(shader.fragmentShader);
}

在这里插入图片描述
现在我们可以查看vertexShader ,fragmentShaderuniforms 了。

给顶点着色器添加代码

在这里插入图片描述
通过查看顶点着色器代码,每个#include ...都会插入Three.js依赖包里边特定文件夹中的代码,因此我们可以使用js来替换这部分。
进入到/node_modules/three/src/renders/shaders/文件夹中,这是我们能找到的大多数Three.js着色器代码的地方。
include包裹住的部分称为块chunk,可以在ShaderChunk/文件夹中找到它们。
进入某个块js文件中,可以看到begin_vertex通过创建名为transformed的变量来处理位置。
在这里插入图片描述
接下来我们回到js中替换这部分代码:
在y轴上移动模型

material.onBeforeCompile = (shader) =>
{
    shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
        #include <begin_vertex>

        transformed.y += 3.0;
    `
	)
}

在这里插入图片描述
可以观察到确实在y轴方向上移动了,但是阴影出现了些问题,待会再修复,现在移除transformed.y += 3.0;

扭曲

我们将创建一个矩阵来对顶点进行扭曲。
(下边的代码都是位于要替换的glsl中)
首先,我们将尝试以相同的角度旋转所有顶点。然后根据顶点的高度偏移该角度,并为其设置动画。
创建具有任意值的角度变量:
(即便还没有移动顶点,依然可以刷新查看后台是否报错)

#include <begin_vertex>

float angle = 0.3;

在学着色器的时候知道,矩阵就像是一个管道,我们可以在这发送像向量vector这样的数据。管道将对该向量应用变换并输出结果。我们可以创建一个矩阵来缩放顶点,一个来旋转,另一个来移动,甚至可以组合起来,这些矩阵可以处理2D变换、3D变换等等。

在本例中,我们会做一个2D变换,只在x和z轴上旋转顶点进行扭曲,不会在y轴上进行旋转。
使用get2dRotateMatrix函数返回二维矩阵(mat2):
通过The Book of Shaders了解更多

mat2 get2dRotateMatrix(float _angle)
{
    return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
}

我们该把这段代码放在glsl哪个位置?如果我们自己写着色器,则会将其放在main函数前,而在这里,main函数外边的有一个块是common,这个块的优点是它位于所有着色器中,替换它:

material.onBeforeCompile = (shader) =>
{
    // 替换common
    shader.vertexShader = shader.vertexShader.replace(
        '#include <common>',
        `
            #include <common>
            mat2 get2dRotateMatrix(float _angle)
            {
                return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
            }
        `
    )
    
    // 替换begin_vertex
    // ...
}

使用get2dRotateMatrix函数创建rotateMatrix矩阵变量,然后旋转该矩阵:

#include <begin_vertex>
    
float angle = 0.3;
mat2 rotateMatrix = get2dRotateMatrix(angle);
transformed.xz = rotateMatrix * transformed.xz;

在这里插入图片描述
根据顶点高度改变角度:

float angle = position.y * 0.9;

在这里插入图片描述

动画

在函数外部声明一个变量:

const customUniforms = {
  uTime: { value: 0 }
}

跟以往一样,将名为uTimeuniform发送到着色器:

material.onBeforeCompile = function(shader)
{
    shader.uniforms.uTime = customUniforms.uTime

    // ...
}

common块中检索uTime

#include <common>

uniform float uTime;

mat2 get2dRotateMatrix(float _angle)
{
    return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
}

回到动画函数中更新uTime的值

const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update material
    customUniforms.uTime.value = elapsedTime

    // ...
}

在这里插入图片描述

修复阴影

正如阴影那节课所说的,在灯光渲染下,网格材质将被深度网格材质MeshDepthMaterial所替代,而我们并没有修改MeshDepthMaterial。
在后边加一个平面可以将看清楚阴影:
在这里插入图片描述
由于阴影使用的是深度网格材质,我们可以在网格上使用customDepthMaterial属性覆盖该材质以便让Three.js使用自定义材质。
首先,创建一个深度网格材质并设置其 depthPacking属性值为THREE.RGBADepthPacking

const depthMaterial = new THREE.MeshDepthMaterial({
    depthPacking: THREE.RGBADepthPacking
})

加载模型时要使用自定义的深度网格材质depthMaterial

gltfLoader.load(
    '/models/LeePerrySmith/LeePerrySmith.glb',
    (gltf) =>
    {
        // ...

        mesh.material = material // Update the material
        mesh.customDepthMaterial = depthMaterial // Update the depth material

        // ...
    }
)

然后跟前面的材质同理:

depthMaterial.onBeforeCompile = (shader) =>
{
    shader.uniforms.uTime = customUniforms.uTime
    shader.vertexShader = shader.vertexShader.replace(
        '#include <common>',
        `
            #include <common>

            uniform float uTime;

            mat2 get2dRotateMatrix(float _angle)
            {
                return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
            }
        `
    )
    shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        `
            #include <begin_vertex>

            float angle = (position.y + uTime) * 0.9;
            mat2 rotateMatrix = get2dRotateMatrix(angle);

            transformed.xz = rotateMatrix * transformed.xz;
        `
    )
}

现在看平面上的阴影也可以看出在进行扭曲,但是模型上的阴影其实是错误的,看起来阴影在随着顶点旋转,这与法线相关。
在这里插入图片描述

修复法线

当我们旋转顶点时,我们只旋转了位置,但没有旋转法线,因此需要修改处理法线的块。
处理法线的块称为beginnormal_vertex。让我们将其替换为material,记住不是depthMaterial,因为这阴影材质不需要法线:

material.onBeforeCompile = (shader) =>
{
    // ...

    shader.vertexShader = shader.vertexShader.replace(
        '#include <beginnormal_vertex>',
        `
            #include <beginnormal_vertex>
        `
    )

    // ...
}

如果你去查看
/node_modules/three/src/renderers/shaders/ShaderChunks/beginnormal_vertex.glsl.js,会看到法线变量名为objectNormal,因此我们会对其进行扭曲旋转的相同操作:
(记得移除begin_vertex中的anglerotateMatrix以避免重复声明)

material.onBeforeCompile = function(shader)
{
    // ...

    shader.vertexShader = shader.vertexShader.replace(
        '#include <beginnormal_vertex>',
        `
            #include <beginnormal_vertex>

            float angle = (position.y + uTime) * 0.9;
            mat2 rotateMatrix = get2dRotateMatrix(angle);

            objectNormal.xz = objectNormal.xz * rotateMatrix;
        `
    )
    shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        `
            #include <begin_vertex>

            transformed.xz = rotateMatrix * transformed.xz;
        `
    )
}

在这里插入图片描述

源代码

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import * as dat from 'dat.gui'

/**
 * Base
 */
// Debug
const gui = new dat.GUI()

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Loaders
 */
const textureLoader = new THREE.TextureLoader()
const gltfLoader = new GLTFLoader()
const cubeTextureLoader = new THREE.CubeTextureLoader()

/**
 * Update all materials
 */
const updateAllMaterials = () => {
  scene.traverse((child) => {
    if (
      child instanceof THREE.Mesh &&
      child.material instanceof THREE.MeshStandardMaterial
    ) {
      child.material.envMapIntensity = 5
      child.material.needsUpdate = true
      child.castShadow = true
      child.receiveShadow = true
    }
  })
}

/**
 * Environment map
 */
const environmentMap = cubeTextureLoader.load([
  '/textures/environmentMaps/0/px.jpg',
  '/textures/environmentMaps/0/nx.jpg',
  '/textures/environmentMaps/0/py.jpg',
  '/textures/environmentMaps/0/ny.jpg',
  '/textures/environmentMaps/0/pz.jpg',
  '/textures/environmentMaps/0/nz.jpg'
])
environmentMap.encoding = THREE.sRGBEncoding

scene.background = environmentMap
scene.environment = environmentMap

/**
 * Material
 */

// Textures
const mapTexture = textureLoader.load('/models/LeePerrySmith/color.jpg')
mapTexture.encoding = THREE.sRGBEncoding

const normalTexture = textureLoader.load('/models/LeePerrySmith/normal.jpg')
const customUniforms = {
  uTime: { value: 0 }
}
// Material
const depthMaterial = new THREE.MeshDepthMaterial({
  depthPacking: THREE.RGBADepthPacking
})
const material = new THREE.MeshStandardMaterial({
  map: mapTexture,
  normalMap: normalTexture
})
material.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = customUniforms.uTime
  shader.vertexShader = shader.vertexShader.replace(
    '#include <common>',
    `
            #include <common>
            uniform float uTime;
            mat2 get2dRotateMatrix(float _angle)
            {
                return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
            }
        `
  )
  shader.vertexShader = shader.vertexShader.replace(
    '#include <beginnormal_vertex>',
    `
            #include <beginnormal_vertex>
            
            float angle = (position.y + uTime) * 0.9;
            mat2 rotateMatrix = get2dRotateMatrix(angle);

            objectNormal.xz = objectNormal.xz * rotateMatrix;
        `
  )
  shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
            #include <begin_vertex>

            transformed.xz = rotateMatrix * transformed.xz;
        `
  )
}

depthMaterial.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = customUniforms.uTime
  shader.vertexShader = shader.vertexShader.replace(
    '#include <common>',
    `
            #include <common>

            uniform float uTime;

            mat2 get2dRotateMatrix(float _angle)
            {
                return mat2(cos(_angle), - sin(_angle), sin(_angle), cos(_angle));
            }
        `
  )
  
  shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
            #include <begin_vertex>

            float angle = (position.y + uTime) * 0.9;
            mat2 rotateMatrix = get2dRotateMatrix(angle);

            transformed.xz = rotateMatrix * transformed.xz;
        `
  )
}

/**
 * Models
 */
gltfLoader.load('/models/LeePerrySmith/LeePerrySmith.glb', (gltf) => {
  // Model
  const mesh = gltf.scene.children[0]
  mesh.rotation.y = Math.PI * 0.5
  mesh.material = material // Update the material
  mesh.customDepthMaterial = depthMaterial // Update the depth material
  scene.add(mesh)

  // Update materials
  updateAllMaterials()
})

/**
 * Plane
 */
const plane = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(15, 15, 15),
  new THREE.MeshStandardMaterial()
)
plane.rotation.y = Math.PI
plane.position.y = -5
plane.position.z = 5
scene.add(plane)

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.camera.far = 15
directionalLight.shadow.normalBias = 0.05
directionalLight.position.set(0.25, 2, -2.25)
scene.add(directionalLight)

/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight
}

window.addEventListener('resize', () => {
  // Update sizes
  sizes.width = window.innerWidth
  sizes.height = window.innerHeight

  // Update camera
  camera.aspect = sizes.width / sizes.height
  camera.updateProjectionMatrix()

  // Update renderer
  renderer.setSize(sizes.width, sizes.height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  100
)
camera.position.set(4, 1, -4)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  antialias: true
})
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFShadowMap
renderer.physicallyCorrectLights = true
renderer.outputEncoding = THREE.sRGBEncoding
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () => {
  const elapsedTime = clock.getElapsedTime()
  // Update material
  customUniforms.uTime.value = elapsedTime
  // Update controls
  controls.update()

  // Render
  renderer.render(scene, camera)

  // Call tick again on the next frame
  window.requestAnimationFrame(tick)
}

tick()

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐