
本文详解如何在 three.js 中利用 shader 实现三张图像间的位移映射过渡效果,涵盖纹理采样、uv 插值控制、uniform 动态更新及像素级数据理解,帮助开发者摆脱“黑盒式”模板,真正掌握 webgl 图像混合原理。
本文详解如何在 three.js 中利用 shader 实现三张图像间的位移映射过渡效果,涵盖纹理采样、uv 插值控制、uniform 动态更新及像素级数据理解,帮助开发者摆脱“黑盒式”模板,真正掌握 webgl 图像混合原理。
在 Three.js 中实现多图之间的位移贴图(Displacement Map)过渡,核心并非直接操作像素内存,而是通过着色器(Shader)在 GPU 端对纹理进行采样、插值与条件混合。你当前代码中将三张图像作为 sampler2D 传入 Fragment Shader 是正确的起点,但关键缺失在于:缺乏过渡控制变量(如 uProgress)与基于位移逻辑的混合策略——而不仅仅是 img1 * img2 * img3 这种无意义的逐通道相乘。
✅ 正确的过渡逻辑:用 mix() 实现可控插值
WebGL GLSL 提供了内置函数 mix(a, b, t),它等价于 a * (1.0 - t) + b * t,是实现平滑过渡的基石。对于三图过渡,可采用两阶段插值:
// fragment.glsl
varying vec2 vUv;
uniform sampler2D uTexture1;
uniform sampler2D uTexture2;
uniform sampler2D uTexture3;
uniform float uProgress; // 控制过渡进度:0.0 → 1.0 → 2.0
void main() {
vec4 tex1 = texture2D(uTexture1, vUv);
vec4 tex2 = texture2D(uTexture2, vUv);
vec4 tex3 = texture2D(uTexture3, vUv);
vec4 finalColor;
if (uProgress < 1.0) {
// 第一阶段:tex1 → tex2(0.0 ~ 1.0)
finalColor = mix(tex1, tex2, uProgress);
} else {
// 第二阶段:tex2 → tex3(1.0 ~ 2.0,归一化为 0.0 ~ 1.0)
finalColor = mix(tex2, tex3, uProgress - 1.0);
}
gl_FragColor = finalColor;
}⚠️ 注意:
uProgress值域建议设为[0.0, 2.0],便于线性驱动三图切换;你可在 JS 中用requestAnimationFrame或THREE.Clock平滑更新它。
✅ JavaScript 层:正确绑定纹理与动态 uniform
你原代码中 uTexture = { value: new THREE.TextureLoader().load(texturesArr) } 存在严重错误:TextureLoader.load() 不接受数组,且该字段未在 Shader 中声明使用。应删除此行,并确保仅传递实际需要的三张纹理:
使用约定式提交(Conventional Commits)生成标准化的 Git 提交信息。适用于以下场景: (1) 暂存更改时需要提交信息; (2) 审查 diff 时希望获得结构化摘要; (3) 编写 PR 描述或变更日志(changelog)条目; (4) 用户明确要求“为本次更改编写提交信息”。 该功能会分析已暂存/未暂存的 diff,并生成按类型(type)划分范围的提交信息。
// ✅ 正确初始化材质
const planeMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTexture1: { value: imgTexture1 },
uTexture2: { value: imgTexture2 },
uTexture3: { value: imgTexture3 },
uProgress: { value: 0.0 }, // 关键:添加可控过渡变量
},
side: THREE.DoubleSide,
transparent: true, // 若图像含 Alpha,建议启用
});
// ✅ 动态更新过渡进度(例如每帧递增)
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
// 循环三图:0→1→2→0... 每 3 秒切一次
const progress = (elapsed % 6) / 3; // [0, 2)
planeMaterial.uniforms.uProgress.value = progress;
renderer.render(scene, camera);
}
animate();✅ 进阶:引入位移贴图实现「真实」Displacement 效果
若你追求的是几何形变式位移(如图像被“推起”或“撕裂”),需在顶点着色器中读取位移贴图的灰度值,并扰动顶点位置:
// vertex.glsl(增强版)
varying vec2 vUv;
uniform sampler2D uDisplacementMap;
uniform float uDisplacementScale;
void main() {
vUv = uv;
// 从位移贴图采样高度(假设是灰度图)
float displacement = texture2D(uDisplacementMap, vUv).r;
// 扰动顶点 Z 坐标(需确保 geometry 有足够顶点密度)
vec3 displacedPosition = position + normal * (displacement - 0.5) * uDisplacementScale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0);
}此时你需要额外加载一张灰度位移贴图(如噪声图、波纹图),并传入 uDisplacementMap uniform。注意:PlaneBufferGeometry 默认顶点数较少,建议用 new THREE.PlaneGeometry(3, 3, 64, 64) 提升细分精度。
? 调试技巧:获取像素坐标与颜色值
你提到想“获取像素位置和值”,这在着色器中天然由 vUv 和 texture2D() 完成。若需在 CPU 端调试,可通过 Raycaster 获取屏幕点击处对应的 UV,再用 canvas.getContext('2d') 读取原始图像像素(但不推荐用于实时渲染逻辑,因性能极低):
// 示例:鼠标悬停时打印当前 UV 对应的纹理颜色(仅用于调试)
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / sizes.width) * 2 - 1;
const y = -((e.clientY - rect.top) / sizes.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera({ x, y }, camera);
const intersects = raycaster.intersectObject(plane);
if (intersects.length > 0) {
const uv = intersects[0].uv;
console.log(`UV: (${uv.x.toFixed(3)}, ${uv.y.toFixed(3)})`);
// 注意:此处无法直接读 GPU 纹理,需提前用 ImageData 缓存原始图片
}
});✅ 总结:关键四步走
-
Shader 层:用
mix()替代硬编码乘法,引入uProgress控制插值权重; - JS 层:正确声明 uniform 并在动画循环中动态更新其值;
-
纹理管理:确保三张图像已通过
TextureLoader加载完成(检查texture.needsUpdate = true及加载状态); - 调试思维:理解 WebGL 是“声明式渲染”——所有像素计算发生在 GPU,CPU 仅负责配置与驱动,避免尝试同步读取 GPU 纹理。
掌握这套模式后,你不仅能实现三图位移过渡,还可轻松扩展至 N 图轮播、鼠标驱动变形、时间函数控制的非线性过渡等高级效果。真正的 WebGL 能力,始于对 uniform、varying 和 texture2D 之间数据流的清晰掌控。

















