普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月22日掘金 前端

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. 基础的着色器动画实现

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

三个方法优化JS的setTimeout实现的倒计误差,看完包会!

2026年1月22日 17:42

你肯定遇到过这种情况。页面上有一个倒计时,显示“距离活动结束还有 10 秒”。你屏住呼吸,准备在最后一刻点击抢购按钮。但奇怪的是,倒计时从 10 跳到 9 时,好像停顿了一下,或者跳得特别快。最终,你点击按钮时,系统提示“活动已结束”。

这不是你的错觉。前端实现的倒计时,确实存在误差。今天,我们就来聊聊这个误差是怎么产生的,以及我们能做些什么来减小它。

误差从何而来?

要理解误差,我们得先看看最常见的前端倒计时是怎么工作的。

1. 核心机制:setInterval 与 setTimeout

大多数倒计时使用 JavaScript 的 setInterval 或递归的 setTimeout 来实现。代码逻辑很简单:

  1. 设定一个目标时间(比如活动结束时间)。
  2. 每秒执行一次函数,计算“当前时间”与“目标时间”的差值。
  3. 将这个差值转换成天、时、分、秒,显示在页面上。 看起来天衣无缝,对吗?问题就藏在“每秒执行一次”这个动作里。

2. 误差的三大“元凶”

元凶一:JavaScript 的单线程与事件循环

JavaScript 是单线程语言。这意味着它一次只能做一件事。setInterval 和 setTimeout 指定的延迟时间,并不是精确的“等待 X 毫秒后执行”,而是“等待至少 X 毫秒后,将回调函数放入任务队列”。

什么时候执行呢?要等主线程上当前的任务都执行完了,才会从队列里取出这个回调来执行。

想象一下:

  • 你设定 setInterval(fn, 1000),希望每秒跑一次。
  • 第0秒,fn 执行了。
  • 第1秒,fn 被放入队列。但此时主线程正在处理一个复杂的动画计算,花了 200 毫秒。
  • 结果,fn 直到第1.2秒才真正开始执行。

这就产生了至少 200 毫秒的延迟。

元凶二:浏览器标签页休眠

为了节省电量,当用户切换到其他标签页或最小化浏览器时,当前页面的 setInterval 和 setTimeout 会被“限流”。它们的执行频率会大大降低,可能变成每秒一次,甚至更慢。

如果你的倒计时在后台运行了5分钟,再切回来,它可能直接显示“已结束”,或者时间跳了一大截。

元凶三:系统时间依赖

很多倒计时是这样计算剩余时间的:

剩余秒数 = Math.floor((目标时间戳 - Date.now()) / 1000);

这里有两个潜在问题:

  1.  Date.now() 的精度:它返回的是系统时间。如果用户手动修改了电脑时间,或者系统时间同步有微小偏差,倒计时就会出错。
  2.  计算时机:这个计算发生在回调函数执行的那一刻。如果回调函数本身被延迟了,那么用来计算的“当前时刻”也已经晚了。

如何减小误差?试试这些方案

知道了原因,我们就可以对症下药。解决方案的目标是:让显示的时间尽可能接近真实的世界时间

方案一:优化计时器逻辑

这是最基础的改进,核心思想是:不依赖计时器的周期,而是依赖绝对时间

具体做法:

  1. 在倒计时启动时,记录一个精确的开始时间戳startTime = Date.now())和目标结束时间戳endTime)。

  2. 在每次更新函数中,不再简单地“减1秒”,而是重新计算:

    const now = Date.now();
    const elapsed = now - startTime; // 已经过去的时间
    const remainingTime = endTime - now; // 剩余时间
    const displaySeconds = Math.floor(remainingTime / 1000);
    
  3. 动态调整下一次执行的时间。例如,我们希望每 1000 毫秒更新一次显示,但上次执行晚了 50 毫秒,那么下次就只延迟 950 毫秒。

    function updateTimer() {
      // ... 计算并显示时间
      const deviation = Date.now() - (startTime + expectedElapsed); // 计算偏差
      const nextTick = 1000 - deviation; // 调整下次间隔
      setTimeout(updateTimer, Math.max(0, nextTick)); // 确保间隔不为负数
    }
    

优点:

• 实现相对简单。 • 能有效抵消单次延迟的累积。一次慢了,下次会找补回来一些。

缺点:

• 无法解决浏览器标签页休眠导致的长时间停滞。 • 仍然依赖 Date.now(),受系统时间影响。

方案二:使用 Web Worker(隔离线程)

既然主线程繁忙会导致延迟,那我们就把计时任务放到一个独立的线程里去。

Web Worker 可以让脚本在后台线程运行。在这个线程里运行的 setInterval 不容易被主线程的繁重任务阻塞。

实现思路:

  1. 创建一个 Web Worker 文件(timer.worker.js),在里面用 setInterval 向主线程发送消息。
  2. 主线程接收消息,更新界面。

优点:

• 计时更稳定,受主线程影响小。 • 代码分离,逻辑清晰。

缺点:

• 仍然无法解决浏览器标签页休眠限流的问题。 • 增加了一定的架构复杂度。

方案三:终极方案:服务器时间同步 + 前端补偿

这是目前最精确、最可靠的方案。核心原则是:前端不再信任本地时间,而是以服务器时间为准,并持续校准。

步骤拆解:

第一步:获取权威的服务器时间
在页面加载或倒计时开始时,向服务器发送一个请求。服务器在响应中返回当前的服务器时间戳

注意:这个时间戳应该放在 HTTP 响应的 Date 头或 body 里,避免受到网络传输时间的影响。更专业的做法是,计算一个往返延迟(RTT),然后估算出当前的准确服务器时间。

第二步:在前端建立一个“虚拟的服务器时钟”
我们不在前端直接使用 Date.now(),而是自己维护一个时钟:

// 假设通过 API 得到:serverTime 是服务器当前时间,rtt 是网络往返延迟
const initialServerTime = serverTime + rtt / 2; // 估算的准确服务器时间
const localTimeAtThatMoment = Date.now();

// 此后,要获取“当前服务器时间”,就用这个公式:
function getCurrentServerTime() {
  const nowLocal = Date.now();
  const elapsedLocal = nowLocal - localTimeAtThatMoment;
  return initialServerTime + elapsedLocal;
}

这个时钟的原理是:服务器告诉我们一个“起点时间”,我们记录下那个时刻的本地时间。之后,我们相信本地时间的流逝速度是基本准确的(电脑的晶体振荡器很稳定),用本地流逝的时间加上服务器的起点时间,就得到了连续的“服务器时间”。

第三步:用这个虚拟时钟驱动倒计时
倒计时的更新函数,使用 getCurrentServerTime() 来计算剩余时间,而不是 Date.now()

第四步:定期校准
本地时钟的流逝速度可能有微小偏差(时钟漂移)。我们可以设置一个间隔(比如每1分钟或5分钟),悄悄地再向服务器请求一次时间,来修正我们的 initialServerTime 和 localTimeAtThatMoment,让虚拟时钟始终与服务器保持同步。

这个方案的优点非常突出:

• 抗干扰:用户修改本地时间,完全不影响倒计时。 • 高精度:误差主要来自时钟漂移和网络延迟,通过定期校准可以控制在极低水平(百毫秒内)。 • 一致性:所有用户看到的倒计时基于同一时间源,公平公正。

当然,它的实现也最复杂,需要前后端配合。

实战建议:如何选择?

面对不同的场景,你可以这样选择:

• 对精度要求不高的展示型倒计时(如文章发布后的阅读时间):使用方案一(优化计时器逻辑)  就足够了。简单有效。 • 营销活动、秒杀抢购倒计时:必须使用方案三(服务器时间同步) 。这是保证公平性和准确性的底线。方案一和方案二可以作为辅助,让更新更平滑。

❌
❌