Three.js 着色器加工材质详解

64 阅读4分钟

概述

本文将详细介绍如何使用 Three.js 中的着色器来加工和定制材质效果。我们将学习如何通过修改材质的着色器代码,实现动态变形、纹理处理和复杂视觉效果,从而创建出独特且富有表现力的三维场景。

screenshot_2026-01-27_22-29-59.gif

准备工作

首先,我们需要引入必要的 Three.js 库和相关工具:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

// 初始化场景
const scene = new THREE.Scene();

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerHeight / window.innerHeight,
  1,
  50
);

// 设置相机位置
camera.position.set(0, 0, 10);
scene.add(camera);

// 加入辅助轴,帮助我们查看3维坐标轴
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

环境纹理设置

为了创建更真实的光照效果,我们添加环境纹理:

// 加载纹理
const textureLoader = new THREE.TextureLoader();

// 添加环境纹理
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMapTexture = 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",
]);

// 设置方向光
const directionLight = new THREE.DirectionalLight("#ffffff", 1);
directionLight.castShadow = true;
directionLight.position.set(0, 0, 200);
scene.add(directionLight);

// 应用环境纹理
scene.environment = envMapTexture;
scene.background = envMapTexture;

模型加载与材质处理

加载模型并应用基本材质:

// 加载模型纹理
const modelTexture = textureLoader.load("./models/LeePerrySmith/color.jpg");
// 加载模型的法向纹理
const normalTexture = textureLoader.load("./models/LeePerrySmith/normal.jpg");

const material = new THREE.MeshStandardMaterial({
  map: modelTexture,
  normalMap: normalTexture,
});

着色器修改的关键技术

方法一:使用 onBeforeCompile 修改现有材质

这是最常用的方法,可以在 Three.js 自动生成的着色器代码基础上进行修改:

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

material.onBeforeCompile = (shader) => {
  console.log(shader.vertexShader);
  console.log(shader.fragmentShader);
  
  // 传递时间变量到着色器
  shader.uniforms.uTime = customUniforms.uTime;
  
  // 在顶点着色器的 common 部分插入自定义函数和变量
  shader.vertexShader = shader.vertexShader.replace(
    "#include <common>",
    `
    #include <common>
    mat2 rotate2d(float _angle){
      return mat2(cos(_angle),-sin(_angle),
                  sin(_angle),cos(_angle));
    }
    uniform float uTime;
    `
  );

  // 在顶点变换前修改法线
  shader.vertexShader = shader.vertexShader.replace(
    "#include <beginnormal_vertex>",
    `
    #include <beginnormal_vertex>
    float angle = sin(position.y+uTime) *0.5;
    mat2 rotateMatrix = rotate2d(angle);
    
    objectNormal.xz = rotateMatrix * objectNormal.xz;
    `
  );
  
  // 在顶点变换前修改顶点位置
  shader.vertexShader = shader.vertexShader.replace(
    "#include <begin_vertex>",
    `
    #include <begin_vertex>
    // float angle = transformed.y*0.5;
    // mat2 rotateMatrix = rotate2d(angle);
    
    transformed.xz = rotateMatrix * transformed.xz;
    `
  );
};

方法二:为深度材质添加相同效果

对于阴影等特殊情况,我们还需要为深度材质添加相同的效果:

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

depthMaterial.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = customUniforms.uTime;
  shader.vertexShader = shader.vertexShader.replace(
    "#include <common>",
    `
    #include <common>
    mat2 rotate2d(float _angle){
      return mat2(cos(_angle),-sin(_angle),
                  sin(_angle),cos(_angle));
    }
    uniform float uTime;
    `
  );
  shader.vertexShader = shader.vertexShader.replace(
    "#include <begin_vertex>",
    `
    #include <begin_vertex>
    float angle = sin(position.y+uTime) *0.5;
    mat2 rotateMatrix = rotate2d(angle);
    
    transformed.xz = rotateMatrix * transformed.xz;
    `
  );

  console.log("depthMaterial", shader.vertexShader);
};

模型加载与材质应用

加载 3D 模型并应用自定义材质:

