three.js学习笔记(十八)——调整材质
介绍
到现在为止,我们都在创建新的着色器材质,但是如果我们想要修改一个Three.js内置的材质呢?或许我们对MeshStandardMaterial
的处理结果感到满意,但是希望往里边添加顶点动画。
如果重写整个MeshStandardMaterial
,那么处理灯光、环境贴图、基于物理的渲染、所有类型的纹理等将花费太多时间。
相反,我们将从MeshStandardMaterial
开始,并尝试将自己的代码集成到其着色器中。
有两种方法可以做到:
- 通过在编译着色器前触发Three.js钩子,可以让我们处理着色器并注入自己的代码。
- 重新创建一个全新材质,使用与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
,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()
更多推荐
所有评论(0)