赞
踩
到现在为止,我们都在创建新的着色器材质,但是如果我们想要修改一个Three.js内置的材质呢?或许我们对MeshStandardMaterial
的处理结果感到满意,但是希望往里边添加顶点动画。
如果重写整个MeshStandardMaterial
,那么处理灯光、环境贴图、基于物理的渲染、所有类型的纹理等将花费太多时间。
相反,我们将从MeshStandardMaterial
开始,并尝试将自己的代码集成到其着色器中。
有两种方法可以做到:
我们会用第一个方法,将以一种有趣的方式扭曲模型顶点,但材质的所有基本特征仍在工作,如阴影、纹理、法线贴图等。
我们将使用于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
,fragmentShader
和uniforms
了。
通过查看顶点着色器代码,每个#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 }
}
跟以往一样,将名为uTime
的uniform
发送到着色器:
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
中的angle
和rotateMatrix
以避免重复声明)
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()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。