// 模型加载
const gltfLoader = new GLTFLoader();
gltfLoader.load("./models/LeePerrySmith/LeePerrySmith.glb", (gltf) => {
  const mesh = gltf.scene.children[0];
  console.log(mesh);
  mesh.material = material;
  mesh.castShadow = true;
  // 设定自定义的深度材质
  mesh.customDepthMaterial = depthMaterial;
  scene.add(mesh);
});

// 添加地面
const plane = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(20, 20),
  new THREE.MeshStandardMaterial()
);
plane.position.set(0, 0, -6);
plane.receiveShadow = true;
scene.add(plane);

渲染器和控制器设置

设置渲染器和控制器:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;

// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  // 更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 设置渲染器的像素比例
  renderer.setPixelRatio(window.devicePixelRatio);
});

// 将渲染器添加到body
document.body.appendChild(renderer.domElement);

// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;

动画循环

在动画循环中更新时间变量,以驱动着色器中的动画效果:

const clock = new THREE.Clock();

function animate(t) {
  controls.update();
  const time = clock.getElapsedTime();
  customUniforms.uTime.value = time;
  requestAnimationFrame(animate);
  // 使用渲染器渲染相机看这个场景的内容渲染出来
  renderer.render(scene, camera);
}

animate();

自定义着色器材质示例

除了修改现有材质,我们也可以创建完全自定义的着色器材质:

let basicMaterial = new THREE.MeshBasicMaterial({
  color: "#00ff00",
  side: THREE.DoubleSide,
});

const basicUniforms = {
  uTime: {
    value: 0
  }
};

basicMaterial.onBeforeCompile = (shader, renderer) => {
  console.log(shader);
  console.log(shader.vertexShader);
  console.log(shader.fragmentShader);
  
  // 添加时间变量到着色器
  shader.uniforms.uTime = basicUniforms.uTime;
  
  // 在顶点着色器的 common 部分添加时间变量
  shader.vertexShader = shader.vertexShader.replace(
    '#include <common>',
    `
    #include <common>
    uniform float uTime;
    `
  );
  
  // 在顶点变换部分添加动态位移
  shader.vertexShader = shader.vertexShader.replace(
    '#include <begin_vertex>',
    `
    #include <begin_vertex>
    transformed.x += sin(uTime)* 2.0;
    transformed.z += cos(uTime)* 2.0;
    `
  );
};

GLSL 着色器代码详解

基本着色器 (basic/vertex.glsl)

uniform vec3 uColor;
uniform float uFrequency;
uniform float uScale;
uniform float uTime;

varying float vElevation;
varying vec2 vUv;

precision highp float;

void main(){
    vec4 modelPosition = modelMatrix * vec4( position, 1.0 );

    modelPosition.z += sin((modelPosition.x+uTime) * uFrequency)*uScale ;
    modelPosition.z += cos((modelPosition.y+uTime) * uFrequency)*uScale ;

    vElevation = modelPosition.z;
    gl_Position =  projectionMatrix * viewMatrix * modelPosition;
    vUv = uv;
}

基础片元着色器 (basic/fragment.glsl)

uniform vec3 uColor;
varying float vElevation;
precision highp float;
varying vec2 vUv;

uniform sampler2D uTexture;

void main(){
    float alpha = (vElevation+0.1)+0.8;
    
    vec4 textureColor = texture2D(uTexture,vUv);
    textureColor.rgb*=alpha;
    gl_FragColor = textureColor;
}

深度着色器技术

深度着色器通常用于阴影计算和特殊视觉效果,其中包含了丰富的 GLSL 技术:

  • 随机函数:用于生成程序化噪声
  • 旋转函数:用于变换 UV 坐标
  • 噪声函数:用于创建自然效果
  • Perlin 噪声:用于高质量的程序化纹理

总结

通过这个项目,我们学习了如何使用 Three.js 的着色器加工材质技术:

  1. 使用 onBeforeCompile 回调修改现有材质的着色器代码
  2. 向着色器传递自定义 uniform 变量
  3. 在着色器的关键位置插入自定义代码
  4. 为深度材质应用相同的修改以保证一致性
  5. 利用 GLSL 编写高效的顶点和片元着色器

这种方法让我们能够在不完全重写材质的情况下,对 Three.js 的内置材质进行深度定制,创造出独特的视觉效果。掌握这项技术是实现高级视觉效果的关键技能,可以用于创建动态地形、流体效果、变形动画等各种复杂场景。