更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123
在本文中,我们将学习如何使用 Three.js 创建像素/网格位移效果,并通过着色器和 GPGPU 技术进行增强。并介绍了动态响应光标移动的 RGB 移位效果的应用。最后,我们将深入了解如何在 WebGL 中操作纹理和创建交互式视觉效果,从而使用 Three.js 扩展我们的创意能力。在线示例访问: http://dt.sim3d.cn -> 可视化效果/Web3D页面效果
基础设置
要创建此效果,我们需要两个纹理:第一个是我们要应用效果的图像,第二个是包含效果数据的纹理,效果如下:
首先,我们将使用 ShaderMaterial
创建一个基本的平面,然后添加到场景中,用来显示我们的图像。
createGeometry() {
this.geometry = new THREE.PlaneGeometry(1, 1)
}
createMaterial() {
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTexture: new THREE.Uniform(new THREE.Vector4()),
uContainerResolution: new THREE.Uniform(new THREE.Vector2(window.innerWidth, window.innerHeight)),
uImageResolution: new THREE.Uniform(new THREE.Vector2()),
},
})
}
setTexture() {
this.material.uniforms.uTexture.value = new THREE.TextureLoader().load(this.element.src, ({
image }) => {
const {
naturalWidth, naturalHeight } = image
this.material.uniforms.uImageResolution.value = new THREE.Vector2(naturalWidth, naturalHeight)
})
}
createMesh() {
this.mesh = new THREE.Mesh(this.geometry, this.material)
}
我们将视口尺寸传递给 uContainerResolution
变量,因为我们的网格占据了整个视口空间。如果你希望图像具有不同的尺寸,则需要传递包含图像的 HTML 元素的宽度和高度。
下面是顶点着色器代码,由于我们不打算修改顶点,因此该代码将保持不变。
varying vec2 vUv;
void main()
{
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vUv=uv;
}
接下来是初始的片段着色器:
uniform sampler2D uTexture;
varying vec2 vUv;
uniform vec2 uContainerResolution;
uniform vec2 uImageResolution;
vec2 coverUvs(vec2 imageRes,vec2 containerRes)
{
float imageAspectX = imageRes.x/imageRes.y;
float imageAspectY = imageRes.y/imageRes.x;
float containerAspectX = containerRes.x/containerRes.y;
float containerAspectY = containerRes.y/containerRes.x;
vec2 ratio = vec2(
min(containerAspectX / imageAspectX, 1.0),
min(containerAspectY / imageAspectY, 1.0)
);
vec2 newUvs = vec2(
vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
);
return newUvs;
}
void main()
{
vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);
vec4 image = texture2D(uTexture,newUvs);
gl_FragColor = image;
}
coverUvs
函数会返回一组 UV,使图像纹理包裹的行为类似于 CSS 里的 object-fit: cover;
属性。结果如下:
使用 GPGPU 实现位移
现在我们将在单独的着色器中实现位移纹理,这是因为基础的 Three.js 着色器无法实现我们的效果。
我们从最上面的位移纹理动图中可以看到,鼠标移动后会出现一条痕迹,当鼠标离开该区域时,该痕迹会慢慢淡出。我们无法在当前的着色器中创建这种效果,因为数据不是持久的,着色器使用其初始输入(uniform和varying)在每帧中运行,并且无法访问之前的状态。
幸运的是,Three.js 提供了一个名为 GPUComputationRenderer
的实用程序。它允许我们将计算的片段着色器输出为纹理,并使用该纹理作为下一帧中着色器的输入,这个纹理被称为缓冲区纹理。它的工作原理如下:
首先,我们将初始化 GPUComputationRenderer
实例。为此,我将创建一个名为 GPGPU 的类。
// 我们将在 gpgpu 中使用的片段着色器
import fragmentShader from '../shaders/gpgpu/gpgpu.glsl'
// ...类初始化
createGPGPURenderer() {
this.gpgpuRenderer = new GPUComputationRenderer(
this.size, //我们要创建的网格的大小,在示例中大小为 27
this.size,
this.renderer
)
}
createDataTexture() {
this.dataTexture = this.gpgpuRenderer.createTexture()
}
createVariable() {
this.variable = this.gpgpuRenderer.addVariable('uGrid', fragmentShader, this.dataTexture)
this.variable.material.uniforms.uGridSize = new THREE.Uniform(this.size)
this.variable.material.uniforms.uMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
this.variable.material.uniforms.uDeltaMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
}
setRendererDependencies() {
this.gpgpuRenderer.setVariableDependencies(this.variable, [this.variable])
}
initiateRenderer() {
this.gpgpuRenderer.init()
}
下面就是 GPUComputationRenderer
的通用实例化代码步骤:
- 在
createGPGPURenderer
方法中创建实例。 - 在
createDataTexture
方法中创建一个DataTexture
对象,用来填充计算着色器的结果。 - 在
createVariable
方法中创建一个“变量”。GPUComputationRenderer
会使用这个变量来指代我们要输出的纹理,这个纹理会根据我们的计算而在每一帧发生变化。 - 设置 GPGPU 的依赖关系。
- 初始化我们的实例。
现在我们来创建 GPGPU 使用的片段着色器。
void main()
{
vec2 uv = gl_FragCoord.xy/resolution.xy;
vec4 color = texture(uGrid,uv);
color.r = 1.;
gl_FragColor = color;
}
现在我们的 GPGPU 创建的纹理是纯红色图像。注意我们不必在着色器的顶部声明uniform sampler2D uGrid
,因为我们已经将其声明为 GPUComputationRenderer
实例的变量。
接下来我们要检索纹理并将其应用到我们的图像中。
下面是 GPGPU 类的完整代码:
constructor({
renderer, scene }: Props) {
this.scene = scene
this.renderer = renderer
this.params = {
size: 700,
}
this.size = Math.ceil(Math.sqrt(this.params.size))
this.time = 0
this.createGPGPURenderer()
this.createDataTexture()
this.createVariable()
this.setRendererDependencies()
this.initiateRenderer()
}
createGPGPURenderer() {
this.gpgpuRenderer = new GPUComputationRenderer(
this.size,
this.size,
this.renderer
)
}
createDataTexture() {
this.dataTexture = this.gpgpuRenderer.createTexture()
}
createVariable() {
this.variable = this.gpgpuRenderer.addVariable('uGrid', fragmentShader, this.dataTexture)
this.variable.material.uniforms.uGridSize = new THREE.Uniform(this.size)
this.variable.material.uniforms.uMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
this.variable.material.uniforms.uDeltaMouse = new THREE.Uniform(new THREE.Vector2(0, 0))
}
setRendererDependencies() {
this.gpgpuRenderer.setVariableDependencies(this.variable, [this.variable])
}
initiateRenderer() {
this.gpgpuRenderer.init()
}
getTexture() {
return this.gpgpuRenderer.getCurrentRenderTarget(this.variable).textures[0]
}
render() {
this.gpgpuRenderer.compute()
}
在我们的程序中每帧都会调用 render
方法,并通过 getTexture
方法返回我们计算的纹理。
在我们创建的第一个平面的材质中,我们将添加 uGrid
变量,它会包含 GPGPU 检索到的纹理。
createMaterial() {
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTexture: new THREE.Uniform(new THREE.Vector4()),
uContainerResolution: new THREE.Uniform(new THREE.Vector2(window.innerWidth, window.innerHeight)),
uImageResolution: new THREE.Uniform(new THREE.Vector2()),
//新加的uniform变量
uGrid: new THREE.Uniform(new THREE.Vector4()),
},
})
}
然后我们将在计算 GPGPU 纹理后在每一帧中更新这个uniform,
render() {
this.gpgpu.render()
this.material.uniforms.uGrid.value = this.gpgpu.getTexture()
}
现在让我们在第一个图像平面的片段着色器内显示该纹理。
uniform sampler2D uGrid;
void main()
{
vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);
vec4 image = texture2D(uTexture,newUvs);
vec4 displacement = texture2D(uGrid,newUvs);
gl_FragColor = displacement;
}
处理鼠标操作
现在我们开始研究位移效应。首先,我们需要跟踪鼠标移动并将其作为uniform传递给 GPGPU 着色器。
我们将创建一个 Raycaster
并将鼠标 UV 传递到 GPGPU。由于本例中场景中只有一个网格,因此它将返回的唯一 UV 是包含图像的平面的 UV。
createRayCaster() {
this.raycaster = new THREE.Raycaster()
this.mouse = new THREE.Vector2()
}
onMouseMove(event: MouseEvent) {
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
this.raycaster.setFromCamera(this.mouse, this.camera)
const intersects = this.raycaster.intersectObjects(this.scene.children)
const target = intersects[0]
if (target && 'material' in target.object) {
const targetMesh = intersects[0].object as THREE.Mesh
if(targetMesh && target.uv)
{
this.gpgpu.updateMouse(target.uv)
}
}
}
addEventListeners() {
window.addEventListener('mousemove', this.onMouseMove.bind(this))
}
在 GPGPU 的 createVariable
方法中,我们为其分配了一个 uMouse
的uniform,然后我们会在 GPGPU 类的 updateMouse
方法中更新此uniform,另外我们还将更新 uDeltaMouse
的uniform(我们很快就会需要它)。
updateMouse(uv: THREE.Vector2) {
const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2
current.subVectors(uv, current)
this.variable.material.uniforms.uDeltaMouse.value = current
this.variable.material.uniforms.uMouse.value = uv
}
在 GPGPU 片段着色器中,我们检索鼠标坐标来计算纹理的每个像素与鼠标之间的距离。然后根据该距离将鼠标增量应用到纹理。
uniform vec2 uMouse;
uniform vec2 uDeltaMouse;
void main()
{
vec2 uv = gl_FragCoord.xy/resolution.xy;
vec4 color = texture(uGrid,uv);
float dist = distance(uv,uMouse);
dist = 1.-(smoothstep(0.,0.22,dist));
color.rg+=uDeltaMouse*dist;
gl_FragColor = color;
}
我们会得到如下效果:
当我们将光标从左向右移动时,显示正在着色,而当从右向左移动光标时,显示正在擦除。这是因为从右向左移动时,UV 的增量为负,反之亦然。
然后我们将位移纹理应用到我们的初始图像中:
void main()
{
vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);
vec4 image = texture2D(uTexture,newUvs);
vec4 displacement = texture2D(uGrid,newUvs);
vec2 finalUvs = newUvs - displacement.rg*0.01;
vec4 finalImage = texture2D(uTexture,finalUvs);
gl_FragColor = finalImage;
}
我们会得到如下效果:
从效果上看,还是有些问题。首先第一个问题是位移的形状不是正方形。这是因为我们使用与图像相同的 UV 进行置换。为了解决这个问题,我们将使用 coverUvs
函数为位移提供自己的 UV。
void main()
{
vec2 newUvs = coverUvs(uImageResolution,uContainerResolution);
vec2 squareUvs = coverUvs(vec2(1.),uContainerResolution);
vec4 image = texture2D(uTexture,newUvs);
vec4 displacement = texture2D(uGrid,squareUvs);
vec2 finalUvs = newUvs - displacement.rg*0.01;
vec4 finalImage = texture2D(uTexture,finalUvs);
gl_FragColor = finalImage;
}
现在我们就有了一个方形的位移。然后我们当前纹理的最大问题是它没有淡出效果。为了解决这个问题,我们将颜色乘以一个小于 1 的值,可以使它逐渐趋向于 0。
//... gpgpu shader
color.rg+=uDeltaMouse*dist;
float uRelaxation = 0.965;
color.rg*=uRelaxation;
gl_FragColor = color;
然而对于靠近光标的像素,则需要更长的时间才能淡出。这是因为它们积累了更多的颜色,所以需要更长的时间才能达到 0。为了解决这个问题,我们将添加一个新的float型uniform:
this.variable.material.uniforms.uMouseMove = new THREE.Uniform(0)
updateMouse(uv: THREE.Vector2) {
this.variable.material.uniforms.uMouseMove.value = 1
const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2
current.subVectors(uv, current)
current.multiplyScalar(80)
this.variable.material.uniforms.uDeltaMouse.value = current
this.variable.material.uniforms.uMouse.value = uv
}
render() {
this.variable.material.uniforms.uMouseMove.value *= 0.95
this.variable.material.uniforms.uDeltaMouse.value.multiplyScalar(0.965)
this.gpgpuRenderer.compute()
}
我们就得到了我们想要的位移效果:
创建 RGB 偏移效果
剩下要做的就是 RGB 偏移效果,我们要做的是将位移应用于图像的每种颜色,但强度不同。这样,我们就会注意到颜色之间的变化。
在平面的片段着色器中,将此代码添加到 gl_FragColor = FinalImage;
之前:
/*
* rgb shift
*/
//每种颜色都有单独的 UV 集
vec2 redUvs = finalUvs;
vec2 blueUvs = finalUvs;
vec2 greenUvs = finalUvs;
//移动将遵循位移方向,但强度降低
//we need the effect to be subtle
vec2 shift = displacement.rg*0.001;
//移动强度取决于鼠标移动的速度,
//由于强度依赖于 deltaMouse,我们只需使用(红色,绿色)向量的强度
float displacementStrength=length(displacement.rg);
displacementStrength = clamp(displacementStrength,0.,2.);
//对每种颜色应用不同的强度
float redStrength = 1.+displacementStrength*0.25;
redUvs += shift*redStrength;
float blueStrength = 1.+displacementStrength*1.5;
blueUvs += shift*blueStrength;
float greenStrength = 1.+displacementStrength*2.;
greenUvs += shift*greenStrength;
float red = texture2D(uTexture,redUvs).r;
float blue = texture2D(uTexture,blueUvs).b;
float green = texture2D(uTexture,greenUvs).g;
//将位移效果应用于我们的图像
finalImage.r =red;
finalImage.g =green;
finalImage.b =blue;
gl_FragColor = finalImage;
最终的效果: