更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123
在本文中,我们将学习如何使用 Three.js 创建像素/网格位移效果,并通过着色器和 GPGPU 技术进行增强。并介绍了动态响应光标移动的 RGB 移位效果的应用。最后,我们将深入了解如何在 WebGL 中操作纹理和创建交互式视觉效果,从而使用 Three.js 扩展我们的创意能力。在线示例访问: http://dt.sim3d.cn -> 可视化效果/Web3D页面效果
基础设置
要创建此效果,我们需要两个纹理:第一个是我们要应用效果的图像,第二个是包含效果数据的纹理,效果如下:
首先,我们将使用 ShaderMaterial
创建一个基本的平面,然后添加到场景中,用来显示我们的图像。
javascript">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 } = imagethis.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 的类。
javascript">// 我们将在 gpgpu 中使用的片段着色器
import fragmentShader from '../shaders/gpgpu/gpgpu.glsl' // ...类初始化
createGPGPURenderer() {this.gpgpuRenderer = new GPUComputationRenderer(this.size, //我们要创建的网格的大小,在示例中大小为 27this.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 类的完整代码:
javascript">constructor({ renderer, scene }: Props) {this.scene = scenethis.renderer = rendererthis.params = {size: 700,}this.size = Math.ceil(Math.sqrt(this.params.size))this.time = 0this.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 检索到的纹理。
javascript">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,
javascript">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。
javascript">createRayCaster() {this.raycaster = new THREE.Raycaster()this.mouse = new THREE.Vector2()
}onMouseMove(event: MouseEvent) {this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1this.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.Meshif(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(我们很快就会需要它)。
javascript">updateMouse(uv: THREE.Vector2) {const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2current.subVectors(uv, current)this.variable.material.uniforms.uDeltaMouse.value = currentthis.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:
javascript">this.variable.material.uniforms.uMouseMove = new THREE.Uniform(0)updateMouse(uv: THREE.Vector2) {this.variable.material.uniforms.uMouseMove.value = 1const current = this.variable.material.uniforms.uMouse.value as THREE.Vector2current.subVectors(uv, current)current.multiplyScalar(80)this.variable.material.uniforms.uDeltaMouse.value = currentthis.variable.material.uniforms.uMouse.value = uv}render() {this.variable.material.uniforms.uMouseMove.value *= 0.95this.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;
最终的效果: