普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月22日首页

Three.js 入门:30行代码画出你的第一条3D线条

作者 烛阴
2026年1月22日 22:33

核心概念:3个必备元素

在 Three.js 中,想要渲染任何东西,你需要理解3个核心概念:

  1. 场景 (Scene) - 就像一个舞台,所有物体都放在这里
  2. 相机 (Camera) - 就像你的眼睛,决定从哪个角度看舞台
  3. 渲染器 (Renderer) - 把场景和相机的内容画到屏幕上

完整代码

import * as THREE from 'three';

// 1️⃣ 创建场景 - 所有物体的容器
const scene = new THREE.Scene();

// 2️⃣ 创建相机 - 决定我们从哪里看
const camera = new THREE.PerspectiveCamera(
  75,                           // 视野角度
  innerWidth / innerHeight,     // 宽高比
  0.1,                          // 近裁剪面
  1000                          // 远裁剪面
);
camera.position.z = 5;          // 把相机往后移,才能看到原点的物体

// 3️⃣ 创建渲染器 - 把3D内容画到网页上
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// 4️⃣ 画线!
// 定义线条经过的点
const points = [
  new THREE.Vector3(-2, 0, 0),   // 左边的点
  new THREE.Vector3(0, 2, 0),    // 顶部的点
  new THREE.Vector3(2, 0, 0)     // 右边的点
];

// 用这些点创建几何体
const geometry = new THREE.BufferGeometry().setFromPoints(points);

// 创建线条材质(绿色)
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });

// 组合几何体和材质,创建线条对象
const line = new THREE.Line(geometry, material);

// 把线条添加到场景中
scene.add(line);

// 5️⃣ 渲染!
renderer.render(scene, camera);

代码解析

画线三步曲

步骤 代码 说明
1 BufferGeometry().setFromPoints(points) 定义线条的形状(经过哪些点)
2 LineBasicMaterial({ color }) 定义线条的外观(颜色)
3 new THREE.Line(geometry, material) 把形状和外观组合成线条对象

📂 核心代码与完整示例:      my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

ThreeJS 着色器图形特效

2026年1月22日 18:18

本文档涵盖Three.js中高级着色器图形特效的实现方法,基于实际代码示例进行讲解。

最终效果如图: Title

1. 着色器图形特效基础

1.1 复杂着色器材质创建

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";
import deepVertexShader from "../shaders/deep/vertex.glsl";
import deepFragmentShader from "../shaders/deep/fragment.glsl";

// 创建带有多个uniforms的着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: deepVertexShader,
  fragmentShader: deepFragmentShader,
  uniforms: {
    uColor: {
      value: new THREE.Color("purple"),
    },
    // 波浪的频率
    uFrequency: {
      value: params.uFrequency,
    },
    // 波浪的幅度
    uScale: {
      value: params.uScale,
    },
    // 动画时间
    uTime: {
      value: 0,
    },
    uTexture: {
      value: texture,
    },
  },
  side: THREE.DoubleSide,
  transparent: true,
});

1.2 GUI参数控制

通过dat.GUI实时控制着色器参数:

// 控制频率参数
gui
  .add(params, "uFrequency")
  .min(0)
  .max(50)
  .step(0.1)
  .onChange((value) => {
    shaderMaterial.uniforms.uFrequency.value = value;
  });

// 控制幅度参数
gui
  .add(params, "uScale")
  .min(0)
  .max(1)
  .step(0.01)
  .onChange((value) => {
    shaderMaterial.uniforms.uScale.value = value;
  });

2. 高级片元着色器技术

2.1 UV坐标操作

UV坐标是纹理映射的基础,也是创建各种图形效果的关键:

void main(){
    // 1. 通过顶点对应的uv,决定每一个像素在uv图像的位置,通过这个位置x,y决定颜色
    // gl_FragColor =vec4(vUv,0,1) ;

    // 2. 对第一种变形
    // gl_FragColor = vec4(vUv,1,1);

    // 3. 利用uv实现渐变效果,从左到右
    float strength = vUv.x;
    gl_FragColor =vec4(strength,strength,strength,1);
}

2.2 数学函数应用

利用GLSL内置数学函数创建复杂效果:

// 随机函数
float random (vec2 st) {
    return fract(sin(dot(st.xy,vec2(12.9898,78.233)))*43758.5453123);
}

// 噪声函数
float noise (in vec2 _st) {
    vec2 i = floor(_st);
    vec2 f = fract(_st);

    // 四个角落的随机值
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));

    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(a, b, u.x) +
            (c - a)* u.y * (1.0 - u.x) +
            (d - b) * u.x * u.y;
}

2.3 几何图形绘制

使用数学函数绘制各种几何图形:

// 绘制圆形
float strength = 1.0 - step(0.5,distance(vUv,vec2(0.5))+0.25) ;
gl_FragColor =vec4(strength,strength,strength,1);

// 绘制圆环
float strength = step(0.5,distance(vUv,vec2(0.5))+0.35) ;
strength *= (1.0 - step(0.5,distance(vUv,vec2(0.5))+0.25)) ;
gl_FragColor =vec4(strength,strength,strength,1);

// 波浪效果
vec2 waveUv = vec2(
    vUv.x+sin(vUv.y*100.0)*0.1,
    vUv.y+sin(vUv.x*100.0)*0.1
);
float strength = 1.0 - step(0.01,abs(distance(waveUv,vec2(0.5))-0.25)) ;
gl_FragColor =vec4(strength,strength,strength,1);

3. 动画与时间控制

3.1 时间uniform应用

在动画循环中更新时间uniform:

const clock = new THREE.Clock();
function animate(t) {
  const elapsedTime = clock.getElapsedTime();
  shaderMaterial.uniforms.uTime.value = elapsedTime;  // 更新时间
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

3.2 着色器中的动画效果

// 使用时间创建波浪动画
float strength = step(0.9,sin(cnoise(vUv * 10.0)*20.0+uTime)) ;

// 波纹效果
float strength = sin(cnoise(vUv * 10.0)*5.0+uTime) ;

4. 颜色混合与插值

4.1 颜色混合函数

// 使用混合函数混颜色
vec3 purpleColor = vec3(1.0, 0.0, 1.0);
vec3 greenColor = vec3(1.0, 1.0, 1.0);
vec3 uvColor = vec3(vUv,1.0);
float strength = step(0.9,sin(cnoise(vUv * 10.0)*20.0)) ;

vec3 mixColor =  mix(greenColor,uvColor,strength);
gl_FragColor =vec4(mixColor,1.0);

5. 纹理与采样

5.1 纹理采样

uniform sampler2D uTexture;

void main(){
    vec4 textureColor = texture2D(uTexture,vUv);
    textureColor.rgb*=height;
    gl_FragColor = textureColor;
}

6. 几何变换

6.1 旋转函数

// 旋转函数
vec2 rotate(vec2 uv, float rotation, vec2 mid)
{
    return vec2(
      cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
      cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
    );
}

// 使用旋转函数
vec2 rotateUv = rotate(vUv,-uTime*5.0,vec2(0.5));

7. 复杂效果实现

7.1 万花筒效果

// 万花筒效果
float angle = atan(vUv.x-0.5,vUv.y-0.5)/PI;
float strength = mod(angle*10.0,1.0);
gl_FragColor =vec4(strength,strength,strength,1);

7.2 雷达扫描效果

// 雷达扫描效果
vec2 rotateUv = rotate(vUv,-uTime*5.0,vec2(0.5));
float alpha =  1.0 - step(0.5,distance(vUv,vec2(0.5)));
float angle = atan(rotateUv.x-0.5,rotateUv.y-0.5);
float strength = (angle+3.14)/6.28;
gl_FragColor =vec4(strength,strength,strength,alpha);

8. 性能优化与调试

8.1 性能优化技巧

  1. 减少复杂计算:避免在着色器中进行过于复杂的数学运算
  2. 合理使用纹理:预先计算复杂效果并存储在纹理中
  3. 简化几何体:在不影响视觉效果的前提下减少顶点数

8.2 调试技巧

  1. 逐步构建:从简单效果开始,逐步增加复杂性
  2. 输出中间值:将中间计算结果输出为颜色进行调试
  3. 使用常量验证:先用常量验证逻辑,再引入变量

总结

本章深入探讨了Three.js中高级着色器图形特效的实现方法,包括:

  1. 复杂着色器材质的创建和参数控制
  2. 数学函数在图形生成中的应用
  3. UV坐标操作和几何图形绘制
  4. 时间动画和颜色混合技术
  5. 纹理采样和几何变换
  6. 复杂视觉效果的实现方法
  7. 性能优化和调试技巧

通过掌握这些技术,可以创建出丰富的视觉效果和动态图形。

ThreeJS 着色器编程基础入门

2026年1月22日 18:11

本文档涵盖Three.js中着色器编程的基础概念和实现方法,基于实际代码示例进行讲解。

最终效果如图: 懂王在风中凌乱

1. 着色器基础概念

着色器(Shader)是运行在GPU上的小程序,用于计算3D场景中每个像素的颜色。在Three.js中,有两种主要的着色器:

  • 顶点着色器(Vertex Shader):处理每个顶点的位置变换
  • 片元着色器(Fragment Shader):确定每个像素的最终颜色

1.1 着色器导入和初始化

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

// 顶点着色器
import basicVertexShader from "../shader/raw/vertex.glsl";
// 片元着色器
import basicFragmentShader from "../shader/raw/fragment.glsl";

2. 着色器材质创建

2.1 RawShaderMaterial vs ShaderMaterial

RawShaderMaterial直接使用GLSL代码,不会自动添加默认的uniforms和attributes:

// 创建原始着色器材质
const rawShaderMaterial = new THREE.RawShaderMaterial({
  vertexShader: basicVertexShader,
  fragmentShader: basicFragmentShader,
  side: THREE.DoubleSide,
  uniforms: {
    uTime: {
      value: 0,
    },
    uTexture: {
      value: texture,
    },
  },
});

2.2 基础着色器材质

// 创建着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: `
    void main(){
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
    }
  `,
  fragmentShader: `
    void main(){
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
  `,
});

3. 顶点着色器详解

顶点着色器负责处理3D空间中的顶点位置,以下是一个包含动画效果的顶点着色器:

precision lowp float;
attribute vec3 position;
attribute vec2 uv;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

// 获取时间
uniform float uTime;

varying vec2 vUv;
varying float vElevation;

void main(){
    vUv = uv;
    vec4 modelPosition = modelMatrix * vec4( position, 1.0 );
    
    // 添加基于时间的波浪动画
    modelPosition.z = sin((modelPosition.x+uTime) * 10.0)*0.05 ;
    modelPosition.z += sin((modelPosition.y+uTime)  * 10.0)*0.05 ;
    vElevation = modelPosition.z;

    gl_Position = projectionMatrix * viewMatrix * modelPosition ;
}

4. 片元着色器详解

片元着色器负责确定每个像素的颜色,以下是一个处理纹理和高度的片元着色器:

precision lowp float;
varying vec2 vUv;
varying float vElevation;

uniform sampler2D uTexture; 

void main(){
    // 根据UV,取出对应的颜色
    float height = vElevation + 0.05 * 20.0;
    vec4 textureColor = texture2D(uTexture,vUv);
    textureColor.rgb*=height;
    gl_FragColor = textureColor;
}

5. Uniforms统一变量

Uniforms是在JavaScript代码和着色器之间传递数据的变量:

const rawShaderMaterial = new THREE.RawShaderMaterial({
  vertexShader: basicVertexShader,
  fragmentShader: basicFragmentShader,
  side: THREE.DoubleSide,
  uniforms: {
    uTime: {
      value: 0,  // 时间变量,用于动画
    },
    uTexture: {
      value: texture,  // 纹理变量
    },
  },
});

在动画循环中更新uniform值:

const clock = new THREE.Clock();
function animate(t) {
  const elapsedTime = clock.getElapsedTime();
  // 更新着色器中的时间uniform
  rawShaderMaterial.uniforms.uTime.value = elapsedTime;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

6. 几何体与着色器结合

使用平面几何体展示着色器效果:

// 创建平面
const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(1, 1, 64, 64),  // 细分更多,波浪效果更明显
  rawShaderMaterial
);

scene.add(floor);

7. 基础着色器示例

创建一个简单的黄色平面着色器:

// 创建基础着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: `
    void main(){
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
    }
  `,
  fragmentShader: `
    void main(){
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);  // 黄色
    }
  `,
});

8. 着色器开发最佳实践

  1. 精度声明:在片元着色器中声明精度

    precision lowp float;  // 低精度
    precision mediump float;  // 中等精度
    precision highp float;  // 高精度
    
  2. 变量类型

    • attribute:每个顶点独有的数据(如位置、UV坐标)
    • uniform:所有顶点共享的数据(如时间、纹理)
    • varying:在顶点着色器和片元着色器之间传递的数据
  3. 性能优化:避免在着色器中使用复杂运算,尽可能在CPU端预计算

  4. 调试技巧:通过将中间计算结果输出到颜色来调试着色器

总结

本章介绍了Three.js中着色器编程的基础知识,包括:

  1. 着色器的基本概念和类型
  2. 如何创建和使用着色器材质
  3. 顶点着色器和片元着色器的编写
  4. 如何通过uniforms在JavaScript和着色器间传递数据
  5. 基础的着色器动画实现

通过掌握这些基础知识,可以进一步探索更复杂的着色器效果。

学习Three.js--曲线(Curve)

2026年1月22日 14:43

学习Three.js--曲线(Curve)

前置核心说明

Curve 是 Three.js 中所有曲线/直线的基类,定义了曲线的核心行为(如采样顶点、计算长度等)。所有2D/3D曲线均继承自 Curve,核心作用是「通过数学公式生成连续的顶点序列」,再基于这些顶点创建线条模型,实现任意自定义曲线的绘制。

核心规则

  1. 维度分类
    • 2D曲线:基于XY平面(Z=0),继承 THREE.Curve,如 LineCurve/ArcCurve
    • 3D曲线:基于XYZ三维空间,继承 THREE.Curve(部分别名/子类),如 LineCurve3/CatmullRomCurve3
  2. 核心流程
    创建曲线实例 → 采样顶点(getPoints) → 几何体绑定顶点 → 创建线模型 → 添加到场景
  3. 线模型类型(决定曲线渲染方式):
    模型类型 核心特点 适用场景
    THREE.Line 按顶点顺序绘制连续线条 开放曲线(如直线、贝塞尔曲线)
    THREE.LineLoop 闭合线条(最后一个顶点连接第一个) 封闭曲线(如椭圆、圆)
    THREE.LineSegments 每两个顶点为一组绘制分段线 离散线条(如网格线)
  4. 采样精度getPoints(n)n 是采样点数(非顶点数),n 越大曲线越平滑(推荐50~100,复杂曲线可设200)。

一、2D曲线(XY平面,Z=0)

所有2D曲线均基于XY平面,Z坐标默认0,核心用于绘制平面曲线。

1. LineCurve(2D直线)

核心说明

两点确定的2D直线,是最简单的2D曲线,无曲率。

构造函数参数
// 语法:new THREE.LineCurve(起点向量, 终点向量)
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(x1, y1), // 必传:起点(Vector2对象)
  new THREE.Vector2(x2, y2)  // 必传:终点(Vector2对象)
);
参数 类型 说明
v1 THREE.Vector2 直线起点(XY坐标)
v2 THREE.Vector2 直线终点(XY坐标)
使用示例
// 1. 创建2D直线(从(0,0)到(100, 50))
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(0, 0),
  new THREE.Vector2(100, 50)
);

// 2. 采样顶点(50个点,直线足够平滑)
const points = lineCurve.getPoints(50);

// 3. 创建几何体并绑定顶点
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);

// 4. 创建线材质
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });

// 5. 创建线模型(开放直线用Line)
const line = new THREE.Line(geometry, material);
scene.add(line);

2. ArcCurve(2D圆弧)

核心说明

基于圆心、半径、起始角/终止角的2D圆弧,可绘制圆、半圆、任意弧度的圆弧。

构造函数参数
// 语法:new THREE.ArcCurve(圆心X, 圆心Y, 半径, 起始角, 终止角, 是否逆时针)
const arcCurve = new THREE.ArcCurve(
  0,        // 必传:圆心X坐标
  0,        // 必传:圆心Y坐标
  50,       // 必传:圆弧半径
  0,        // 必传:起始角(弧度,0=右向X轴)
  Math.PI,  // 必传:终止角(弧度,Math.PI=180°)
  false     // 可选:是否逆时针绘制,默认false(顺时针)
);
参数 类型 默认值 说明
aX Number 圆心X坐标
aY Number 圆心Y坐标
aRadius Number 圆弧半径
aStartAngle Number 起始角度(弧度,0=X轴正方向,Math.PI/2=Y轴正方向)
aEndAngle Number 终止角度(弧度)
aClockwise Boolean false 是否顺时针绘制,true=顺时针,false=逆时针
使用示例(绘制半圆)
// 1. 创建半圆(圆心(0,0),半径50,0~π弧度)
const arcCurve = new THREE.ArcCurve(0, 0, 50, 0, Math.PI);

// 2. 采样顶点
const points = arcCurve.getPoints(50);

// 3. 几何体+材质+模型
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

3. EllipseCurve(2D椭圆/圆)

核心说明

基于圆心、长半轴、短半轴的2D椭圆,当长半轴=短半轴时即为圆(替代ArcCurve绘制完整圆)。

构造函数参数
// 语法:new THREE.EllipseCurve(圆心X, 圆心Y, 长半轴, 短半轴, 起始角, 终止角, 是否逆时针, 旋转角)
const ellipseCurve = new THREE.EllipseCurve(
  0,          // 必传:圆心X坐标
  0,          // 必传:圆心Y坐标
  100,        // 必传:X轴方向长半轴
  50,         // 必传:Y轴方向短半轴
  0,          // 可选:起始角,默认0
  2 * Math.PI,// 可选:终止角,默认2π(完整椭圆)
  false,      // 可选:是否逆时针,默认false
  0           // 可选:椭圆旋转角(弧度),默认0
);
参数 类型 默认值 说明
aX Number 圆心X坐标
aY Number 圆心Y坐标
xRadius Number X轴方向半轴长度(长半轴)
yRadius Number Y轴方向半轴长度(短半轴)
aStartAngle Number 0 起始角度(弧度)
aEndAngle Number 终止角度(弧度,2π=完整椭圆)
aClockwise Boolean false 是否顺时针绘制
aRotation Number 0 椭圆整体旋转角度(弧度)
使用示例(优化版,用户示例升级)
// 1. 创建椭圆(圆心(0,0),长半轴100,短半轴50,完整椭圆)
const ellipseCurve = new THREE.EllipseCurve(0, 0, 100, 50);

// 2. 采样顶点(50个点,椭圆足够平滑)
const points = ellipseCurve.getPoints(50);

// 3. 创建几何体并绑定顶点
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);

// 4. 线材质(红色,线宽1)
const material = new THREE.LineBasicMaterial({ 
  color: 0xff0000,
  linewidth: 1 // 注意:WebGL中线宽仅部分浏览器支持>1
});

// 5. 创建闭合线模型(椭圆用LineLoop)
const line = new THREE.LineLoop(geometry, material);
scene.add(line);

4. SplineCurve(2D样条曲线)

核心说明

通过多个控制点生成的平滑2D曲线(插值曲线),曲线会穿过所有控制点,比贝塞尔曲线更易控制。

构造函数参数
// 语法:new THREE.SplineCurve(控制点数组)
const splineCurve = new THREE.SplineCurve([
  new THREE.Vector2(x1, y1),
  new THREE.Vector2(x2, y2),
  // ... 更多控制点
]);
参数 类型 说明
points Array<THREE.Vector2> 必传:2D控制点数组(至少2个,越多曲线越复杂)
使用示例
// 1. 创建2D样条曲线(4个控制点)
const splineCurve = new THREE.SplineCurve([
  new THREE.Vector2(-100, 0),  // 控制点1
  new THREE.Vector2(-50, 80),  // 控制点2
  new THREE.Vector2(50, -80),  // 控制点3
  new THREE.Vector2(100, 0)    // 控制点4
]);

// 2. 采样顶点(100个点,保证平滑)
const points = splineCurve.getPoints(100);

// 3. 几何体+材质+模型
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

5. QuadraticBezierCurve(2D二次贝塞尔曲线)

核心说明

由「起点+控制点+终点」3个点定义的2D贝塞尔曲线,单曲率,适合简单弯曲。

构造函数参数
// 语法:new THREE.QuadraticBezierCurve(起点, 控制点, 终点)
const quadBezier = new THREE.QuadraticBezierCurve(
  new THREE.Vector2(x1, y1), // 必传:起点
  new THREE.Vector2(x2, y2), // 必传:控制点(决定弯曲方向)
  new THREE.Vector2(x3, y3)  // 必传:终点
);
参数 类型 说明
v0 THREE.Vector2 起点
v1 THREE.Vector2 控制点(核心,决定曲线形状)
v2 THREE.Vector2 终点
使用示例
// 1. 创建二次贝塞尔曲线
const quadBezier = new THREE.QuadraticBezierCurve(
  new THREE.Vector2(-80, 0),  // 起点
  new THREE.Vector2(0, 80),   // 控制点(向上弯曲)
  new THREE.Vector2(80, 0)    // 终点
);

// 2. 采样顶点
const points = quadBezier.getPoints(80);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xffff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

6. CubicBezierCurve(2D三次贝塞尔曲线)

核心说明

由「起点+控制点1+控制点2+终点」4个点定义的2D贝塞尔曲线,双曲率,适合复杂弯曲(如字体轮廓、路径动画)。

构造函数参数
// 语法:new THREE.CubicBezierCurve(起点, 控制点1, 控制点2, 终点)
const cubicBezier = new THREE.CubicBezierCurve(
  new THREE.Vector2(x1, y1), // 必传:起点
  new THREE.Vector2(x2, y2), // 必传:控制点1
  new THREE.Vector2(x3, y3), // 必传:控制点2
  new THREE.Vector2(x4, y4)  // 必传:终点
);
参数 类型 说明
v0 THREE.Vector2 起点
v1 THREE.Vector2 控制点1(左侧弯曲)
v2 THREE.Vector2 控制点2(右侧弯曲)
v3 THREE.Vector2 终点
使用示例
// 1. 创建三次贝塞尔曲线
const cubicBezier = new THREE.CubicBezierCurve(
  new THREE.Vector2(-100, 0), // 起点
  new THREE.Vector2(-50, 100),// 控制点1(向上)
  new THREE.Vector2(50, -100),// 控制点2(向下)
  new THREE.Vector2(100, 0)   // 终点
);

// 2. 采样顶点
const points = cubicBezier.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff00ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

二、3D曲线(XYZ三维空间)

3D曲线突破XY平面限制,支持XYZ三维坐标,核心用于3D路径(如飞行轨迹、管道模型)。

1. LineCurve3(3D直线)

核心说明

两点确定的3D直线,Z坐标可自定义,是3D最基础的曲线。

构造函数参数
// 语法:new THREE.LineCurve3(起点向量, 终点向量)
const lineCurve3 = new THREE.LineCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2)  // 必传:3D终点
);
参数 类型 说明
v1 THREE.Vector3 3D起点(XYZ坐标)
v2 THREE.Vector3 3D终点(XYZ坐标)
使用示例
// 1. 创建3D直线(从(0,0,0)到(100, 50, 80))
const lineCurve3 = new THREE.LineCurve3(
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(100, 50, 80)
);

// 2. 采样顶点
const points = lineCurve3.getPoints(50);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

2. CatmullRomCurve3(3D样条曲线)

核心说明

Three.js官方推荐的3D样条曲线(替代SplineCurve3),通过多个3D控制点生成平滑曲线,曲线穿过所有控制点,适合3D路径规划。

构造函数参数
// 语法:new THREE.CatmullRomCurve3(控制点数组, 是否闭合, 曲线类型, 张力)
const catmullCurve = new THREE.CatmullRomCurve3(
  [
    new THREE.Vector3(x1, y1, z1),
    new THREE.Vector3(x2, y2, z2),
    // ... 更多控制点
  ],
  false,       // 可选:是否闭合,默认false
  'centripetal',// 可选:曲线类型,默认'centripetal'
  0.5          // 可选:张力(0~1),默认0.5,值越大曲线越平缓
);
参数 类型 默认值 说明
points Array<THREE.Vector3> 必传:3D控制点数组(至少2个)
closed Boolean false 是否闭合曲线
type String 'centripetal' 曲线类型:'centripetal'(默认,自然)、'chordal'(更紧绷)、'catmullrom'(更平滑)
tension Number 0.5 张力(0=无张力,1=最大张力)
使用示例
// 1. 创建3D样条曲线(4个控制点,空间弯曲)
const catmullCurve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-100, 0, 0),   // 控制点1
  new THREE.Vector3(-50, 80, 50),  // 控制点2
  new THREE.Vector3(50, -80, 100), // 控制点3
  new THREE.Vector3(100, 0, 50)    // 控制点4
]);

// 2. 采样顶点(100个点,保证3D平滑)
const points = catmullCurve.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

3. QuadraticBezierCurve3(3D二次贝塞尔曲线)

核心说明

3D版本的二次贝塞尔曲线,由「3D起点+3D控制点+3D终点」定义,支持空间弯曲。

构造函数参数
// 语法:new THREE.QuadraticBezierCurve3(起点, 控制点, 终点)
const quadBezier3 = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2), // 必传:3D控制点
  new THREE.Vector3(x3, y3, z3)  // 必传:3D终点
);
参数 类型 说明
v0 THREE.Vector3 3D起点
v1 THREE.Vector3 3D控制点(决定空间弯曲方向)
v2 THREE.Vector3 3D终点
使用示例
// 1. 创建3D二次贝塞尔曲线
const quadBezier3 = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(-80, 0, 0),   // 起点
  new THREE.Vector3(0, 80, 50),   // 控制点(Z轴偏移)
  new THREE.Vector3(80, 0, 100)   // 终点
);

// 2. 采样顶点
const points = quadBezier3.getPoints(80);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xffff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

4. CubicBezierCurve3(3D三次贝塞尔曲线)

核心说明

3D版本的三次贝塞尔曲线,由4个3D点定义,支持复杂空间弯曲,是3D路径动画的核心曲线。

构造函数参数
// 语法:new THREE.CubicBezierCurve3(起点, 控制点1, 控制点2, 终点)
const cubicBezier3 = new THREE.CubicBezierCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2), // 必传:3D控制点1
  new THREE.Vector3(x3, y3, z3), // 必传:3D控制点2
  new THREE.Vector3(x4, y4, z4)  // 必传:3D终点
);
参数 类型 说明
v0 THREE.Vector3 3D起点
v1 THREE.Vector3 3D控制点1
v2 THREE.Vector3 3D控制点2
v3 THREE.Vector3 3D终点
使用示例
// 1. 创建3D三次贝塞尔曲线
const cubicBezier3 = new THREE.CubicBezierCurve3(
  new THREE.Vector3(-100, 0, 0),  // 起点
  new THREE.Vector3(-50, 100, 50),// 控制点1
  new THREE.Vector3(50, -100, 80),// 控制点2
  new THREE.Vector3(100, 0, 100)  // 终点
);

// 2. 采样顶点
const points = cubicBezier3.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff00ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

三、CurvePath(组合曲线)

核心说明

CurvePath 是「曲线容器」,可将多个2D/3D曲线组合成一个复合曲线,支持统一采样、平移、旋转等操作,适合绘制复杂路径(如迷宫、文字轮廓)。

核心方法
方法名 说明 示例
add(curve) 添加单个曲线到容器 curvePath.add(lineCurve)
getPoints(n) 统一采样所有曲线的顶点 curvePath.getPoints(100)
closePath() 闭合组合曲线(最后一个曲线终点连接第一个曲线起点) curvePath.closePath()
使用示例(组合2D直线+圆弧)
// 1. 创建CurvePath容器
const curvePath = new THREE.CurvePath();

// 2. 添加子曲线(直线+圆弧)
// 子曲线1:2D直线(从(0,0)到(100,0))
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(0, 0),
  new THREE.Vector2(100, 0)
);
curvePath.add(lineCurve);

// 子曲线2:圆弧(从(100,0)到(100,100),90°圆弧)
const arcCurve = new THREE.ArcCurve(
  100, 0, 100, 0, Math.PI/2, true
);
curvePath.add(arcCurve);

// 3. 闭合曲线(可选)
// curvePath.closePath();

// 4. 统一采样顶点(100个点)
const points = curvePath.getPoints(100);

// 5. 渲染组合曲线
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff6600 });
const line = new THREE.Line(geometry, material);
scene.add(line);

四、完整实战示例(多曲线组合)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Three.js 曲线完整示例</title>
  <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
 <script type="module">
    // 地址,升级为174版本
    import * as THREE from 'https://esm.sh/three@0.174.0';
    import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
    import { GLTFLoader } from 'https://esm.sh/three@0.174.0/examples/jsm/loaders/GLTFLoader.js';
    // 1. 创建三大核心
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    camera.position.set(0, 0, 300); // 相机后退,看清所有曲线

    // 2. 轨道控制器(3D视角交互)
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;

    // 3. 绘制2D椭圆
    const ellipseCurve = new THREE.EllipseCurve(0, 0, 80, 40);
    const ellipsePoints = ellipseCurve.getPoints(50);
    const ellipseGeo = new THREE.BufferGeometry();
    ellipseGeo.setFromPoints(ellipsePoints);
    const ellipseMat = new THREE.LineBasicMaterial({ color: 0xff0000 });
    const ellipseLine = new THREE.LineLoop(ellipseGeo, ellipseMat);
    scene.add(ellipseLine);

    // 4. 绘制3D样条曲线
    const catmullCurve = new THREE.CatmullRomCurve3([
      new THREE.Vector3(-100, 0, 0),
      new THREE.Vector3(-50, 80, 50),
      new THREE.Vector3(50, -80, 100),
      new THREE.Vector3(100, 0, 50)
    ]);
    const catmullPoints = catmullCurve.getPoints(100);
    const catmullGeo = new THREE.BufferGeometry();
    catmullGeo.setFromPoints(catmullPoints);
    const catmullMat = new THREE.LineBasicMaterial({ color: 0x0000ff });
    const catmullLine = new THREE.Line(catmullGeo, catmullMat);
    scene.add(catmullLine);

    // 5. 动画循环
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // 6. 窗口适配
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

示例效果

c7c96963-4107-4138-a72c-08caaa36d7f3.png

  1. 场景中显示红色2D椭圆(XY平面)和蓝色3D样条曲线(空间弯曲);
  2. 支持鼠标旋转/缩放视角,查看3D曲线的空间形态;
  3. 曲线平滑无锯齿,色彩无偏色。

五、注意事项与优化

1. 常见坑点

  • 线宽限制:WebGL标准中线宽(linewidth)仅支持1px,部分浏览器支持>1但兼容性差,如需粗线条建议用 Mesh 模拟(如挤压曲线成面);
  • 3D曲线视角:3D曲线需调整相机位置(Z轴后退),否则可能看不到;
  • 采样点数:复杂曲线(如三次贝塞尔)需增加采样点数(100~200),否则会出现锯齿;
  • CurvePath闭合closePath() 仅对2D曲线有效,3D曲线闭合需手动调整控制点。

2. 性能优化

  • 复用几何体:多个曲线复用同一个 BufferGeometry(清空顶点后重新绑定);
  • 减少采样点数:简单曲线(直线、圆弧)采样点数设50即可,无需过高;
  • 批量渲染:多个曲线合并为一个 Line 模型,减少渲染调用。

核心总结

  1. 核心流程创建曲线 → getPoints采样顶点 → 几何体绑定顶点 → Line/LineLoop渲染
  2. 曲线选型
    • 2D简单曲线:LineCurve/ArcCurve/EllipseCurve;
    • 2D复杂曲线:SplineCurve/CubicBezierCurve;
    • 3D路径:CatmullRomCurve3(推荐)/CubicBezierCurve3;
    • 复合曲线:CurvePath(组合多个子曲线);
  3. 关键参数
    • getPoints(n)n 决定平滑度,推荐50~100;
    • 3D曲线需调整相机Z轴位置,确保可见;
    • 贝塞尔曲线的「控制点」是决定曲线形状的核心。

学习Three.js--加载外部三维模型(gltf)

2026年1月22日 10:07

学习Three.js--加载外部三维模型(gltf)

前置核心说明

GLTF(GL Transmission Format)是 Three.js 官方推荐的首选3D模型格式,也是行业通用标准(被Blender、3ds Max、C4D等主流建模软件支持),相比OBJ/FBX等格式有三大核心优势:

  1. 体积小:采用二进制压缩,文件体积远小于传统格式;
  2. 功能全:原生支持PBR材质、骨骼动画、顶点动画、纹理集、层级结构;
  3. 渲染优:Three.js对GLTF的渲染优化最完善,性能最高。

核心规则

  1. 版本兼容:Three.js r152+ 版本重构了颜色空间系统,废弃 gammaOutput/gammaFactor,改用 outputColorSpace
  2. 渲染配置是关键:GLTF模型(尤其是PBR材质)依赖正确的颜色空间和色调映射,否则会出现偏色、过暗/过亮;
  3. 模型适配:不同类型模型(植物/金属/普通)需针对性调整材质参数(如透明、双面、曝光)。

一、核心渲染配置(解决偏色/过亮问题)

GLTF模型渲染的「底层基础配置」,必须在创建渲染器后立即设置,否则模型视觉效果异常。

1. 颜色空间配置(r152+ 版本)

// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 抗锯齿(可选,提升画质)
// 核心:设置输出颜色空间为SRGB,匹配GLTF模型的颜色标准,解决偏色
renderer.outputColorSpace = THREE.SRGBColorSpace;

2. 色调映射(解决高反射材质过亮)

针对金属/高反射塑料/玻璃材质的GLTF模型,默认渲染会过亮/过曝,需开启色调映射并调整曝光值:

// 启用ACES电影级色调映射(最接近真实物理的色调映射,推荐)
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 调整曝光值(核心参数,根据模型类型适配)
renderer.toneMappingExposure = 2.0; // 通用推荐值:1.5 ~ 2.5

3. 旧版本兼容(r152 以下)

若项目必须使用旧版本Three.js,需替换为gamma配置:

// 替代 outputColorSpace,解决偏色
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2; // 固定值,匹配SRGB颜色空间

4. 色调映射可选值(按效果优先级)

取值 效果特点 适用场景
THREE.ACESFilmicToneMapping(推荐) 电影级效果,色彩自然,高光压制优秀 所有PBR模型(金属/塑料/植物)
THREE.ReinhardToneMapping 基础色调映射,效果柔和 普通低反射模型
THREE.CineonToneMapping 对比度高,暗部细节丰富 暗色调场景/模型
THREE.LinearToneMapping(默认) 无色调映射,易过曝 仅调试使用

5. 曝光值适配表(按模型类型)

模型类型 推荐曝光值(toneMappingExposure) 说明
普通模型(低反射,如木头/石头) 1.0 ~ 1.5 无需过度调整
高反射模型(金属/塑料/玻璃) 1.5 ~ 2.5 压制高光,避免过亮
植物/透明模型(草/树叶) 2.0 ~ 2.8 提升亮度,凸显细节

二、GLTFLoader 完整使用指南

1. 导入方式(两种场景全覆盖)

方式1:CDN导入(新手/快速测试,推荐)

直接在HTML中引入,无需安装依赖,适配r174版本:

<!-- 先引入Three.js核心库 -->
<script type="module">
  // 导入Three.js核心
  import * as THREE from 'https://esm.sh/three@0.174.0';
  // 导入轨道控制器
  import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
  // 导入GLTF加载器(核心)
  import { GLTFLoader } from 'https://esm.sh/three@0.174.0/examples/jsm/loaders/GLTFLoader.js';
  
  // 后续代码写在这里...
</script>
方式2:NPM导入(工程化项目,Vue/React/Vite)
# 安装Three.js
npm install three --save
// 模块化导入
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

2. GLTFLoader.load() 方法参数详解

load 是加载GLTF模型的核心方法,支持加载进度、成功、失败回调:

// 创建GLTF加载器实例
const loader = new GLTFLoader();

// 语法:loader.load(url, onLoad, onProgress, onError)
loader.load(
  // 参数1:必传,模型文件路径(本地/CDN)
  './models/grass_medium_01_1k.gltf',
  
  // 参数2:必传,加载成功回调(核心)
  (gltf) => {
    // gltf对象核心属性说明
    console.log('模型加载成功', gltf);
    // gltf.scene:模型的根场景(包含所有模型节点,必加)
    // gltf.scenes:模型包含的所有场景数组
    // gltf.nodes:模型的所有节点(网格/相机/光源)
    // gltf.materials:模型的所有材质
    // gltf.animations:模型的动画数据(如有)
    
    // 1. 将模型添加到场景(核心步骤)
    scene.add(gltf.scene);
    
    // 2. 模型材质适配(按需处理,如植物/透明模型)
    configureMaterialForModel(gltf.scene);
    
    // 3. 自动聚焦模型(让相机对准模型中心,避免看不到)
    autoFocusCamera(gltf.scene, camera, controls);
  },
  
  // 参数3:可选,加载进度回调
  (xhr) => {
    const progress = (xhr.loaded / xhr.total) * 100;
    console.log(`加载进度:${progress.toFixed(1)}%`);
  },
  
  // 参数4:可选,加载失败回调
  (error) => {
    console.error('模型加载失败:', error);
  }
);

3. 核心辅助函数(模型适配+自动聚焦)

3.1 模型材质适配函数(按类型处理)

针对不同模型类型调整材质参数,解决透明、双面、裁剪问题:

/**
 * 配置GLTF模型材质
 * @param {THREE.Object3D} root - 模型根节点(gltf.scene)
 * @param {String} type - 模型类型:plant(植物)/metal(金属)/normal(普通)
 */
function configureMaterialForModel(root, type = 'normal') {
  // 遍历模型所有子节点
  root.traverse((child) => {
    // 只处理网格模型(Mesh)
    if (!child.isMesh) return;
    
    const material = child.material;
    // 处理多材质情况(数组)
    const materials = Array.isArray(material) ? material : [material];
    
    materials.forEach(mat => {
      // 通用配置:开启双面渲染(避免模型背面不可见)
      mat.side = THREE.DoubleSide;
      
      // 按模型类型适配
      switch (type) {
        case 'plant': // 植物/草/树叶模型
          // Alpha裁剪:丢弃透明像素(解决草模型透明区域显示异常)
          mat.alphaTest = 0.5; // 阈值:0.3~0.9,值越大裁剪越严格
          // 可选:若alphaTest无效,启用透明(慎用,可能有排序问题)
          // mat.transparent = true;
          // mat.depthWrite = false; // 关闭深度写入,解决透明闪烁
          break;
          
        case 'metal': // 金属/高反射模型
          // 调整材质粗糙度(可选,让金属更真实)
          if (mat.roughness !== undefined) {
            mat.roughness = 0.1; // 降低粗糙度,提升反光
          }
          break;
          
        case 'normal': // 普通模型(木头/石头)
          // 无需额外配置,保持默认
          break;
      }
      
      // 关键:更新材质(确保配置生效)
      mat.needsUpdate = true;
    });
  });
}
3.2 自动聚焦相机函数(避免模型看不到)

计算模型包围盒,自动调整相机位置和视角,适配任意尺寸模型:

/**
 * 自动聚焦模型,让相机对准模型中心
 * @param {THREE.Object3D} model - 模型节点
 * @param {THREE.Camera} camera - 相机对象
 * @param {THREE.OrbitControls} controls - 轨道控制器
 */
function autoFocusCamera(model, camera, controls) {
  // 1. 计算模型的包围盒(包含所有子节点)
  const box = new THREE.Box3().setFromObject(model);
  // 2. 获取模型中心坐标
  const center = box.getCenter(new THREE.Vector3());
  // 3. 获取模型尺寸(对角线长度)
  const size = box.getSize(new THREE.Vector3()).length();
  // 4. 计算相机距离(模型尺寸的2倍,保证完整显示)
  const distance = size * 2;
  
  // 5. 设置相机位置(在模型中心后方)
  camera.position.copy(center).add(new THREE.Vector3(0, 0, distance));
  // 6. 相机看向模型中心
  camera.lookAt(center);
  // 7. 更新轨道控制器目标(让控制器围绕模型中心旋转)
  controls.target = center;
  controls.update(); // 生效
}

三、不同类型GLTF模型加载示例

1. 示例1:加载植物/草模型(核心适配透明)

// 创建加载器
const loader = new GLTFLoader();
loader.load(
  './models/grass_medium_01_1k.gltf',
  (gltf) => {
    scene.add(gltf.scene);
    // 配置植物材质(alphaTest+双面)
    configureMaterialForModel(gltf.scene, 'plant');
    // 自动聚焦
    autoFocusCamera(gltf.scene, camera, controls);
  },
  (xhr) => console.log(`加载中:${(xhr.loaded/xhr.total*100).toFixed(1)}%`),
  (err) => console.error('加载失败:', err)
);

2. 示例2:加载金属/高反射模型(调整曝光+粗糙度)

// 先调整渲染器曝光(金属模型适配)
renderer.toneMappingExposure = 2.2;

// 加载金属模型
const loader = new GLTFLoader();
loader.load(
  './models/metal_car.gltf',
  (gltf) => {
    scene.add(gltf.scene);
    // 配置金属材质(降低粗糙度)
    configureMaterialForModel(gltf.scene, 'metal');
    // 自动聚焦
    autoFocusCamera(gltf.scene, camera, controls);
  },
  null,
  (err) => console.error('加载失败:', err)
);

3. 示例3:加载带动画的GLTF模型(扩展)

// 额外导入动画播放器
import { AnimationMixer } from 'three';

let mixer; // 动画混合器
const loader = new GLTFLoader();
loader.load(
  './models/animated_character.gltf',
  (gltf) => {
    scene.add(gltf.scene);
    autoFocusCamera(gltf.scene, camera, controls);
    
    // 初始化动画混合器(播放模型动画)
    mixer = new AnimationMixer(gltf.scene);
    // 播放第一个动画(如有多个,可遍历gltf.animations)
    if (gltf.animations.length > 0) {
      mixer.clipAction(gltf.animations[0]).play();
    }
  },
  null,
  (err) => console.error('加载失败:', err)
);

// 动画循环中更新混合器
function animate() {
  requestAnimationFrame(animate);
  if (mixer) mixer.update(0.01); // 传入时间增量
  controls.update();
  renderer.render(scene, camera);
}
animate();

四、完整实战代码(适配所有模型类型)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Three.js 加载GLTF模型完整示例</title>
  <style>
    body { margin: 0; overflow: hidden; }
    #progress { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: white; font-size: 16px; }
  </style>
</head>
<body>
  <div id="progress">加载中... 0.0%</div>
  <script type="module">
    // 1. 导入核心模块
    import * as THREE from 'https://esm.sh/three@0.174.0';
    import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
    import { GLTFLoader } from 'https://esm.sh/three@0.174.0/examples/jsm/loaders/GLTFLoader.js';

    // 2. 创建三大核心
    const scene = new THREE.Scene();
    //背景色改成灰色
    scene.background = new THREE.Color(0x444444); 
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 核心渲染配置(必加)
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 2.0; // 通用曝光值
    document.body.appendChild(renderer.domElement);

    // 3. 添加光源(PBR模型必须加)
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // 环境光
    const dirLight = new THREE.DirectionalLight(0xffffff, 1.2); // 平行光(主光源)
    dirLight.position.set(5, 8, 5);
    dirLight.castShadow = true; // 可选:开启阴影
    scene.add(ambientLight, dirLight);

    // 4. 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // 阻尼顺滑
    controls.dampingFactor = 0.05;

    // 5. 核心辅助函数
    // 5.1 材质配置函数
    function configureMaterialForModel(root, type = 'normal') {
      root.traverse((child) => {
        if (!child.isMesh) return;
        const material = child.material;
        const materials = Array.isArray(material) ? material : [material];
        
        materials.forEach(mat => {
          mat.side = THREE.DoubleSide; // 双面渲染
          if (type === 'plant') {
            mat.alphaTest = 0.5; // 植物Alpha裁剪
          } else if (type === 'metal') {
            if (mat.roughness !== undefined) mat.roughness = 0.1;
          }
          mat.needsUpdate = true;
        });
      });
    }

    // 5.2 自动聚焦函数
    function autoFocusCamera(model, camera, controls) {
      const box = new THREE.Box3().setFromObject(model);
      const center = box.getCenter(new THREE.Vector3());
      const size = box.getSize(new THREE.Vector3()).length();
      const distance = size * 2;
      camera.position.copy(center).add(new THREE.Vector3(0, 0, distance));
      camera.lookAt(center);
      controls.target = center;
      controls.update();
    }

    // 6. 加载GLTF模型(替换为你的模型路径)
    const progressDom = document.getElementById('progress');
    const loader = new GLTFLoader();
    loader.load(
      // 示例:使用CDN植物模型(避免本地路径问题)
      './grass_medium_01_1k/grass_medium_01_1k.gltf',
      (gltf) => {
        progressDom.textContent = '加载完成!';
        scene.add(gltf.scene);
        // 配置植物材质
        configureMaterialForModel(gltf.scene, 'plant');
        // 自动聚焦
        autoFocusCamera(gltf.scene, camera, controls);
      },
      (xhr) => {
        const progress = (xhr.loaded / xhr.total) * 100;
        progressDom.textContent = `加载中... ${progress.toFixed(1)}%`;
      },
      (err) => {
        progressDom.textContent = '加载失败!';
        console.error('模型加载失败:', err);
      }
    );

    // 7. 动画循环
    function animate() {
      requestAnimationFrame(animate);
      controls.update(); // 更新控制器
      renderer.render(scene, camera);
    }
    animate();

    // 8. 窗口适配
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

示例效果

46d6f02a-b7f8-42ac-86b7-6cc382d026d0.png

  1. 页面显示加载进度,完成后显示植物模型;
  2. 植物模型透明区域正常显示,无黑边/闪烁;
  3. 相机自动对准模型中心,支持鼠标旋转/缩放视角;
  4. 模型色彩自然,无偏色、过亮问题;
  5. 窗口缩放时,场景自动适配尺寸。

五、常见问题与避坑指南

1. 模型加载失败

问题现象 原因 解决方法
控制台报404 模型路径错误 检查路径(相对路径/绝对路径),确保.gltf/.glb文件存在
跨域错误 本地直接打开HTML,无HTTP服务 用VSCode Live Server/Node.js服务启动项目
CORS错误 CDN/服务器未配置跨域 配置服务器CORS头(Access-Control-Allow-Origin: *)

2. 模型显示异常

问题现象 原因 解决方法
模型偏色/发灰 未设置outputColorSpace 添加 renderer.outputColorSpace = THREE.SRGBColorSpace
模型过亮/过曝 高反射材质未调整曝光 增大 toneMappingExposure(1.5~2.5)
模型透明区域黑边 植物模型未开alphaTest 设置 mat.alphaTest = 0.5
模型背面不可见 未开双面渲染 设置 mat.side = THREE.DoubleSide
模型看不到/太小 相机位置不对 使用 autoFocusCamera 自动聚焦

3. 性能优化

  • 模型轻量化:用Blender简化模型面数,压缩纹理尺寸;
  • 复用材质:避免模型中重复创建相同材质;
  • 关闭不必要功能:静态模型关闭动画、阴影;
  • LOD级别:为复杂模型创建多级别细节(远距显示低模)。

核心总结

  1. 渲染配置核心
    • r152+ 必加 outputColorSpace = THREE.SRGBColorSpace
    • 高反射模型开启 ACESFilmicToneMapping + 调整 toneMappingExposure(1.5~2.5);
  2. 加载流程
    • GLTFLoader.load() → 成功后添加 gltf.scene 到场景;
    • 按模型类型适配材质(植物:alphaTest,金属:调整粗糙度);
    • Box3 计算包围盒,实现相机自动聚焦;
  3. 避坑关键
    • 必须启动HTTP服务,避免跨域;
    • 植物模型开启双面+Alpha裁剪;
    • 不同模型适配不同曝光值,避免过亮/过暗。

Three.js实现更真实的3D地球🌍动态昼夜交替

作者 谢小飞
2026年1月22日 08:40

  这一切始于一个偶然的发现。前几天笔者在应用商店闲逛时,被一款3D动态壁纸深深吸引——那颗在手机屏幕上缓缓旋转的地球,光影随着时间自然流转,从阳光灿烂的白昼到星光点点的黑夜,过渡得如此丝滑而真实。那一刻,我被这种将宇宙微观化的美感震撼了。

  作为一名前端开发,笔者的第一反应不是“这个壁纸真好看”,而是“这个效果我能实现吗?”。这种奇特的好奇心驱使我开始了用代码复现这一视觉奇观的探索之旅。

  有趣的是,最打动我的不是最终地球模型的逼真程度,而是那个微妙的光影过渡——那条被称为“晨昏线”的光暗分界线,它既清晰又模糊,既分割又连接着地球的白天与黑夜。如何在代码中捕捉这种自然界的诗意过渡?这个问题成为了整个项目最迷人的挑战。

  现在我们一起踏上这段从视觉灵感转化为技术实现的旅程,我们将用Three.js绘制星辰与大海,用着色器计算光影效果,用数学公式模拟昼夜的交替;当你看到那颗由你亲手编码的地球在浏览器中开始第一次转动时,你会发现,前端不仅仅是职业,更是一种生活方式。

本文的最终效果可以访问这个链接查看查看,随手截图就是一张精美的壁纸。

最终效果

环境准备

  要实现这样的效果,我们先准备需要的一些素材贴图:

// 背景星空球体半径
const BACKGROUND_STARS_RADIUS = 200;

// 地球球体的半径
const EARTH_RADIUS = 5;

// 太阳半径
const SUN_RADIUS = 1;

// 月球半径
const MOON_RADIUS = 0.5

// 月球轨道半径
const MOON_TRACK_RADIUS = EARTH_RADIUS * 2

class Earth {
  constructor() {
    this.assetsLoader = new AssetsLoader();
    this.assetsLoader.load([
      {
        type: AssetsType.Texture,
        name: "sun", // 太阳贴图
        path: "/images/earth/8k_sun.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "moon", // 月球贴图
        path: "/images/earth/8k_moon.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "stars", // 星空背景贴图
        path: "/images/earth/8k_stars_milky_way.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "dayTexture", // 白天贴图
        path: "/images/earth/8k_earth_daymap.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "nightTexture", // 夜晚贴图
        path: "/images/earth/8k_earth_nightmap.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "normalMap", // 法线贴图
        path: "/images/earth/8k_earth_normal_map.jpg",
      },
      {
        type: AssetsType.Texture,
        name: "clouds", // 云层贴图
        path: "/images/earth/earth_clouds_2048.png",
      },
    ]);
    this.assetsLoader.on("onLoad", () => {
      this.initMesh();
    });
  }
}

本文所有素材均下载于Solar Textures

  素材准备好后,我们就可以来初始化场景下的物体;我们先创造我们美丽的蓝色星球,让它位于中心原点的位置:

class Earth {
  initEarth() {
    if (dayTexture && nightTexture) {
      const earthMaterial = new ShaderMaterial({
        uniforms: {
          dayTexture: { value: dayTexture },
          nightTexture: { value: nightTexture },
          sunPosition: { value: this.sunPosition },
        },
        vertexShader: earthVertexShader,
        fragmentShader: earthFragmentShader,
      });

      const earthGeometry = new SphereGeometry(EARTH_RADIUS, 128, 128);

      const earthMesh = new Mesh(earthGeometry, earthMaterial);
      earthMesh.position.set(0, 0, 0);

      this.basic.addScene(earthMesh);
    }
  }
}

  然后给空旷的宇宙安装一颗恒星,放在右边偏上的位置:

class Earth {
  sunPosition: Vector3 = new Vector3(20, 10, 0);
  initSun() {
    const sunTexture = this.assetsLoader.getAssets("sun") as Texture | null;

    if (sunTexture) {
      const sunGeometry = new SphereGeometry(SUN_RADIUS, 32, 32);
      const sunMaterial = new MeshBasicMaterial({
        map: sunTexture,
      });
      const sun = new Mesh(sunGeometry, sunMaterial);
      sun.position.copy(this.sunPosition);
      this.scene.add(sun);
    }
  }
}

  有意思的是,这里的太阳虽然看起来像个发光的球,但实际上它只是个“装饰品”;我们真正用到的其实是它的位置信息sunPosition,用于在后面模拟昼夜交替时,将太阳的位置信息传入到着色器代码中。

  在真实宇宙中,是地球绕着太阳转。但在我们的虚拟场景中,为了保持地球始终在画面中央(坐标原点),笔者耍了个小聪明——让太阳“绕着”地球转,同时给太阳一个自转。

class Earth {
  rotateVector3ByRadian(vec3: Vector3, axis: Vector3, radian: number) {
    // 创建旋转矩阵
    const matrix = new Matrix4()
    // 设置绕轴旋转的矩阵
    matrix.makeRotationAxis(axis.normalize(), radian)
    // 应用旋转矩阵到向量
    vec3.applyMatrix4(matrix)
  }
  render(clock: Clock) {
    rotateVector3ByRadian(
      this.sunPosition,
      new Vector3(0, 1, 0),
      0.0004,
    )
    if (this.sunMesh) {
      this.sunMesh.position.copy(sunPos)
      this.sunMesh.rotation.y += 0.002
    }
  }
}

  接着,给我们的宇宙增加一份浩瀚感,这里的星空背景通过球体加上贴图来进行渲染:

class Earth {
  initStarBackground() {
    const starsTexture = this.assetsLoader.getAssets("stars") as Texture | null;
    if (starsTexture) {
      const sphereGeometry = new SphereGeometry(
        BACKGROUND_STARS_RADIUS,
        64,
        64
      );
      sphereGeometry.scale(-1, 1, 1);
      const sphereMaterial = new MeshBasicMaterial({
        map: starsTexture,
        side: DoubleSide,
      });
      const sphere = new Mesh(sphereGeometry, sphereMaterial);
      this.scene.add(sphere);
    }
  }
}

  地球怎么能独自在宇宙中流浪呢?怎么能少得了它忠实的小跟班——月球呢?但只是放个月球太普通了,我决定给它加个专属的“跑道”track:

class Earth {
  moonPosition: Vector3 = new Vector3(0, MOON_TRACK_RADIUS, 0)
  initMoon() {
    const group = new Group()

    const trackGeo = new TorusGeometry(MOON_TRACK_RADIUS, 0.01, 64, 64)
    const trackMt = new MeshBasicMaterial({
      color: guiOption.moon.trackColor,
      transparent: true,
      opacity: 0.5,
    })
    const track = new Mesh(trackGeo, trackMt)
    group.add(track)

    const moonGeo = new SphereGeometry(MOON_RADIUS, 64, 64)
    const moonMt = new MeshBasicMaterial({
      map: moonTexture,
    })
    const moon = new Mesh(moonGeo, moonMt)
    moon.position.copy(this.moonPosition)
    group.add(moon)
    
    group.rotateX(MathUtils.degToRad(100))
    this.scene.add(group)    
  }
}

  那个半透明的轨道环其实是个视觉引导——它告诉用户“嘿,月球是沿着这条路径运动的”。虽然真实月球没有可见轨道,但这个设计增加了场景的科技感和可读性。

  当我看到月球带着它的光环开始绕着地球旋转时,那感觉就像完成了一个精密的宇宙钟表——每个部件都有它的位置,每个运动都有它的规律。这个小跟班让我们的地球不再孤单,整个太阳系开始有了“系统”的感觉。

实现昼夜分明

  前面我们搭建好了整个太阳系舞台,但此刻的地球还只是一个静止的球体,没有光影变化,没有昼夜交替。现在,是时候为这颗蓝色星球注入灵魂了。还记得我们初始化地球时预留的vertexShader和fragmentShader吗?那两个看似简单的GLSL代码文件,才是实现昼夜交替魔法的核心所在。

  如果说地球模型是个巨大的工厂,那么顶点着色器就是为每个工人(像素点)准备工牌的生产线。下面着色器代码其实在做两件重要的事情:

// 纹理坐标
varying vec2 vUv;
// 变换后的法线向量
varying vec3 vNormal;

void main(){
    vUv=uv;
    vNormal=normalize(normalMatrix*normal);
    gl_Position= projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

  vUv用来记录每个顶点的纹理坐标,vNormal计算并传递法线向量,每个顶点的法线就像一根小指针,直直地指向该点的“正上方”,normalize()函数让法线向量保持长度为1(归一化)。varying表示把顶点着色器的计算结果传递给下面的片元着色器。

  所以别看上面这段代码短,它可是整个昼夜效果的地基。它为地球表面每个点都准备好了:“我是谁(uv坐标)”、“我面朝哪里(法线)”,就等着片元着色器来判断:“你现在应该是白天还是黑夜”;下面是我们最重要的片元着色器代码了:

#ifdef GL_ES
precision mediump float;
#endif

uniform sampler2D dayTexture;
uniform sampler2D nightTexture;
uniform vec3 sunPosition;

varying vec2 vUv;
varying vec3 vNormal;

void main(){
    vec3 lightDir=normalize(sunPosition);
    
    float dotProduct=dot(normalize(vNormal),lightDir);
    
    if(dotProduct>0.){
        gl_FragColor=texture2D(dayTexture,vUv);
    }else{
        gl_FragColor=texture2D(nightTexture,vUv);
    }
}

  这段代码虽然只有短短的十几行,却决定了地球表面每一处是光明还是黑暗。GLSL语法比较难懂,我们下面就来详细介绍一下。

  首先我们将前面顶点着色器中处理好的单位法线向量vNormal接收,然后将传入的太阳的位置接收,通过normalize函数进行归一化操作,得到了太阳的方向;最轴将上面计算的法线向量和太阳的方向进行点积运算。

将太阳位置归一化后的方向向量,表示从原点指向太阳位置的单位向量,而不是从太阳位置出发指向原点,这一点需要注意。

  这里详细说一下点积运算的几何意义,在三维空间中,两个向量的点积公式是:

dot(A, B) = |A| × |B| × cos(θ)

  其中θ是A和B之间的夹角;而我们上面已经对两个向量都进行了归一化操作,因此公式简化为:

dot(A, B) = cos(θ)

  因此这里的角度θ其实代表了法线与太阳光线方向的夹角,我们通过一张图来理解,球体表面的蓝色箭头,表示法线;而原点黄色的箭头,表示太阳的方向:

太阳方向夹角

  因此在上面片元着色器代码中,计算得到的dotProduct变量,其实也是cos(θ)的值;我们通过对它的值进行判断,如果dotProduct大于0,则表示法线与太阳方向夹角小于90度,则表示当前点在白天,则使用白天贴图进行渲染;否则使用夜晚贴图进行渲染;运行后,我们就能看到地球的白天黑夜有明显的界线分隔了。

地球白天黑夜效果

  最后在太阳转动的同时,不要忘记更新地球材质的uniforms中的sunPosition属性:

class Earth {
  render(clock: Clock) {
    if (this.earthMaterial) {
      this.earthMaterial.uniforms.sunPosition.value.copy(this.sunPosition)
    }
  }
}

星空顶

  如果你最近去过高端楼盘展厅或者坐过某些豪华车型,大概率见过那个让人惊艳的设计——星空顶。无数光点在头顶缓缓闪烁,像是把整个银河系微缩在了方寸之间。这种将宇宙浪漫融入空间的设计,早已成为“高端感”的代名词。

  我们项目怎么能少了这样迷人的星空呢?不行,我们的项目也要向高端、豪华看齐。虽然之前我们用一张星空贴图作为背景,但是总感觉不够真实,缺少了星空那种忽明忽暗的光亮效果;我们在太阳到星空背景球体之间,通过Points来添加众多的星星,我们首先初始化星星的一些参数:

// 星星数量
const STARS_AMOUNT = 1000;
// 星星最小距离
const STARS_MIN_DISTANCE = 100;
// 星星最大距离
const STARS_MAX_DISTANCE = 200;
class Earth {
  initStars() {
    const starGeometry = new BufferGeometry();
    // 每个点的xyz坐标
    const positions = new Float32Array(STARS_AMOUNT * 3);
    // 每个点rgb颜色
    const colors = new Float32Array(STARS_AMOUNT * 3);
    // 每个点的初始大小
    const sizes = new Float32Array(STARS_AMOUNT);
    // 每个点的闪烁相位
    const phases = new Float32Array(STARS_AMOUNT);
    // 每个点的闪烁频率
    const frequencies = new Float32Array(STARS_AMOUNT);
  }
}

  由于我们想要生成从太阳到星空背景球体之间圆环内的随机点,因此我们可以通过极坐标的方式来计算,通过极坐标转换到三维空间内的坐标:

class Earth {
  initStars() {
    for (let i = 0; i < STARS_AMOUNT; i++) {
      const i3 = i * 3

      // 在球体空间内随机生成位置
      const distance = getRandomInt(STARS_MIN_DISTANCE, STARS_MAX_DISTANCE)
      const theta = Math.random() * Math.PI * 2 // 方位角
      const phi = Math.acos(2 * Math.random() - 1) // 极角

      // 球坐标转直角坐标
      positions[i3] = distance * Math.sin(phi) * Math.cos(theta)
      positions[i3 + 1] = distance * Math.sin(phi) * Math.sin(theta)
      positions[i3 + 2] = distance * Math.cos(phi)
    }
  }
}

  坐标位置搞定了,我们继续给每个点生成随即的颜色、大小、闪烁相位、闪烁频率属性:

// 随机颜色(偏向白色和蓝色)
const colorChoice = Math.random()
if (colorChoice < 0.7) {
  // 白色/淡黄色星星
  colors[i3] = 1.0 // R
  colors[i3 + 1] = 0.9 + Math.random() * 0.1 // G
  colors[i3 + 2] = 0.8 + Math.random() * 0.2 // B
} else if (colorChoice < 0.9) {
  // 蓝色星星
  colors[i3] = 0.4 + Math.random() * 0.3 // R
  colors[i3 + 1] = 0.6 + Math.random() * 0.3 // G
  colors[i3 + 2] = 1.0 // B
} else {
  // 红色/橙色星星
  colors[i3] = 1.0 // R
  colors[i3 + 1] = 0.5 + Math.random() * 0.3 // G
  colors[i3 + 2] = 0.3 + Math.random() * 0.2 // B
}

// 大小
sizes[i] = Math.random() * 2 + 0.5
// 闪烁频率
frequencies[i] = Math.random() * 0.5 + 0.5
// 闪烁相位
phases[i] = Math.random() * Math.PI * 2

  最后,我们通过BufferGeometry将这些数据传入Points对象中:

starGeometry.setAttribute("position", new BufferAttribute(positions, 3))
starGeometry.setAttribute("color", new BufferAttribute(colors, 3))
starGeometry.setAttribute("size", new BufferAttribute(sizes, 1))
starGeometry.setAttribute("phase", new BufferAttribute(phases, 1))
starGeometry.setAttribute("frequency", new BufferAttribute(frequencies, 1))

  然后创建Points对象,同时在uniforms中添加一个time属性,用于控制星星闪烁:

const starMaterial = new ShaderMaterial({
  uniforms: {
    time: { value: 0.0 },
  },
  vertexShader: starsVertexShader,
  fragmentShader: starsFragmentShader,
  transparent: true,
  blending: AdditiveBlending,
})
const stars = new Points(starGeometry, starMaterial)

  在我们的顶点着色器代码中,接收上面的顶点数据:

attribute float size;
attribute vec3 color;
attribute float phase;
attribute float frequency;

varying vec3 vColor;

uniform float time;

void main() {
    vColor = color;
    
    // 闪烁效果计算
    float blink = sin(time * frequency + phase) * 0.5 + 0.8;
    
    // 添加一些随机噪声使闪烁更自然
    float noise = sin(dot(position, vec3(12.9898, 78.233, 45.5432)) * 43758.5453) * 0.1;
    
    // 最终大小
    float finalSize = size * (blink + noise);
    
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = finalSize * (300.0 / -mvPosition.z);
    gl_Position = projectionMatrix * mvPosition;
}

  vColor用来将前面生成的点的颜色传递给片元着色器,让星星有独立的颜色;blink是实现星星闪烁的关键公式,time × frequency表示随时间变化的相位,phase控制每个粒子的初始相位偏移,使得闪烁不会同步进行;sin函数产生平滑的正弦波振荡,取值范围是[-1, 1],最终blink的范围是[0.3, 1.3],再乘以size初始化大小,得到了星星在不同时刻的最终大小。

  最后片元着色器代码如下:

#ifdef GL_ES
precision mediump float;
#endif

varying vec3 vColor;

void main() {
    // 圆形点
    float distanceToCenter = length(gl_PointCoord - vec2(0.5));
    if (distanceToCenter > 0.5) {
        discard;
    }
    
    // 添加一些发光效果
    float alpha = 1.0 - smoothstep(0.0, 0.5, distanceToCenter);
    
    gl_FragColor = vec4(vColor, alpha * 0.9);
}

美丽的晨昏线

  如果仔细观察真实的地球照片,你会发现一个迷人的细节:白天和黑夜之间,并没有一条生硬的分界线。取而代之的,是一片温柔过渡的“灰色地带”——这就是我们常说的晨昏线,也是日出日落时分最富诗意的区域。

  回头看我们之前实现的昼夜分割的效果,虽然功能完整,但是少了些自然界的柔美;现实世界的光影变化,从来不是非黑即白的开关,而是渐变的艺术;下面我们就来创造出一个平滑过渡的晨昏区域,让白昼缓缓融入黑夜。

  上面我们详细介绍了白天黑夜如何通过太阳光和法线进行判断,而其核心原理就是下面的计算公式:

float dotProduct=dot(normalize(vNormal),lightDir);

  dotProduct的取值范围是[-1, 1],当在0~1之间时,代表表面正对太阳,是白天;-1~0之间,表示背对太阳,是黑夜;我们想要让太阳在中间地带有一个过渡的范围,我们先给ShaderMaterial传入一个参数transitionWidth

const earthMaterial = new ShaderMaterial({
  uniforms: {
    dayTexture: { value: dayTexture },
    nightTexture: { value: nightTexture },
    // 新增过渡范围参数
    transitionWidth: { value: 0.2 },
  },
  vertexShader: earthVertexShader,
  fragmentShader: earthFragmentShader,
})

  然后,在片元着色器中添加过渡参数:

uniform float transitionWidth; 
void main(){
  float transitionCenter = 0.0; // 晨昏线
  float transitionStart = transitionCenter - transitionWidth * 0.5;
  float transitionEnd = transitionCenter + transitionWidth * 0.5;
}

  当传入transitionWidth是0.2时,计算得到下面的范围:

  • transitionStart = -0.1
  • transitionEnd = 0.1

  这就意味着在dotProduct点积值[-0.1, 0.1]范围内是过渡区域;然后使用smoothstep创建一个平滑插值:

void main(){
  // 使用smoothstep创建平滑过渡
  float mixFactor = smoothstep(transitionStart, transitionEnd, dotProduct);
}

  smoothstep函数的行为如下:

  • 当 dotProduct <= transitionStart 时:mixFactor = 0.0
  • 当 dotProduct >= transitionEnd 时:mixFactor = 1.0
  • 当 transitionStart < dotProduct < transitionEnd 时:mixFactor 平滑过渡

  因此,smoothstep函数实际上将dotProduct区间值[-1, 1]映射到mixFactor的[0, 1]范围内,并创建一个平滑过渡;其中[-1 , -0.1],映射为0,表示黑夜,[0.1 , 1],映射为1,表示白天,中间的(-0.1 , 0.1)映射到(0, 1)表示过渡的区域;我们通过一个表格来详细表示:

dotProduct值 mixFactor 纹理
-1 0 黑夜
-0.5 0 黑夜
-0.1 0 逐渐从黑夜过渡到白天
0 0.5 中间过渡区域
0.1 1 逐渐从白天过渡到黑夜
0.5 1 白天
1 1 白天

  最后使用mix函数将白天和黑夜的纹理进行混合:

void main(){
  // 采样纹理
  vec4 dayColor = texture2D(dayTexture, vUv);
  vec4 nightColor = texture2D(nightTexture, vUv);
  
  // 混合白天和黑夜纹理
  gl_FragColor = mix(nightColor, dayColor, mixFactor);
}

  这样,我们就实现了白天黑夜的过渡效果。

白天黑夜过渡效果

尾声:在代码中雕刻时光的意义

  当我们看着这个由自己亲手绘制的“微缩版太阳系”在浏览器中静静旋转时,一种奇特的感受油然而生——在这个微观的数字宇宙里,我既是造物主,也是最渺小的观察者。

  我们用了不到1000行的代码,就模拟了一个直径12742公里的星球上每时每刻的光影变迁。真实的地球需要24小时完成一次昼夜交替,我们的数字地球只需几分钟。这种尺度上的巨大反差让我突然明白:人类的所有创造,本质上都是对无限宇宙的有限翻译。

  那颗在轨道上“绕着”地球转的太阳,其实只是在绕着原点旋转的几行向量计算。但就是这简单的数学,却再现了我们祖先仰望天空时看到的景象——日出东方,日落西沉。从地心说到日心说,再到今天的代码模拟,人类对宇宙的理解在变,但那份想要理解世界的好奇心从未改变。

“给岁月以文明,而不是给文明以岁月”。——《三体》

  我们不是在追求让这个数字地球永恒运行,那颗正在你屏幕上旋转的蓝色星球,可能在下一秒就会浏览器缓存清理;那些闪烁的星星,可能随着标签页关闭而永远熄灭。

  而是在有限的运行时间里,赋予它最丰富的意义:让晨昏线温柔过渡,让星空会呼吸,让光影如诗歌般流动。我们关心的不是代码能运行多久,而是它在运行的每一帧里,是否足够美好,是否传递了我们对真实世界的观察与敬意。

  毕竟,在浩瀚的宇宙面前,所有文明都只是瞬间的火花。而最美的火花,不是燃烧得最久的那个,而是燃烧时最亮、最温暖的那个。

本文的最终效果可以访问这个链接查看查看。

如果觉得写得还不错,请关注我的掘金主页。更多文章请访问谢小飞的博客

昨天以前首页

ThreeJS GSAP动画库综合应用

2026年1月21日 16:31

本文档涵盖了Three.js与GSAP动画库综合应用的关键技术和实现方法,基于实际代码示例进行讲解,展示如何利用GSAP库创建丰富的3D动画效果。

1. GSAP动画库导入与初始化

在项目中使用GSAP动画库需要先导入并进行初始化:

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";

const textureLoader = new THREE.TextureLoader();
const particlesTexture = textureLoader.load("./textures/particles/1.png");

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  300
);

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

// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;

// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 设置时钟
const clock = new THREE.Clock();

// 鼠标的位置对象
const mouse = new THREE.Vector2();

// 创建投射光线对象
const raycaster = new THREE.Raycaster();

// 红色材质(用于交互效果)
const redMaterial = new THREE.MeshBasicMaterial({
  color: "#ff0000",
});

2. 立方体网格动画

2.1 立方体网格创建

创建一个由多个立方体组成的3D网格,用于第一屏的视觉效果: Title

// 创建立方体几何体
const cubeGeometry = new THREE.BoxBufferGeometry(2, 2, 2);
const material = new THREE.MeshBasicMaterial({
  wireframe: true,                    // 线框模式
});

// 创建立方体网格
let cubeArr = [];
let cubeGroup = new THREE.Group();
for (let i = 0; i < 5; i++) {
  for (let j = 0; j < 5; j++) {
    for (let z = 0; z < 5; z++) {
      const cube = new THREE.Mesh(cubeGeometry, material);
      cube.position.set(i * 2 - 4, j * 2 - 4, z * 2 - 4);  // 设置立方体位置
      cubeGroup.add(cube);
      cubeArr.push(cube);
    }
  }
}

scene.add(cubeGroup);

2.2 立方体网格旋转动画

使用GSAP库实现立方体网格的连续旋转动画:

// 立方体网格旋转动画
gsap.to(cubeGroup.rotation, {
  x: "+=" + Math.PI * 2,              // X轴旋转一周
  y: "+=" + Math.PI * 2,              // Y轴旋转一周
  duration: 10,                        // 动画持续时间10秒
  ease: "power2.inOut",                // 缓动函数
  repeat: -1,                          // 无限重复
});

3. 三角形几何体动画

3.1 随机三角形生成

创建一系列随机形状的三角形,形成独特的视觉效果:

// 创建三角形组
var sjxGroup = new THREE.Group();
for (let i = 0; i < 50; i++) {
  // 每一个三角形,需要3个顶点,每个顶点需要3个值
  const geometry = new THREE.BufferGeometry();
  const positionArray = new Float32Array(9);
  for (let j = 0; j < 9; j++) {
    if (j % 3 == 1) {
      positionArray[j] = Math.random() * 10 - 5;  // Y轴特殊处理
    } else {
      positionArray[j] = Math.random() * 10 - 5;
    }
  }
  geometry.setAttribute(
    "position",
    new THREE.BufferAttribute(positionArray, 3)
  );
  
  // 随机颜色
  let color = new THREE.Color(Math.random(), Math.random(), Math.random());
  const material = new THREE.MeshBasicMaterial({
    color: color,
    transparent: true,
    opacity: 0.5,
    side: THREE.DoubleSide,
  });
  
  // 根据几何体和材质创建物体
  let sjxMesh = new THREE.Mesh(geometry, material);
  sjxGroup.add(sjxMesh);
}
sjxGroup.position.set(0, -30, 0);     // 设置三角形组的位置
scene.add(sjxGroup);

3.2 三角形组旋转动画

使用GSAP库实现三角形组的连续旋转动画:

// 三角形组旋转动画
gsap.to(sjxGroup.rotation, {
  x: "-=" + Math.PI * 2,              // X轴反向旋转一周
  z: "+=" + Math.PI * 2,              // Z轴正向旋转一周
  duration: 12,                        // 动画持续时间12秒
  ease: "power2.inOut",                // 缓动函数
  repeat: -1,                          // 无限重复
});

4. 点光源与小球动画

4.1 球体和光源设置

创建一个带有光源的动态场景:

// 创建球体组
const sphereGroup = new THREE.Group();
const sphereGeometry = new THREE.SphereBufferGeometry(1, 20, 20);
const spherematerial = new THREE.MeshStandardMaterial({
  side: THREE.DoubleSide,
});
const sphere = new THREE.Mesh(sphereGeometry, spherematerial);
sphere.castShadow = true;              // 启用阴影投射

sphereGroup.add(sphere);

// 创建平面
const planeGeometry = new THREE.PlaneBufferGeometry(20, 20);
const plane = new THREE.Mesh(planeGeometry, spherematerial);
plane.position.set(0, -1, 0);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;            // 启用阴影接收
sphereGroup.add(plane);

// 添加环境光
const light = new THREE.AmbientLight(0xffffff, 0.5);
sphereGroup.add(light);

// 创建小球(带光源)
const smallBall = new THREE.Mesh(
  new THREE.SphereBufferGeometry(0.1, 20, 20),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
smallBall.position.set(2, 2, 2);

// 点光源
const pointLight = new THREE.PointLight(0xff0000, 3);
pointLight.castShadow = true;
pointLight.shadow.radius = 20;         // 阴影模糊度
pointLight.shadow.mapSize.set(512, 512); // 阴影贴图分辨率

smallBall.add(pointLight);
sphereGroup.add(smallBall);

sphereGroup.position.set(0, -60, 0);
scene.add(sphereGroup);

4.2 小球位置动画

使用GSAP库实现小球的位置动画,创造弹跳和移动效果:

// 小球水平移动动画
gsap.to(smallBall.position, {
  x: -3,                               // 移动到x=-3位置
  duration: 6,                         // 动画持续时间6秒
  ease: "power2.inOut",                // 缓动函数
  repeat: -1,                          // 无限重复
  yoyo: true,                          // 往返运动
});

// 小球垂直移动动画(弹跳效果)
gsap.to(smallBall.position, {
  y: 0,                                // 移动到y=0位置
  duration: 0.5,                       // 动画持续时间0.5秒
  ease: "power2.inOut",                // 缓动函数
  repeat: -1,                          // 无限重复
  yoyo: true,                          // 往返运动
});

5. 动画循环与渲染

5.1 动画循环函数

实现基本的动画循环和渲染:

function render() {
  let deltaTime = clock.getDelta();

  // 鼠标移动影响相机位置
  camera.position.x += (mouse.x * 10 - camera.position.x) * deltaTime * 5;
  
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

render(); // 启动动画循环

// 监听鼠标位置
window.addEventListener("mousemove", (event) => {
  mouse.x = event.clientX / window.innerWidth - 0.5;
  mouse.y = event.clientY / window.innerHeight - 0.5;
});

5.2 相机跟随动画

实现相机跟随鼠标移动的动画效果:

// 监听鼠标位置
window.addEventListener("mousemove", (event) => {
  mouse.x = event.clientX / window.innerWidth - 0.5;
  mouse.y = event.clientY / window.innerHeight - 0.5;
});

function render() {
  let deltaTime = clock.getDelta();

  // 鼠标移动影响相机位置
  camera.position.x += (mouse.x * 10 - camera.position.x) * deltaTime * 5;
  
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

6. 交互动画

6.1 鼠标点击交互动画

实现鼠标点击时的交互效果:

// 创建投射光线对象
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 监听鼠标点击事件
window.addEventListener("click", (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -((event.clientY / window.innerHeight) * 2 - 1);
  raycaster.setFromCamera(mouse, camera);
  let result = raycaster.intersectObjects(cubeArr);
  
  // 改变相交物体的材质颜色
  result.forEach((item) => {
    item.object.material = redMaterial;
  });
});

总结

本章详细介绍了Three.js中创建3D动画特效的几种主要动画效果:

  1. GSAP动画库导入与初始化:导入GSAP动画库并初始化所需变量
  2. 立方体网格动画:通过GSAP库实现的连续旋转动画,创造动态的3D网格效果
  3. 三角形几何体动画:随机生成的三角形组合,配合旋转动画创造抽象艺术效果
  4. 点光源与小球动画:带动画的小球与点光源,展现动态光影效果
  5. 动画循环与渲染:实现基本的动画循环和渲染机制
  6. 相机跟随动画:实现相机跟随鼠标移动的动画效果

此外,还实现了实现鼠标点击交互效果,共同构成了完整的3D动画体验。通过合理运用Three.js和GSAP动画库,可以创造出丰富多样的3D动画效果。

学习Three.js--纹理贴图(Texture)

2026年1月21日 15:01

学习Three.js--纹理贴图(Texture)

前置核心说明

纹理贴图是 Three.js 中让3D模型呈现真实外观的核心手段,本质是将2D图片(纹理)「贴」到3D几何体表面,替代单一的纯色材质,实现照片级的视觉效果(如墙面纹理、地面瓷砖、金属质感、木纹等)。

核心规则

  1. 核心流程加载图片 → 创建纹理对象(Texture) → 绑定到材质.map属性 → 几何体通过UV坐标映射纹理
  2. 颜色空间必设:加载纹理后必须设置 texture.colorSpace = THREE.SRGBColorSpace,否则图片会出现偏色(Three.js r152+ 版本新增,适配真实色彩);
  3. UV坐标是桥梁:UV坐标(2D)关联纹理图片和几何体顶点(3D),是纹理「贴在哪个位置」的核心控制手段;
  4. 材质适配:所有 Mesh 系列材质(MeshBasicMaterial/MeshStandardMaterial 等)都支持 map 纹理属性,仅 Line/Points/Sprite 材质不支持。

一、纹理核心概念与基础加载

1. 核心术语解析

术语 核心说明
纹理对象(Texture) Three.js 对2D图片的封装,包含图片数据、映射规则、重复模式等属性
UV坐标 2D纹理坐标系(U=横向,V=纵向),范围默认0~1,(0,0)=图片左下角,(1,1)=图片右上角
纹理加载器(TextureLoader) Three.js 专门用于加载图片并生成纹理对象的工具类
映射(Mapping) 纹理通过UV坐标与几何体顶点的绑定关系,决定图片哪部分贴在几何体哪个位置

2. 纹理加载(TextureLoader):完整用法与参数

TextureLoader 是加载纹理的核心工具,支持单张加载、批量加载,可处理加载进度/错误/完成回调。

2.1 基础加载
// 1. 创建纹理加载器实例
const texLoader = new THREE.TextureLoader();

// 2. 加载图片并创建纹理对象
// 语法:texLoader.load(图片路径, 加载完成回调, 加载进度回调, 加载错误回调)
const texture = texLoader.load(
  './gravelly_sand_diff_1k.jpg', // 必传:图片路径(本地/CDN)
  (texture) => { // 可选:加载完成回调
    console.log('纹理加载完成', texture);
  },
  (xhr) => { // 可选:加载进度回调(xhr=XMLHttpRequest)
    console.log(`加载进度:${(xhr.loaded / xhr.total) * 100}%`);
  },
  (err) => { // 可选:加载错误回调
    console.error('纹理加载失败', err);
  }
);

// 3. 关键:设置颜色空间(避免图片偏色,r152+必加)
texture.colorSpace = THREE.SRGBColorSpace;
2.2 TextureLoader 核心参数(load方法)
参数名 类型 必填 说明
url String 图片路径(支持本地相对路径、CDN链接、Base64)
onLoad Function 加载完成回调,参数为生成的Texture对象
onProgress Function 加载进度回调,参数为XMLHttpRequest对象
onError Function 加载失败回调,参数为错误对象
2.3 批量加载纹理(TextureLoader+Promise)
// 封装批量加载函数
async function loadTextures(urls) {
  const loader = new THREE.TextureLoader();
  const textures = [];
  for (const url of urls) {
    const texture = await new Promise((resolve, reject) => {
      loader.load(url, resolve, null, reject);
    });
    texture.colorSpace = THREE.SRGBColorSpace;
    textures.push(texture);
  }
  return textures;
}

// 使用:加载多张纹理
const urls = ['./texture1.jpg', './texture2.jpg'];
loadTextures(urls).then(textures => {
  console.log('所有纹理加载完成', textures);
});
2.4 跨域问题解决

加载本地图片或跨域CDN图片时,若出现 THREE.WebGLRenderer: Texture has no image data 错误:

  1. 本地开发:启动HTTP服务(如VSCode的Live Server),不要直接打开HTML文件;
  2. CDN/服务器:配置图片服务器的CORS跨域头(Access-Control-Allow-Origin: *);
  3. 临时方案:将图片转为Base64格式嵌入代码(适合小图片)。

二、UV坐标核心解析(纹理映射的关键)

UV坐标是「2D纹理」和「3D几何体」的桥梁,决定了纹理图片的哪部分会贴在几何体的哪个面上。

1. UV坐标基础规则

UV坐标 对应图片位置 说明
(0, 0) 图片左下角 纹理原点
(1, 0) 图片右下角 U轴(横向)最大值
(0, 1) 图片左上角 V轴(纵向)最大值
(1, 1) 图片右上角 UV坐标最大值
(0.5, 0.5) 图片中心 UV中点

2. 自定义UV坐标(BufferGeometry)

预设几何体(BoxGeometry/SphereGeometry)已内置UV坐标,若使用自定义 BufferGeometry,需手动定义UV属性:

2.1 完整映射(纹理全部显示)
// 步骤1:创建自定义几何体(4个顶点的矩形)
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
  -0.5, -0.5, 0, // 顶点0
   0.5, -0.5, 0, // 顶点1
   0.5,  0.5, 0, // 顶点2
  -0.5,  0.5, 0  // 顶点3
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

// 步骤2:定义UV坐标(完整映射,4个顶点对应图片4个角)
const uvs = new Float32Array([
  0, 0,  // 顶点0 → 图片左下角
  1, 0,  // 顶点1 → 图片右下角
  1, 1,  // 顶点2 → 图片右上角
  0, 1   // 顶点3 → 图片左上角
]);
// 绑定UV属性:itemSize=2(每2个值为一组UV坐标)
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
2.2 局部映射(仅显示纹理1/4区域)
// UV坐标范围设为0~0.5,仅映射图片左下角1/4区域
const uvs = new Float32Array([
  0,   0,   // 顶点0 → 图片(0,0)
  0.5, 0,   // 顶点1 → 图片(0.5,0)
  0.5, 0.5, // 顶点2 → 图片(0.5,0.5)
  0,   0.5  // 顶点3 → 图片(0,0.5)
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
2.3 圆形几何体映射(CircleGeometry)

CircleGeometry 内置了适配圆形的UV坐标,无需自定义,直接绑定纹理即可:

// 创建圆形几何体(半径2,分段数100,越高分段越平滑)
const geometry = new THREE.CircleGeometry(2, 100);

// 加载纹理
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./gravelly_sand_diff_1k.jpg');
texture.colorSpace = THREE.SRGBColorSpace;

// 创建材质(双面渲染,避免背面不可见)
const material = new THREE.MeshBasicMaterial({
  map: texture, // 绑定纹理
  side: THREE.DoubleSide
});

// 创建网格对象
const circleMesh = new THREE.Mesh(geometry, material);
scene.add(circleMesh);

3. 预设几何体UV特点

几何体 UV坐标特点 适用场景
BoxGeometry 每个面独立UV,纹理会贴到6个面上 立方体、盒子
SphereGeometry UV按经纬度分布,纹理包裹球体 星球、球体模型
PlaneGeometry 单平面UV,完整映射纹理 地面、墙面
CircleGeometry 圆形UV,纹理适配圆形 圆形地面、雷达图

三、纹理对象核心属性(参数详解+用法)

纹理对象(Texture)的核心属性决定了纹理的显示方式(重复、偏移、旋转等),是实现瓷砖阵列、UV动画的关键,以下是高频使用的属性:

1. 重复模式:wrapS / wrapT

控制纹理在U轴(横向)/V轴(纵向)超出0~1范围时的显示模式,必须配合 repeat 属性使用。

属性值 说明 示例
THREE.ClampToEdgeWrapping(默认) 超出范围时,拉伸纹理最后一行/列像素 纹理只显示一次,边缘拉伸
THREE.RepeatWrapping 超出范围时,重复显示纹理 实现瓷砖、地板阵列效果
THREE.MirroredRepeatWrapping 超出范围时,镜像重复显示纹理 无缝拼接的对称纹理
用法示例(地面瓷砖阵列)
const geometry = new THREE.PlaneGeometry(10, 10); // 10x10的地面
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./cizhuang.jpg');
texture.colorSpace = THREE.SRGBColorSpace;

// 1. 设置重复模式(U/V轴都重复)
texture.wrapS = THREE.RepeatWrapping; // U轴(横向)
texture.wrapT = THREE.RepeatWrapping; // V轴(纵向)

// 2. 设置重复数量(U轴10次,V轴10次)
texture.repeat.set(10, 10); // 格式:repeat.set(U重复数, V重复数)

// 3. 创建材质并绑定纹理
const material = new THREE.MeshLambertMaterial({ map: texture });
const groundMesh = new THREE.Mesh(geometry, material);
groundMesh.rotation.x = -Math.PI / 2; // 旋转为地面
scene.add(groundMesh);

2. 重复数量:repeat

  • 类型:THREE.Vector2(包含x/y属性,对应U/V轴);
  • 作用:设置纹理在U/V轴的重复次数,值越大,纹理显示越多块;
  • 用法:
    texture.repeat.x = 10; // U轴重复10次
    texture.repeat.y = 10; // V轴重复10次
    // 或批量设置
    texture.repeat.set(10, 10);
    

3. 偏移:offset

  • 类型:THREE.Vector2(x=U轴偏移,y=V轴偏移);
  • 范围:0~1(偏移1=整个纹理宽度/高度);
  • 作用:控制纹理在几何体上的偏移位置,常用于UV动画;
  • 用法:
    texture.offset.x = 0.5; // U轴向右偏移50%
    texture.offset.y = 0.5; // V轴向上偏移50%
    // 或批量设置
    texture.offset.set(0.5, 0.5);
    

4. 旋转:rotation

  • 类型:Number(弧度);
  • 作用:纹理绕中心点旋转,单位为弧度;
  • 配合属性:center(设置旋转中心,默认(0.5,0.5)即纹理中心);
  • 用法:
    texture.rotation = Math.PI / 4; // 旋转45°
    texture.center.set(0.5, 0.5); // 绕纹理中心旋转(默认值)
    // 绕图片左下角旋转
    texture.center.set(0, 0);
    

5. 纹理过滤:magFilter / minFilter

控制纹理在「放大/缩小」时的显示质量,解决纹理模糊/锯齿问题:

属性 作用 推荐值
magFilter 纹理放大时的过滤方式 THREE.LinearFilter(线性过滤,平滑)
minFilter 纹理缩小时的过滤方式 THREE.LinearMipmapLinearFilter(Mipmap线性过滤,最清晰)
用法:
// 提升纹理显示质量(解决模糊)
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.generateMipmaps = true; // 生成Mipmap(minFilter生效必备)

6. 各向异性过滤:anisotropy

  • 类型:Number;
  • 作用:提升纹理在倾斜视角下的清晰度(如地面纹理斜看时不模糊);
  • 用法:
    // 获取渲染器支持的最大各向异性值
    const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
    texture.anisotropy = maxAnisotropy; // 设置为最大值,效果最佳
    

四、纹理高级应用场景(完整用法+示例)

1. UV动画(纹理滚动)

通过动态修改 texture.offset 实现纹理滚动(如流水、火焰、移动的地面):

// 加载纹理
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./water.jpg');
texture.colorSpace = THREE.SRGBColorSpace;
// 开启重复模式(动画更自然)
texture.wrapS = THREE.RepeatWrapping;
texture.repeat.x = 5; // U轴重复5次

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  // U轴偏移量递增,实现纹理向右滚动
  texture.offset.x += 0.01;
  // 可选:V轴偏移,实现斜向滚动
  // texture.offset.y += 0.005;
  
  controls.update();
  renderer.render(scene, camera);
}
animate();

2. 阵列+UV动画组合(瓷砖地面滚动)

// 加载瓷砖纹理
const texture = texLoader.load('./cizhuang.jpg');
texture.colorSpace = THREE.SRGBColorSpace;

// 1. 设置阵列模式
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10); // 10x10瓷砖

// 2. 动画循环(UV滚动)
function animate() {
  requestAnimationFrame(animate);
  texture.offset.x += 0.005; // 缓慢向右滚动
  texture.offset.y += 0.002; // 缓慢向上滚动
  
  controls.update();
  renderer.render(scene, camera);
}
animate();

3. 多纹理叠加(基础色+法线+粗糙度)

PBR材质(MeshStandardMaterial)支持多纹理叠加,实现更真实的质感:

// 加载多组纹理
const texLoader = new THREE.TextureLoader();
const colorMap = texLoader.load('./wood_color.jpg'); // 基础色纹理
const normalMap = texLoader.load('./wood_normal.jpg'); // 法线纹理(凹凸感)
const roughnessMap = texLoader.load('./wood_roughness.jpg'); // 粗糙度纹理

// 设置颜色空间(仅基础色纹理需要)
colorMap.colorSpace = THREE.SRGBColorSpace;

// 创建PBR材质,绑定多纹理
const material = new THREE.MeshStandardMaterial({
  map: colorMap, // 基础色
  normalMap: normalMap, // 法线(凹凸)
  roughnessMap: roughnessMap, // 粗糙度
  roughness: 1.0, // 全局粗糙度(与纹理叠加)
  metalness: 0.1 // 金属度
});

五、完整实战示例(纹理加载+UV自定义+阵列+动画)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Three.js 纹理贴图完整示例</title>
  <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
  <script>
    import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';
    import { OrbitControls }  from "https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/controls/OrbitControls.js";

    // 1. 创建三大核心
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    camera.position.set(3, 3, 5);

    // 2. 加载纹理(示例使用CDN纹理,避免本地路径问题)
    const texLoader = new THREE.TextureLoader();
    // 瓷砖纹理(CDN示例)
    const texture = texLoader.load('https://threejs.org/examples/textures/tiles/tiles_diff.jpg', () => {
      renderer.render(scene, camera); // 加载完成后渲染
    });
    // 关键:设置颜色空间,避免偏色
    texture.colorSpace = THREE.SRGBColorSpace;

    // 3. 纹理配置(阵列+过滤优化)
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(8, 8); // 8x8瓷砖阵列
    // 提升纹理质量
    texture.magFilter = THREE.LinearFilter;
    texture.minFilter = THREE.LinearMipmapLinearFilter;
    texture.generateMipmaps = true;
    // 开启各向异性过滤
    texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

    // 4. 创建地面几何体(PlaneGeometry)
    const groundGeo = new THREE.PlaneGeometry(10, 10);
    const groundMat = new THREE.MeshStandardMaterial({
      map: texture,
      side: THREE.DoubleSide
    });
    const groundMesh = new THREE.Mesh(groundGeo, groundMat);
    groundMesh.rotation.x = -Math.PI / 2; // 旋转为地面
    scene.add(groundMesh);

    // 5. 创建立方体(自定义UV示例)
    const cubeGeo = new THREE.BoxGeometry(2, 2, 2);
    // 自定义立方体UV(仅修改正面,其他面默认)
    const uvs = new Float32Array([
      0, 0, 1, 0, 1, 1, 0, 1, // 正面UV(完整映射)
      0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5, // 右侧面UV(1/4映射)
      // 其他面UV省略,使用默认值
    ]);
    cubeGeo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
    const cubeMat = new THREE.MeshStandardMaterial({ map: texture });
    const cubeMesh = new THREE.Mesh(cubeGeo, cubeMat);
    cubeMesh.position.y = 1; // 立方体放在地面上
    scene.add(cubeMesh);

    // 6. 添加光源(PBR材质需要光源)
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    const dirLight = new THREE.DirectionalLight(0xffffff, 1);
    dirLight.position.set(5, 8, 5);
    scene.add(ambientLight, dirLight);

    // 7. 轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;

    // 8. UV动画循环
    function animate() {
      requestAnimationFrame(animate);
      // 纹理缓慢滚动(U轴+V轴)
      texture.offset.x += 0.002;
      texture.offset.y += 0.001;
      // 立方体旋转
      cubeMesh.rotation.x += 0.01;
      cubeMesh.rotation.y += 0.01;
      
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // 9. 窗口适配
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

示例效果

e628d623-98ee-42f3-85e8-aa2eb050cd5d.png

  1. 场景包含10x10的瓷砖地面,纹理8x8阵列显示,且缓慢滚动;
  2. 地面上有一个立方体,正面完整映射纹理,右侧面仅显示纹理1/4区域;
  3. 支持鼠标旋转/缩放视角,立方体自动旋转,纹理滚动动画流畅;
  4. 纹理显示清晰,无偏色、无模糊问题。

六、纹理优化与避坑指南

1. 性能优化技巧

  • 图片尺寸优化:纹理图片尺寸建议为2的幂次方(如256x256、512x512、1024x1024),GPU处理更快;
  • 复用纹理对象:多个模型使用同一张纹理时,复用同一个Texture对象,避免重复加载;
  • 关闭不必要的功能:静态纹理关闭generateMipmaps,减少内存占用;
  • 压缩纹理:使用basis Universal等压缩纹理格式,减小图片体积,提升加载速度。

2. 常见坑点与解决

问题 原因 解决方法
纹理偏色 未设置colorSpace 添加texture.colorSpace = THREE.SRGBColorSpace
纹理不显示 跨域问题/图片路径错误 启动HTTP服务/检查路径/配置CORS
纹理模糊 过滤方式未优化 设置magFilter=LinearFilter+minFilter=LinearMipmapLinearFilter
阵列不生效 未设置wrapS/wrapT 开启texture.wrapS/wrapT = THREE.RepeatWrapping
UV动画卡顿 偏移量递增过快 减小offset递增步长(如0.001~0.01)

核心总结

  1. 核心流程TextureLoader加载图片 → 设置colorSpace → 配置纹理属性(wrap/repeat/offset) → 绑定到材质.map → 几何体UV映射
  2. UV坐标:0~1范围,是纹理与几何体的桥梁,自定义几何体需手动设置UV属性;
  3. 关键属性
    • wrapS/wrapT:控制重复模式,实现瓷砖阵列;
    • repeat:设置重复数量;
    • offset:实现UV动画;
    • colorSpace:避免纹理偏色(r152+必加);
  4. 优化原则:图片尺寸为2的幂次方,开启各向异性过滤,复用纹理对象,关闭不必要的Mipmap。

ThreeJS 详解光线投射与物体交互

2026年1月21日 10:42

本文档涵盖了Three.js中光线投射(Raycaster)与物体交互的关键概念和实现方法,基于实际代码示例进行讲解。

1. 光线投射基础概念

光线投射是一种在三维空间中追踪光线路径的技术,主要用于检测鼠标与3D物体的交互。在Three.js中,Raycaster类提供了光线投射功能,可以用来检测鼠标点击、悬停等事件与场景中物体的交点。

alt text

2. Raycaster对象创建

首先需要创建一个Raycaster对象和鼠标位置对象:

// 创建投射光线对象
const raycaster = new THREE.Raycaster();

// 鼠标的位置对象
const mouse = new THREE.Vector2();

3. 场景设置

在进行光线投射之前,需要先设置好场景、相机和待检测的物体:

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,                                    // 视野角度
  window.innerWidth / window.innerHeight, // 宽高比
  0.1,                                  // 近平面
  300                                   // 远平面
);

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

// 创建几何体和材质
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({
  wireframe: true,                       // 线框模式显示
});

const redMaterial = new THREE.MeshBasicMaterial({
  color: "#ff0000",                      // 红色材质
});

// 创建多个立方体用于交互测试
let cubeArr = [];
for (let i = -5; i < 5; i++) {
  for (let j = -5; j < 5; j++) {
    for (let z = -5; z < 5; z++) {
      const cube = new THREE.Mesh(cubeGeometry, material);
      cube.position.set(i, j, z);        // 设置立方体位置
      scene.add(cube);
      cubeArr.push(cube);                // 将立方体添加到数组中便于检测
    }
  }
}

4. 鼠标事件监听

监听鼠标事件并将屏幕坐标转换为标准化设备坐标(NDC):

// 监听鼠标点击事件
window.addEventListener("click", (event) => {
  // 将鼠标位置归一化为设备坐标 [-1, 1]
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -((event.clientY / window.innerHeight) * 2 - 1);
  
  // 从相机设置光线投射器
  raycaster.setFromCamera(mouse, camera);
  
  // 检测与物体的交点
  let result = raycaster.intersectObjects(cubeArr);
  
  // 对相交的物体进行处理
  result.forEach((item) => {
    item.object.material = redMaterial;  // 改变相交物体的材质
  });
});

5. 鼠标移动事件监听(可选)

除了点击事件,也可以监听鼠标移动事件实现实时交互:

// 监听鼠标移动事件(注释掉的部分)
/*
window.addEventListener("mousemove", (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -((event.clientY / window.innerHeight) * 2 - 1);
  raycaster.setFromCamera(mouse, camera);
  let result = raycaster.intersectObjects(cubeArr);
  result.forEach((item) => {
    item.object.material = redMaterial;
  });
});
*/

6. 渲染器配置

配置渲染器以支持场景渲染:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;

// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

7. 轨道控制器设置

添加轨道控制器以支持相机交互:

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

8. 动画循环

在动画循环中更新控制器并渲染场景:

// 设置时钟
const clock = new THREE.Clock();

function render() {
  let time = clock.getElapsedTime();

  controls.update();                       // 更新控制器
  renderer.render(scene, camera);          // 渲染场景
  
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

9. 响应式设计

处理窗口大小变化:

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

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

10. Raycaster方法详解

10.1 setFromCamera方法

该方法根据相机和鼠标位置设置光线:

raycaster.setFromCamera(mouse, camera);

10.2 intersectObjects方法

该方法检测光线与指定物体数组的交点:

let result = raycaster.intersectObjects(cubeArr);

返回的结果是一个数组,每个元素包含交点信息,如交点位置、相交的物体等。

11. 交点结果处理

交点结果包含丰富的信息:

result.forEach((item) => {
  // item.distance: 交点与射线起点之间的距离
  // item.point: 交点的三维坐标
  // item.face: 相交的面
  // item.object: 相交的物体
  item.object.material = redMaterial;      // 更改相交物体的材质
});

12. 性能优化建议

  1. 只对需要交互的物体进行检测,避免检测整个场景
  2. 合理设置检测频率,避免每帧都进行检测造成性能问题
  3. 使用分组管理需要检测的物体,便于批量处理

总结

光线投射是Three.js中实现用户交互的重要技术,通过Raycaster类可以轻松实现鼠标与3D物体的交互。主要步骤包括:

  1. 创建Raycaster和鼠标位置对象
  2. 设置场景、相机和待检测物体
  3. 监听鼠标事件并转换坐标
  4. 使用setFromCamera方法设置光线
  5. 使用intersectObjects方法检测交点
  6. 处理交点结果实现交互效果

通过这种技术,可以实现点击选择物体、悬停高亮、拖拽等功能,大大增强用户的交互体验。

学习Three.js--缓冲类型几何体(BufferGeometry)

2026年1月21日 09:50

学习Three.js--缓冲类型几何体(BufferGeometry)

前置核心说明

BufferGeometry 是 Three.js 中所有几何体的底层核心(BoxGeometry/SphereGeometry 等预设几何体均基于它构建),也是官方唯一推荐使用的几何体类型(旧版 Geometry 已被废弃)。

核心区别与优势(为什么用 BufferGeometry)

类型 核心特点 性能 官方态度
BufferGeometry(缓冲几何体) 顶点数据存储在「类型化数组」(Float32Array 等)中,直接对接 GPU 内存 极高(GPU 直接读取,无数据转换) 主推,唯一维护
Geometry(旧版几何体) 顶点数据存储在普通数组中,需转换后才能给 GPU 使用 较低(多一层数据转换) 废弃,不再维护

核心逻辑

BufferGeometry 本身是「空容器」,没有任何预设形状,你需要通过定义顶点数据(坐标、颜色、纹理坐标等)来「自定义任意几何形状」,核心是:
类型化数组(顶点数据)→ BufferAttribute(属性封装)→ BufferGeometry(绑定属性)→ 渲染对象(Mesh/Line/Points)


一、BufferGeometry 核心概念与基础用法

1. 核心术语解释

  • 顶点(Vertex):3D 空间中的一个点,由 X/Y/Z 三个坐标值组成,是构成几何体的最基本单元;
  • 类型化数组:如 Float32Array(32位浮点数组),专门用于存储顶点数据,比普通数组更节省内存、GPU 读取更快;
  • BufferAttribute:Three.js 对「类型化数组」的封装,告诉 Three.js 「数组中的数据如何分组解析」(比如每3个值为一组表示一个顶点坐标);
  • 属性(Attribute):几何体的「数据维度」,如 position(顶点坐标)、color(顶点颜色)、uv(纹理坐标)、normal(法线)等,一个几何体可绑定多个属性。

2. 基础使用流程

以下是从「创建空几何体」到「渲染自定义形状」的完整流程

步骤1:创建空的 BufferGeometry 容器
// 语法:无参数,创建空的缓冲几何体
const geometry = new THREE.BufferGeometry();
步骤2:定义顶点数据(类型化数组)

顶点数据必须用 类型化数组(不能用普通数组),常用:

  • Float32Array:存储浮点型数据(坐标、颜色、UV 等,最常用);
  • Uint16Array:存储无符号16位整数(索引数据)。
// 顶点坐标数据:每3个值为一组(X,Y,Z),表示一个顶点的3D坐标
// 示例:6个顶点,对应2个三角形(Mesh 默认按三角面渲染)
const vertices = new Float32Array([
  0, 0, 0,   // 顶点1:(0,0,0)
  50, 0, 0,  // 顶点2:(50,0,0)
  0, 100, 0, // 顶点3:(0,100,0) → 第一个三角形(顶点1-2-3)
  0, 0, 10,  // 顶点4:(0,0,10)
  0, 0, 100, // 顶点5:(0,0,100)
  50, 0, 10  // 顶点6:(50,0,10) → 第二个三角形(顶点4-5-6)
]);
步骤3:创建 BufferAttribute
// 语法:new THREE.BufferAttribute(类型化数组, 组内元素数量, 是否归一化)
// 关键:itemSize=3 → 每3个值为一组(对应X/Y/Z坐标)
const positionAttribute = new THREE.BufferAttribute(vertices, 3);
BufferAttribute 参数 类型 默认值 核心说明
array TypedArray 必传,存储顶点数据的类型化数组
itemSize Number 必传,每组的元素数量(坐标=3,颜色=3/4,UV=2)
normalized Boolean false 是否归一化数据(颜色数据常用,将0-255转为0-1)
usage Number THREE.StaticDrawUsage 数据使用方式(静态/动态,默认静态即可)
步骤4:将属性绑定到几何体
// 语法:geometry.setAttribute(属性名, BufferAttribute对象)
// 核心:属性名必须是固定值,如 "position"(坐标)、"color"(颜色)、"uv"(纹理)
geometry.setAttribute('position', positionAttribute);
步骤5:创建材质和渲染对象
// 材质:MeshBasicMaterial 不受光照影响,适合调试
const material = new THREE.MeshBasicMaterial({
  color: 0x00ff00,    // 基础颜色(无顶点颜色时生效)
  wireframe: false,   // 是否显示线框(true=线框,false=实体)
  side: THREE.DoubleSide // 双面渲染(避免背面不可见)
});

// 创建网格对象(将几何体+材质绑定)
const mesh = new THREE.Mesh(geometry, material);

// 添加到场景
scene.add(mesh);

二、BufferGeometry 核心参数与常用属性

1. 构造函数(无参数)

// 始终无参数,创建空几何体,后续通过 setAttribute 绑定数据
const geometry = new THREE.BufferGeometry();

2. 核心属性

属性名 类型 说明 示例
attributes Object 存储所有绑定的属性(position/color/uv 等) geometry.attributes.position → 获取坐标属性
index BufferAttribute 索引缓冲区(优化顶点重复,下文详解) geometry.setIndex(索引数组)
boundingBox Box3 几何体的包围盒(自动计算,用于碰撞检测/裁剪) geometry.computeBoundingBox() → 计算包围盒
boundingSphere Sphere 几何体的包围球 geometry.computeBoundingSphere()
drawRange Object 渲染范围(只渲染部分顶点) geometry.drawRange = { start: 0, count: 3 } → 只渲染前3个顶点

3. 核心方法

方法名 说明 示例
setAttribute(name, attribute) 绑定属性到几何体 geometry.setAttribute('position', attr)
getAttribute(name) 获取已绑定的属性 geometry.getAttribute('position')
removeAttribute(name) 移除属性 geometry.removeAttribute('color')
setIndex(array) 设置索引缓冲区 geometry.setIndex(new Uint16Array([0,1,2]))
computeBoundingBox() 计算几何体包围盒 geometry.computeBoundingBox()
computeBoundingSphere() 计算几何体包围球 geometry.computeBoundingSphere()
computeVertexNormals() 计算顶点法线(让光照生效) geometry.computeVertexNormals()
dispose() 销毁几何体(释放内存) geometry.dispose()

三、BufferGeometry 进阶用法

1. 索引缓冲区(Index):减少顶点重复

问题场景

绘制一个矩形(由两个三角面组成),直接定义顶点需要6个(重复2个):
(0,0,0)、(1,0,0)、(0,1,0)、(1,0,0)、(1,1,0)、(0,1,0)

索引解决方案
  • 定义4个唯一顶点 + 6个索引(指定三角面的顶点顺序),节省内存:
// 步骤1:创建空几何体
const geometry = new THREE.BufferGeometry();

// 步骤2:定义4个唯一顶点(无重复)
const vertices = new Float32Array([
  0, 0, 0,  // 顶点0
  1, 0, 0,  // 顶点1
  0, 1, 0,  // 顶点2
  1, 1, 0   // 顶点3
]);
const posAttr = new THREE.BufferAttribute(vertices, 3);
geometry.setAttribute('position', posAttr);

// 步骤3:定义索引(每3个值为一组,指定三角面的顶点索引)
// 第一个三角面:顶点0→1→2;第二个三角面:顶点1→3→2
const indices = new Uint16Array([
  0, 1, 2, 
  1, 3, 2
]);
// 设置索引缓冲区
geometry.setIndex(new THREE.BufferAttribute(indices, 1));

// 步骤4:创建材质和网格
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
核心优势
  • 顶点数量从6个减少到4个,数据量降低33%;
  • 复杂几何体(如球体)可减少大量重复顶点,性能提升显著。

2. 顶点颜色(Color Attribute):每个顶点自定义颜色

核心逻辑

给几何体绑定 color 属性,材质开启 vertexColors: true,即可让每个顶点显示自定义颜色,三角面内自动渐变。

// 步骤1:创建空几何体 + 顶点坐标
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
  0, 0, 0,   // 顶点0
  1, 0, 0,   // 顶点1
  0, 1, 0    // 顶点2
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

// 步骤2:定义顶点颜色(每3个值为一组,RGB,0-1范围)
const colors = new Float32Array([
  1, 0, 0,   // 顶点0:红色
  0, 1, 0,   // 顶点1:绿色
  0, 0, 1    // 顶点2:蓝色
]);
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

// 步骤3:材质开启顶点颜色(关键)
const material = new THREE.MeshBasicMaterial({
  vertexColors: true, // 启用顶点颜色(覆盖基础color)
  side: THREE.DoubleSide
});

// 步骤4:创建网格
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
效果

三角面会从红色顶点渐变到绿色,再渐变到蓝色,实现多彩渐变效果。

3. 法线属性(Normal):让光照生效

MeshStandardMaterial 等受光照影响的材质,需要「法线数据」才能计算光影,可手动定义或自动计算:

// 方式1:自动计算法线(推荐,适合简单几何体)
geometry.computeVertexNormals();

// 方式2:手动定义法线(精准控制,复杂几何体)
const normals = new Float32Array([
  0, 0, 1,  // 顶点0:法线朝向Z轴正方向
  0, 0, 1,  // 顶点1:法线朝向Z轴正方向
  0, 0, 1   // 顶点2:法线朝向Z轴正方向
]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));

4. UV 纹理坐标:绑定纹理贴图

UV 坐标(0-1范围)用于将2D图片贴到3D几何体上,每2个值为一组(U=横向,V=纵向):

// 定义UV坐标(每2个值为一组)
const uvs = new Float32Array([
  0, 0,  // 顶点0:贴图左下角
  1, 0,  // 顶点1:贴图右下角
  0, 1   // 顶点2:贴图左上角
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

// 加载纹理并绑定到材质
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('texture.jpg');
const material = new THREE.MeshBasicMaterial({ map: texture });

四、完整实战示例(自定义三角面+索引+顶点颜色)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>BufferGeometry 完整示例</title>
  <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
  <script type="module">
    import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js'
    import { OrbitControls }  from "https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/controls/OrbitControls.js"
    // 1. 创建三大核心
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    camera.position.z = 2;

    // 2. 自定义 BufferGeometry(带索引+顶点颜色)
    const geometry = new THREE.BufferGeometry();

    // 2.1 顶点坐标(4个唯一顶点,绘制矩形)
    const vertices = new Float32Array([
      -0.5, -0.5, 0,  // 顶点0
       0.5, -0.5, 0,  // 顶点1
      -0.5,  0.5, 0,  // 顶点2
       0.5,  0.5, 0   // 顶点3
    ]);
    geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

    // 2.2 顶点颜色(4个顶点对应4组颜色)
    const colors = new Float32Array([
      1, 0, 0,  // 顶点0:红
      0, 1, 0,  // 顶点1:绿
      0, 0, 1,  // 顶点2:蓝
      1, 1, 0   // 顶点3:黄
    ]);
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

    // 2.3 索引缓冲区(指定三角面的顶点顺序)
    const indices = new Uint16Array([
      0, 1, 2,  // 第一个三角面:0→1→2
      1, 3, 2   // 第二个三角面:1→3→2
    ]);
    geometry.setIndex(new THREE.BufferAttribute(indices, 1));

    // 2.4 计算法线(可选,若用受光照材质则需要)
    geometry.computeVertexNormals();

    // 3. 创建材质(启用顶点颜色)
    const material = new THREE.MeshBasicMaterial({
      vertexColors: true, // 启用顶点颜色
      side: THREE.DoubleSide,
      wireframe: false
    });

    // 4. 创建网格并添加到场景
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);

    // 5. 轨道控制器(交互)
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;

    // 6. 动画循环
    function animate() {
      requestAnimationFrame(animate);
      mesh.rotation.x += 0.01;
      mesh.rotation.y += 0.01;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // 7. 窗口适配
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

示例效果

56382a99-8b86-4af3-9b51-172939c77ff5.png

  • 场景中显示一个彩色矩形(由两个三角面组成);
  • 矩形顶点分别为红、绿、蓝、黄,面内自动渐变;
  • 支持鼠标旋转/缩放视角,矩形缓慢旋转。

五、注意事项与性能优化

1. 关键注意点

  • 类型化数组必须正确:顶点坐标用 Float32Array,索引用 Uint16Array/Uint32Array,不能混用;
  • itemSize 必须匹配:坐标=3,颜色=3/4,UV=2,索引=1,错误会导致几何体显示异常;
  • 双面渲染:自定义几何体默认只渲染正面,需设置 side: THREE.DoubleSide 避免背面不可见;
  • 内存释放:不再使用的几何体,必须调用 geometry.dispose() 释放内存,避免内存泄漏。

2. 性能优化技巧

  • 使用索引缓冲区:减少重复顶点,降低数据量;
  • 控制顶点数量:复杂几何体按需分段,避免顶点过多;
  • 静态数据复用:相同形状的几何体复用,无需重复创建;
  • drawRange 局部渲染:只渲染需要显示的顶点范围,减少计算。

核心总结

  1. 核心地位:BufferGeometry 是 Three.js 所有几何体的底层核心,官方唯一推荐使用;
  2. 核心流程:类型化数组→BufferAttribute→setAttribute→绑定到渲染对象;
  3. 核心优化:索引缓冲区可减少顶点重复,是高性能自定义几何体的关键;
  4. 核心属性position(坐标)、color(颜色)、uv(纹理)、normal(法线)是最常用的属性;
  5. 性能原则:用类型化数组、复用几何体、释放无用数据,最大化 GPU 渲染效率。

ThreeJS 精通粒子特效

2026年1月21日 09:40

本文档涵盖了Three.js中粒子特效的关键概念和实现方法,基于实际代码示例进行讲解。

1. 粒子系统基础

1.1 点材质 (PointsMaterial) 设置

点材质是创建粒子特效的基础,可以通过多种参数配置粒子外观:

// 设置点材质
const pointsMaterial = new THREE.PointsMaterial();
pointsMaterial.size = 0.1;                           // 粒子大小
pointsMaterial.color.set(0xfff000);                 // 粒子颜色
pointsMaterial.sizeAttenuation = true;              // 是否根据相机深度衰减粒子大小
pointsMaterial.sizeAttenuation = true;              // 相机深度衰减

1.2 粒子纹理配置

为了增强粒子的视觉效果,通常会使用纹理:

// 载入纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("./textures/particles/2.png");

// 设置点材质纹理
pointsMaterial.map = texture;                       // 纹理贴图
pointsMaterial.alphaMap = texture;                  // 透明度贴图
pointsMaterial.transparent = true;                  // 启用透明度
pointsMaterial.depthWrite = false;                  // 禁用深度写入
pointsMaterial.blending = THREE.AdditiveBlending;   // 混合模式

2. 粒子几何体创建

2.1 使用缓冲几何体 (BufferGeometry)

高效的粒子系统通常使用缓冲几何体来存储大量顶点数据:

// 创建粒子几何体
const particlesGeometry = new THREE.BufferGeometry();
const count = 5000;                                // 粒子数量

// 设置缓冲区数组
const positions = new Float32Array(count * 3);     // 位置数组
const colors = new Float32Array(count * 3);        // 颜色数组

// 设置顶点位置
for (let i = 0; i < count * 3; i++) {
  positions[i] = (Math.random() - 0.5) * 100;      // 随机位置
  colors[i] = Math.random();                        // 随机颜色
}

// 将属性添加到几何体
particlesGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
particlesGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

2.2 从现有几何体创建粒子

可以从现有的几何体(如球体)转换为粒子:

// 创建球几何体
const sphereGeometry = new THREE.SphereBufferGeometry(3, 30, 30);

// 删除UV属性(如果不需要纹理映射)
delete sphereGeometry.attributes.uv;

// 创建粒子系统
const points = new THREE.Points(sphereGeometry, pointsMaterial);
scene.add(points);

3. 星河粒子系统

3.1 基础星河效果

创建具有随机分布的星河效果:

// 生成星河粒子
const generateGalaxy = () => {
  geometry = new THREE.BufferGeometry();
  const positions = new Float32Array(params.count * 3);
  const colors = new Float32Array(params.count * 3);

  // 循环生成点
  for (let i = 0; i < params.count; i++) {
    // 当前的点应该在哪一条分支的角度上
    const branchAngel = (i % params.branch) * ((2 * Math.PI) / params.branch);

    // 当前点距离圆心的距离
    const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);
    const current = i * 3;

    // 随机偏移
    const randomX = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
    const randomY = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;
    const randomZ = (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;

    // 计算最终位置
    positions[current] = Math.cos(branchAngel + distance * params.rotateScale) * distance + randomX;
    positions[current + 1] = 0 + randomY;
    positions[current + 2] = Math.sin(branchAngel + distance * params.rotateScale) * distance + randomZ;

    // 混合颜色,形成渐变色
    const mixColor = centerColor.clone();
    mixColor.lerp(endColor, distance / params.radius);

    colors[current] = mixColor.r;
    colors[current + 1] = mixColor.g;
    colors[current + 2] = mixColor.b;
  }

  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

  // 设置点材质
  material = new THREE.PointsMaterial({
    size: params.size,
    sizeAttenuation: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    map: particlesTexture,
    alphaMap: particlesTexture,
    transparent: true,
    vertexColors: true,                             // 使用顶点颜色
  });

  points = new THREE.Points(geometry, material);
  scene.add(points);
};

4. 雪花粒子系统

4.1 多层雪花效果

创建多个层次的雪花粒子系统:

function createPoints(url, size = 0.5) {
  const particlesGeometry = new THREE.BufferGeometry();
  const count = 10000;

  // 设置缓冲区数组
  const positions = new Float32Array(count * 3);
  const colors = new Float32Array(count * 3);

  // 设置顶点
  for (let i = 0; i < count * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 100;    // 在空间内随机分布
    colors[i] = Math.random();                      // 随机颜色
  }

  particlesGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  particlesGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

  // 设置点材质
  const pointsMaterial = new THREE.PointsMaterial();
  pointsMaterial.size = size;                       // 不同大小的粒子
  pointsMaterial.color.set(0xfff000);
  pointsMaterial.sizeAttenuation = true;

  // 载入纹理
  const textureLoader = new THREE.TextureLoader();
  const texture = textureLoader.load(`./textures/particles/${url}.png`);

  pointsMaterial.map = texture;
  pointsMaterial.alphaMap = texture;
  pointsMaterial.transparent = true;
  pointsMaterial.depthWrite = false;
  pointsMaterial.blending = THREE.AdditiveBlending;
  pointsMaterial.vertexColors = true;               // 启用顶点颜色

  const points = new THREE.Points(particlesGeometry, pointsMaterial);
  scene.add(points);
  return points;
}

// 创建多层雪花效果
const points = createPoints("1", 1.5);
const points2 = createPoints("xh", 1);
const points3 = createPoints("xh", 2);

4.2 粒子动画

为粒子系统添加动画效果:

function render() {
  let time = clock.getElapsedTime();

  // 旋转动画
  points.rotation.x = time * 0.3;
  points2.rotation.x = time * 0.5;
  points2.rotation.y = time * 0.4;
  points3.rotation.x = time * 0.2;
  points3.rotation.y = time * 0.2;

  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

5. 渲染器配置

5.1 基础渲染器设置

针对粒子系统优化渲染器:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);

// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;

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

6. 粒子系统优化技巧

6.1 混合模式

使用适当的混合模式提升视觉效果:

// 加法混合,常用于发光效果
pointsMaterial.blending = THREE.AdditiveBlending;

6.2 深度写入控制

控制粒子的深度写入行为:

// 禁用深度写入,避免粒子间的遮挡问题
pointsMaterial.depthWrite = false;

6.3 粒子大小衰减

根据距离调整粒子大小:

// 启用大小衰减,远处的粒子看起来更小
pointsMaterial.sizeAttenuation = true;

7. 控制器和辅助工具

7.1 轨道控制器

添加交互式视角控制:

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 设置控制器阻尼,让控制器更有真实效果
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

8. 响应式设计

处理窗口大小变化:

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

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

总结

本章详细介绍了Three.js中粒子特效的各个方面,包括:

  1. 粒子系统基础概念
  2. 点材质的配置和优化
  3. 缓冲几何体的使用
  4. 星河和雪花粒子系统的创建
  5. 粒子动画和交互
  6. 渲染优化技巧

通过合理运用这些技术,可以创建出丰富多彩的粒子特效,如星系、雪花、烟雾、火焰等视觉效果。关键在于理解粒子的几何体构建、材质配置以及性能优化方法,从而在保证视觉效果的同时维持良好的运行性能。

Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字

作者 烛阴
2026年1月20日 22:21

Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字

在 Three.js 中渲染文字有多种方案,本文介绍一种高性能且灵活的方案:CSS2DRenderer。它能将 DOM 元素无缝嵌入 3D 场景,同时保留 CSS 的全部能力。

为什么选择 DOM/CSS 方案?

方案 优点 缺点
TextGeometry 真正的 3D 几何体 性能开销大,需加载字体文件
CSS2DRenderer 清晰锐利、CSS 全特性、高性能 无法被 3D 物体遮挡

CSS2DRenderer 的核心优势:

  • 文字永远清晰:浏览器原生渲染,不受 3D 缩放影响
  • CSS 全特性:阴影、渐变、动画、backdrop-filter 磨砂玻璃效果
  • 性能优异:DOM 渲染与 WebGL 渲染分离,互不干扰

核心实现

1. 初始化双渲染器

import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// WebGL 渲染器(渲染 3D 场景)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// CSS2D 渲染器(渲染 DOM 标签)
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
  position: 'absolute',
  top: '0',
  pointerEvents: 'none' // 关键:让鼠标事件穿透
});
document.body.appendChild(labelRenderer.domElement);

关键点pointerEvents: 'none' 让鼠标事件穿透 DOM 层,否则无法拖拽 3D 场景。

2. 创建 CSS2D 标签

const createLabel = (text, position) => {
  const div = document.createElement('div');
  div.className = 'label';
  div.textContent = text;
  const label = new CSS2DObject(div);
  label.position.copy(position);
  return label;
};

// 将标签添加到 3D 物体上
earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));

标签添加到 earth 网格后,会自动跟随地球的旋转和位移。

3. 双渲染器同步渲染

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);      // 渲染 3D 场景
  labelRenderer.render(scene, camera); // 渲染 DOM 标签
}

4. CSS 样式

.label {
  color: #FFF;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: bold;
  padding: 5px 10px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 4px;
  backdrop-filter: blur(4px); /* 磨砂玻璃效果 */
}

backdrop-filter: blur() 实现的磨砂玻璃效果,在纯 WebGL 中需要复杂的后处理才能实现,而 CSS 一行代码搞定。

完整代码

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// 场景、相机
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.z = 5;

// WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);

// CSS2D 渲染器
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
  position: 'absolute',
  top: '0',
  pointerEvents: 'none'
});
document.body.appendChild(labelRenderer.domElement);

// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 创建地球
const earth = new THREE.Mesh(
  new THREE.SphereGeometry(1, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x2233ff, roughness: 0.5 })
);
scene.add(earth);

// 生成地球纹理
const canvas = Object.assign(document.createElement('canvas'), { width: 512, height: 512 });
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1e90ff';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#228b22';
for (let i = 0; i < 20; i++) {
  ctx.beginPath();
  ctx.arc(Math.random() * 512, Math.random() * 512, Math.random() * 50 + 20, 0, Math.PI * 2);
  ctx.fill();
}
earth.material.map = new THREE.CanvasTexture(canvas);

// 创建标签工厂函数
const createLabel = (text, position) => {
  const div = document.createElement('div');
  div.className = 'label';
  div.textContent = text;
  const label = new CSS2DObject(div);
  label.position.copy(position);
  return label;
};

earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));

// 光源
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 3, 5);
scene.add(dirLight);

// 动画循环
(function animate() {
  requestAnimationFrame(animate);
  earth.rotation.y += 0.005;
  controls.update();
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
})();

// 响应窗口变化
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  labelRenderer.setSize(innerWidth, innerHeight);
});

📂 核心代码与完整示例:     my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

学习Three.js--材质(Material)

2026年1月19日 16:45

学习Three.js--材质(Material)

前置必读:材质通用规则 & 公共核心参数

一、通用规则

  1. 所有网格类材质MeshXXXMaterial)都是 THREE.Material 的子类,共用一套核心公共参数,无需重复记忆;
  2. 材质的参数均为配置对象({})传参,支持创建后通过 material.参数名 = 值 动态修改;
  3. 材质创建后可以复用给多个物体,能极大节省内存(比如100个立方体用同一个材质,只创建1个即可);
  4. 所有颜色值参数:支持16进制0xffffff、RGB字符串#fff/rgb(255,255,255)THREE.Color对象。

二、 所有材质【公共核心参数】

以下参数适用于 90% 的材质(MeshBasic/MeshLambert/MeshPhong/Standard/Physical 全部包含),一次记忆,全部受用!

{
  color: 0xffffff, // 基础颜色,默认白色
  transparent: false, // 是否开启透明效果,默认关闭 ❗开启透明必须设为true,opacity才生效
  opacity: 1, // 透明度,取值范围 0(完全透明) ~ 1(完全不透明),默认1
  visible: true, // 物体是否可见,true显示/false隐藏,默认true
  side: THREE.FrontSide, // 渲染物体的哪个面,核心取值3种:
  // THREE.FrontSide 只渲染正面(默认) | THREE.BackSide 只渲染背面 | THREE.DoubleSide 双面渲染
  depthWrite: true, // 是否写入深度缓冲区,默认true;透明物体建议设为false,避免透明重叠闪烁
  depthTest: true, // 是否开启深度测试,默认true;关闭后物体可能会穿透其他物体
  wireframe: false, // 是否以线框模式渲染物体,默认false(实体),true则只显示几何体的边框线
  wireframeLinewidth: 1, // 线框的线宽,默认1;❗注意:浏览器对WebGL的线宽支持有限,一般只能到1-2px
  alphaTest: 0, // 透明度裁剪阈值,取值0~1;像素透明度低于该值则不渲染,解决透明边缘锯齿问题
  map: null, // 颜色纹理贴图,传 THREE.Texture 纹理对象,用于给物体贴图片(比如木纹、皮肤)
}

一、网格基础材质 THREE.MeshBasicMaterial

核心特点

不受光照影响、无明暗变化,始终纯色/纹理显示,不需要添加光源就能渲染,性能最高

适用场景

调试几何体、UI元素、纯色简单物体、快速原型开发

完整调用语法

const material = new THREE.MeshBasicMaterial({
  // 公共参数 + 自身专属参数(无专属,全是公共参数)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  alphaTest: 0,
  depthWrite: true
});

专属注意点

  • 该材质无视所有光源,添加AmbientLight/DirectionalLight等都不会有任何效果;
  • wireframe: true 时,能直观看到几何体的顶点和面结构,调试几何体形状的最佳材质

二、网格漫反射材质 THREE.MeshLambertMaterial

核心特点

基于Lambert漫反射光照模型,对光源有响应、有明暗过渡,无高光效果,计算量低

适用场景

哑光粗糙表面(纸张、墙面、布料、水泥、木头、哑光塑料)

完整调用语法

const material = new THREE.MeshLambertMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  //  自身核心专属参数
  emissive: 0x000000, // 自发光颜色,默认黑色(无自发光)
  emissiveIntensity: 1, // 自发光强度,取值0~∞,默认1
  emissiveMap: null, // 自发光纹理贴图,给物体贴发光纹理
  vertexColors: false, // 是否使用顶点颜色,默认false
  flatShading: false, // 是否使用平面着色,默认false(平滑着色)
});

所有参数详细释义(含公共+专属)

  1. 公共参数同上文,不再重复;
  2. emissive:自发光色,不会照亮其他物体,只是让材质自身显示该颜色,比如设为0xff0000,物体会带红色发光效果;
  3. emissiveIntensity:调节自发光的亮度,值越大发光越明显;
  4. flatShading: true:几何体每个面会显示纯色,无平滑过渡,适合做低多边形风格;
  5. vertexColors: true:如果几何体设置了顶点颜色,材质会渲染顶点的颜色渐变。

注意点

  • 必须添加光源,否则物体会显示纯黑色,完全不可见;
  • 无高光参数,无法实现光泽效果,是哑光质感的最优选择。

三、网格高光材质 THREE.MeshPhongMaterial

核心特点

基于Phong高光光照模型,支持「漫反射+镜面高光」,对光源响应,有明显的高光光斑,计算量略高于MeshLambertMaterial

适用场景

有光泽的物体(漆面塑料、陶瓷、湿润的石头、抛光木头、电镀金属)

完整调用语法

const material = new THREE.MeshPhongMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  //  继承MeshLambert的自发光参数
  emissive: 0x000000,
  emissiveIntensity: 1,
  emissiveMap: null,
  flatShading: false,
  //  自身核心高光专属参数(重中之重)
  specular: 0x111111, // 高光颜色,默认浅灰色
  shininess: 30, // 高光的「亮度+范围」,取值0~1000+,默认30
});

高光核心参数释义

  1. specular:决定高光光斑的颜色,比如:
    • 塑料材质:设为0xffffff(白色高光),最真实;
    • 金属材质:设为和color相同的颜色,高光和本体同色;
  2. shininess:核心高光参数,值越小 → 高光范围越大、亮度越低值越大 → 高光范围越小、亮度越高
    • 示例:shininess:5 → 大面积柔和高光;shininess:100 → 小面积刺眼高光。

注意点

  1. 同样必须添加光源,无光源则全黑;
  2. Phong的高光效果是模拟的光泽,不是物理真实的,高光光斑略显生硬,但性能不错,适合简单光泽场景;
  3. 该材质的高光计算在顶点上,低面数几何体的高光会有锯齿。

四、物理基础材质 THREE.MeshStandardMaterial 【默认首选】

核心特点

基于 PBR(Physically Based Rendering)物理渲染,完全遵循真实世界的光照规律,参数少但效果极致真实,计算量适中,Three.js官方推荐的默认首选材质,也是目前最常用的材质!

适用场景

99%的现代3D场景:游戏、产品展示、建筑可视化、电商3D商品、写实模型,能替代所有传统材质(Lambert/Phong)

核心优势

  • 传统材质(Phong/Lambert)是「模拟光照」,PBR是「物理光照」,效果真实度天差地别;
  • 参数语义化,无需调复杂的高光/漫反射,通过「粗糙度+金属度」就能实现所有质感。

完整调用语法

const material = new THREE.MeshStandardMaterial({
  // 公共参数(全部支持)
  color: 0x00ff00,
  transparent: false,
  opacity: 1,
  side: THREE.FrontSide,
  wireframe: false,
  map: null,
  alphaTest: 0,
  depthWrite: true,
  //  PBR 核心四大基础参数 (必学!!)
  roughness: 0.5,    // 粗糙度,取值 0(镜面光滑) ~ 1(完全粗糙),默认0.5
  metalness: 0.5,    // 金属度,取值 0(非金属) ~ 1(纯金属),默认0.5
  emissive: 0x000000,// 自发光颜色,默认黑色,无自发光
  emissiveIntensity:1,// 自发光强度,默认1
  //  PBR 进阶纹理贴图(提升真实度)
  roughnessMap: null, // 粗糙度纹理,让物体表面粗糙度有细节变化
  metalnessMap: null, // 金属度纹理,让物体部分区域是金属、部分是非金属
  normalMap: null,    // 法线贴图,模拟物体表面凹凸细节(无需修改几何体)
  aoMap: null,        // 环境遮蔽贴图,模拟物体缝隙/凹陷处的阴影,提升层次感
  displacementMap: null, // 置换贴图,真正修改几何体顶点,实现凹凸效果
  //  特殊物理参数
  clearcoat: 0,       // 清漆层,取值0~1,默认0;模拟车漆/指甲油的表层光泽
  clearcoatRoughness:0,// 清漆层粗糙度,取值0~1,默认0
  transmission: 0,    // 透射率,取值0~1,默认0;模拟玻璃/半透明物体的透光效果
});

PBR 核心参数【必懂必记】(4个)

这是该材质的灵魂,记住取值范围+含义,就能调出所有质感,无任何例外!

  1. roughness (粗糙度) 0~1
    • 0 → 镜面级光滑,反射所有光线,高光极强(比如镜子、抛光金属);
    • 1 → 完全粗糙,无任何高光,纯哑光(比如纸张、水泥);
    • 中间值 → 半光泽(比如塑料、漆面、木头)。
  2. metalness (金属度) 0~1
    • 0 → 非金属材质(塑料、玻璃、石头、布料),反射环境光,高光颜色固定;
    • 1 → 纯金属材质(金、银、铜),反射自身颜色,高光和本体同色;
    • 中间值 → 合金材质(比如不锈钢、黄铜)。
  3. emissive + emissiveIntensity:自发光,不会照亮其他物体,比如霓虹灯、指示灯、发光logo。
  4. 颜色color:金属度0时是「物体本身颜色」;金属度1时,color决定金属的颜色(比如金色0xffd700、银色0xcccccc)。

质感调试万能公式

  • 哑光塑料 → roughness:0.8, metalness:0
  • 镜面塑料 → roughness:0.1, metalness:0
  • 不锈钢 → roughness:0.2, metalness:1, color:0xcccccc
  • 黄金 → roughness:0.1, metalness:1, color:0xffd700
  • 木头 → roughness:0.7, metalness:0, color:0x8b4513
  • 玻璃 → roughness:0, metalness:0, transmission:0.9

五、高级物理材质 THREE.MeshPhysicalMaterial

核心特点

MeshStandardMaterial 的超集,完全兼容其所有参数+方法,在其基础上增加了更精细的物理光学参数,实现极致写实的物理效果,PBR天花板材质!

适用场景

超写实3D场景:汽车车漆、玻璃/水晶、宝石、珍珠、丝绸、拉丝金属、多层镀膜材质

完整调用语法

const material = new THREE.MeshPhysicalMaterial({
  //  完全包含 MeshStandardMaterial 的 所有参数(无需重复写)
  color: 0x00ff00,
  roughness: 0.5,
  metalness: 0.5,
  emissive: 0x000000,
  clearcoat:0,
  transmission:0,
  //  新增的【专属高级参数】(核心!)
  clearcoatRoughness: 0,    // 清漆层粗糙度,0=镜面清漆,1=哑光清漆
  sheen: 0,                 // 光泽层,0~1,默认0;模拟丝绸、天鹅绒、布料的柔和高光
  sheenRoughness: 0.5,      // 光泽层粗糙度,0~1,默认0.5
  sheenColor: 0xffffff,     // 光泽层颜色,默认白色
  iridescence: 0,           // 虹彩/色散,0~1,默认0;模拟珍珠、肥皂泡、CD光盘的彩虹反光
  iridescenceIOR: 1.5,      // 虹彩折射率,默认1.5
  iridescenceThicknessRange: [100, 400], // 虹彩厚度范围,控制彩虹颜色
  anisotropy: 0,            // 各向异性,-1~1,默认0;模拟拉丝金属、头发的方向性高光
  thickness: 0.1,           // 厚度,0~∞,默认0.1;配合transmission使用,模拟玻璃的厚度(厚玻璃颜色更深)
  reflectivity: 1,          // 折射率,1~2.333,默认1;控制玻璃/水的折射效果
});

专属参数核心释义

  1. sheen:丝绸/天鹅绒的核心参数,能实现「反向高光」,布料的质感全靠它;
  2. iridescence:彩虹色散,珍珠、肥皂泡、车漆的金属珠光效果,必调参数;
  3. anisotropy:拉丝金属的核心,比如不锈钢拉丝面板、头发,高光会沿着拉丝方向延伸;
  4. thickness:玻璃厚度,比如厚玻璃杯的边缘会偏绿,薄玻璃则透明,真实度拉满。

注意点

  • 该材质计算量比MeshStandardMaterial略高,但现代浏览器/显卡完全能承载;
  • 能用MeshStandardMaterial实现的效果,就用它;需要极致细节时,再用MeshPhysicalMaterial

六、点材质 THREE.PointsMaterial

核心特点

专门用于 THREE.Points(粒子系统),只能渲染点/粒子,不能渲染网格几何体

适用场景

粒子特效:星空、烟雾、雨滴、雪花、灰尘、点云模型、星空背景

完整调用语法

const material = new THREE.PointsMaterial({
  color: 0xffffff,          // 粒子颜色,默认白色
  size: 1,                  // 粒子大小(像素),默认1
  sizeAttenuation: true,    // 是否开启「近大远小」,默认true;false则所有粒子大小一致
  transparent: true,        // 是否透明,默认false(粒子特效必开)
  opacity: 1,               // 透明度,0~1,默认1
  map: null,                // 粒子纹理贴图,比如用圆形贴图做圆点粒子,用雪花贴图做雪花
  alphaTest: 0.1,           // 透明裁剪,解决粒子边缘锯齿,建议设0.1
  depthWrite: false,        // 关闭深度写入,解决透明粒子重叠时的闪烁问题(必开)
  blending: THREE.AdditiveBlending, // 混合模式,AdditiveBlending=叠加,粒子更亮(适合火焰/星光)
  emissive: 0xffffff,       // 自发光颜色,默认白色
  emissiveIntensity:1,      // 自发光强度
});
// 配套使用:创建粒子系统
const geometry = new THREE.BufferGeometry().setFromPoints( pointsArr ); // 点数组
const points = new THREE.Points( geometry, material );
scene.add(points);

核心参数

  1. size:粒子的像素大小,值越大粒子越大;
  2. sizeAttenuation: true:粒子会像真实物体一样,离相机越近越大,越远越小,必开;
  3. depthWrite: false:透明粒子的必备参数,否则粒子重叠时会出现穿透/闪烁的问题。

七、线材质 「LineBasicMaterial + LineDashedMaterial」

通用特点

专门用于 THREE.Line / THREE.LineSegments(线段/线条几何体),只能渲染线条,不能渲染网格

适用场景

绘制辅助线、坐标轴、边框、电路图、激光线、轨迹线等

7.1 实线材质 THREE.LineBasicMaterial

const material = new THREE.LineBasicMaterial({
  color: 0xffffff,    // 线条颜色,默认白色
  linewidth: 1,       // 线宽(像素),默认1;❗浏览器限制,一般最大只能到2px
  transparent: false, // 是否透明
  opacity: 1,         // 透明度
  dashed: false,      // 是否虚线,默认false(实线)
});

7.2 虚线材质 THREE.LineDashedMaterial【高频使用】

const material = new THREE.LineDashedMaterial({
  color: 0xffffff,    // 线条颜色
  linewidth: 1,       // 线宽
  transparent: false,
  opacity: 1,
  dashSize: 3,        // 【核心】虚线的「实线部分长度」,默认3
  gapSize: 1,         // 【核心】虚线的「空白部分长度」,默认1
  scale: 1,           // 缩放比例,默认1;整体缩放dashSize和gapSize
});
// ❗ 必加:给线段几何体调用 computeLineDistances(),否则虚线不生效!
const lineGeometry = new THREE.BufferGeometry().setFromPoints( pointsArr );
lineGeometry.computeLineDistances(); 
const line = new THREE.Line( lineGeometry, material );
scene.add(line);

注意点

  1. 虚线材质必须调用 geometry.computeLineDistances(),否则会显示为实线,无虚线效果;
  2. linewidth 受WebGL限制,无法设置超大线宽,如需粗线条,建议用网格几何体模拟。

八、精灵材质 THREE.SpriteMaterial

核心特点

专门用于 THREE.Sprite(精灵/广告牌),本质是始终朝向相机的2D平面,不会随视角旋转,永远正面朝向屏幕

适用场景

3D场景中的2D元素:图标、血条、标签、子弹、火焰粒子、雪花粒子、2D精灵特效

完整调用语法

const material = new THREE.SpriteMaterial({
  color: 0xffffff,    // 精灵颜色
  map: null,          // 精灵纹理贴图(核心,一般都用贴图,比如图标图片)
  transparent: true,  // 是否透明,默认false(必开,否则背景是白色)
  opacity: 1,         // 透明度
  rotation: 0,        // 精灵旋转角度(弧度制),默认0;绕中心旋转
  center: new THREE.Vector2(0.5,0.5), // 旋转中心,默认中心(0.5,0.5)
  sizeAttenuation: true, // 是否近大远小,默认true;false则精灵大小固定
  depthWrite: false,  // 关闭深度写入,解决精灵遮挡问题
});
// 配套使用
const sprite = new THREE.Sprite( material );
sprite.scale.set(1,1,1); // 设置精灵大小
scene.add(sprite);

注意点

  • 精灵是2D平面,没有厚度,所以side参数无效;
  • 精灵的大小通过 sprite.scale 控制,不是材质参数。

九、阴影材质 THREE.ShadowMaterial

核心特点

官方正确特性:材质自身完全透明,不接收任何光照,只显示「其他物体投射过来的阴影」

适用场景

地面/平面作为阴影接收器,比如创建一个透明的地面,只显示3D物体的阴影,不显示地面本身,让物体看起来像悬浮在空中,是实现真实阴影的最优方案!

完整调用语法

const material = new THREE.ShadowMaterial({
  opacity: 0.5, // 【唯一核心参数】阴影的透明度,取值0~1,默认0.5
  // 无其他专属参数,公共参数仅支持 transparent/depthWrite
  transparent: true, // 固定为true,无需修改
  depthWrite: false, // 建议关闭,避免阴影闪烁
});

使用注意点

  1. 必须开启光源的阴影投射 + 物体的阴影投射 + 地面的阴影接收
    directionalLight.castShadow = true; // 光源投射阴影
    cube.castShadow = true; // 物体投射阴影
    ground.receiveShadow = true; // 地面接收阴影
    
  2. opacity 越小,阴影越淡;越大,阴影越深。

十、自定义着色器材质 THREE.ShaderMaterial

核心特点

完全自定义顶点着色器(VertexShader)片元着色器(FragmentShader),使用GLSL语言编写,能实现 任何Three.js内置材质做不到的视觉效果,是Three.js的高级核心功能

适用场景

水体、火焰、溶解效果、扭曲变形、玻璃折射、扫描线、像素化、后处理特效等自定义视觉效果

完整调用语法

const material = new THREE.ShaderMaterial({
  //  核心:自定义GLSL着色器代码
  vertexShader: `
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    attribute vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `, // 顶点着色器:控制物体的顶点位置/变形
  fragmentShader: `
    uniform vec3 uColor;
    void main() {
      gl_FragColor = vec4(uColor, 1.0);
    }
  `, // 片元着色器:控制物体的像素颜色/纹理/特效
  //  自定义全局变量(JS传递数据到GLSL,核心!)
  uniforms: {
    uColor: { value: new THREE.Color(0x00ff00) }, // 颜色变量
    uTime: { value: 0 }, // 时间变量,用于做动画
    uTexture: { value: texture }, // 纹理变量
  },
  //  公共材质参数(全部支持)
  transparent: true,
  opacity: 1,
  side: THREE.DoubleSide,
  depthWrite: false,
  wireframe: false,
});

核心参数释义

  1. vertexShader:顶点着色器,运行在GPU上,控制每个顶点的位置、法线、纹理坐标等;
  2. fragmentShader:片元着色器,运行在GPU上,控制每个像素的最终颜色,所有视觉特效都在这里实现;
  3. uniforms:JS和GLSL之间的数据桥梁,可以在JS中动态修改uniforms.uTime.value,GLSL中就能实时获取,实现动画。

注意点

  • Three.js会自动注入内置变量(比如projectionMatrixmodelViewMatrix),无需手动声明;
  • GLSL语言和JS语法不同,需要单独学习基础,入门简单。

十一、原生着色器材质 THREE.RawShaderMaterial

核心特点

THREE.ShaderMaterial 几乎完全一致,唯一的区别是:不会自动注入Three.js的任何内置变量

适用场景

高级自定义着色器,需要完全掌控GLSL代码的所有变量,比如移植外部WebGL着色器、编写极致优化的着色器、实现底层图形学效果

调用语法

const material = new THREE.RawShaderMaterial({
  vertexShader: `
    // ❗ 必须手动声明所有变量,Three.js不自动注入
    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;
    attribute vec3 position;
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 uColor;
    void main() {
      gl_FragColor = vec4(uColor, 1.0);
    }
  `,
  uniforms: { uColor: { value: new THREE.Color(0x00ff00) } },
  transparent: true,
});

核心区别(和ShaderMaterial)

ShaderMaterial:自动注入内置变量,开发效率高,适合绝大多数自定义场景; RawShaderMaterial:无自动注入,完全手动,灵活性最高,适合高级底层开发。


材质学习总结 & 优先级建议(必看)

1. 材质使用优先级(按推荐度排序)

  1. 优先用 MeshStandardMaterial → 99%场景够用,物理真实、参数简单、性能均衡;
  2. 需要极致细节 → 用 MeshPhysicalMaterial
  3. 调试几何体 → 用 MeshBasicMaterial
  4. 简单哑光物体+低性能设备 → 用 MeshLambertMaterial
  5. 简单光泽物体 → 用 MeshPhongMaterial
  6. 粒子/点云 → 用 PointsMaterial
  7. 线条 → 用 LineBasicMaterial/LineDashedMaterial
  8. 2D精灵/图标 → 用 SpriteMaterial
  9. 透明地面+阴影 → 用 ShadowMaterial
  10. 自定义特效 → 用 ShaderMaterial/RawShaderMaterial

2. 核心记忆点

  • 所有网格材质共用一套公共参数,无需重复记忆;
  • PBR材质的核心是「粗糙度+金属度」,记住0~1的取值规则,就能调出所有质感;
  • 材质可以复用,多个物体用同一个材质能极大优化性能;
  • 透明材质建议开启 transparent:true + depthWrite:false,避免闪烁/穿透问题。
❌
❌