问题
让我们从以下示例开始。
如果尚未设置项目,请从此处克隆 Three.js 模板并复制以下代码。
js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 设置背景颜色。
renderer.setClearColor(0xfefefe);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置轨道控制以移动相机。
const orbit = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, -2, 300);
camera.lookAt(0, 0, 0);
const ambLight = new THREE.AmbientLight(0xa3a3a3, 0.8);
scene.add(ambLight);
const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(2, 2, 8);
scene.add(dirLight);
const geo = new THREE.IcosahedronGeometry(4, 30);
const material1 = new THREE.MeshPhysicalMaterial();
for (let i = 0; i < 500; i++) {
const mesh = new THREE.Mesh(geo, material1);
mesh.position.x = Math.random() * 1000 - 500;
mesh.position.y = Math.random() * 1000 - 500;
mesh.position.z = Math.random() * 1000 - 500;
mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1;
scene.add(mesh);
}
function animate() {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
window.addEventListener('resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
完整示例。
我们有 500 个网格,都使用 MeshPhysicalMaterial
,可以清楚地看到光线如何影响它们。
现在,假设我们根据视窗中的位置动态着色它们。
为此,我们需要使用自定义材质。
首先,我们将创建一个变量,例如分辨率。此外,我们还将创建一个 ShaderMaterial
实例。
js
const uniforms = {
u_resolution: {
value: new THREE.Vector2(window.innerWidth, window.innerHeight),
},
};
const material2 = new THREE.ShaderMaterial({
uniforms,
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
});
const geo = new THREE.IcosahedronGeometry(4, 30);
// const material1 = new THREE.MeshPhysicalMaterial();
for (let i = 0; i < 500; i++) {
// const mesh = new THREE.Mesh(geo, material1);
const mesh = new THREE.Mesh(geo, material2);
mesh.position.x = Math.random() * 1000 - 500;
mesh.position.y = Math.random() * 1000 - 500;
mesh.position.z = Math.random() * 1000 - 500;
mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1;
scene.add(mesh);
}
当然,我们还需要创建顶点和片段着色器。
html
<script id="vertexshader" type="vertex">
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script id="fragmentshader" type="fragment">
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = vec4(vec3(st.x / 0.6, st.y / 1.8, (st.x + st.y) / 1.2), 1.0);
}
</script>
一切似乎都正常......对吗?
实际上并非如此。
正如你所见,我们失去了一个重要细节:球体如何响应场景中的光线。
在 Three.js 中使用 ShaderMaterial
时,你不仅会失去着色,还会失去金属度、粗糙度、反射等重要信息。
"那么,如何在保持这些重要细节的同时创建自定义材质呢?"
我们有三种选择。
第一种是在片段着色器中手动处理计算。
第二种是通过使用 onBeforeCompile()
替换现有材质的部分 GLSL 代码来扩展它。
第三种与第二种类似,但我们使用一个库。这正是我们要做的。
Three 自定义着色器材质
Three 自定义着色器材质是一个库,允许你在现有材质的基础上构建新材料。
首先,我们通过在终端中运行以下命令来安装该库:
npm install three-custom-shader-material
。
接下来,我们导入它。
javascript
import CustomShaderMaterial from 'three-custom-shader-material/vanilla';
注意:如果你使用的是标准 Three.js 而不是 React Three Fiber,请确保在路径中添加 '/vanilla'。
现在,我们只需创建一个 CustomShaderMaterial
的实例,并使用 baseMaterial
属性扩展材质。
js
const material3 = new CustomShaderMaterial({
baseMaterial: THREE.MeshPhysicalMaterial,
});
js
// const mesh = new THREE.Mesh(geo, material2);
const mesh = new THREE.Mesh(geo, material3);
正如你所见,一切如预期般工作。此外,我们还可以设置 MeshPhysicalMaterial
的颜色、贴图等属性。
js
const material3 = new CustomShaderMaterial({
baseMaterial: THREE.MeshPhysicalMaterial,
color: 0xffea00,
metalness: 0.6,
roughness: 0.4,
});
现在,让我们回到我之前的例子。
我们将设置片段着色器并传递分辨率变量。
js
const material3 = new CustomShaderMaterial({
baseMaterial: THREE.MeshPhysicalMaterial,
color: 0xffea00,
metalness: 0.6,
roughness: 0.4,
fragmentShader: document.getElementById('fragmentshader').textContent,
uniforms: {
u_resolution: {
value: new THREE.Vector2(window.innerWidth, window.innerHeight),
},
},
});
这样做不会改变任何东西,因为我们需要在片段着色器中进行一些更新。
目前,gl_FragColor
是着色器的输出。
但使用此库时,你肯定不想修改那个变量。相反,我们需要使用一组预定义的变量。
在我们的情况下,我们将使用 csm_DiffuseColor
变量。确保我们只修改材质的颜色,同时保留其他数据,例如阴影计算。
js
csm_DiffuseColor = vec4(vec3(st.x / 0.6, st.y / 1.8, (st.x + st.y) / 1.2), 1.0);
// gl_FragColor = vec4(vec3(st.x / 0.6, st.y / 1.8, (st.x + st.y) / 1.2), 1.0);
现在,我们使用顶点着色器网格设置顶点动画。
为此,我们将顶点着色器添加到 CustomShaderMaterial
实例中。
js
const material3 = new CustomShaderMaterial({
baseMaterial: THREE.MeshPhysicalMaterial,
color: 0xFFEA00,
metalness: 0.6,
roughness: 0.4,
uniforms:{
u_resolution: {value: new THREE.Vector2(window.innerWidth, window.innerHeight)},
u_time: {value: 0}
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent
});
js
const clock = new THREE.Clock();
function animate() {
material3.uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
我们需要在顶点着色器中使用一个名为 csm_Position
的特殊变量。
js
uniform float u_time;
void main() {
vec3 newPosition = position * sin(u_time);
csm_Position = newPosition;
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
总结
至此,本文结束。
现在,你可以尽情发挥创意使用着色器。
再见!