使用 Three.js GPGPU 和着色器进行 RGB 偏移的网格置换纹理

devtools/2024/9/25 15:18:45/

更多精彩内容尽在 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 的通用实例化代码步骤:

  1. createGPGPURenderer 方法中创建实例。
  2. createDataTexture 方法中创建一个 DataTexture 对象,用来填充计算着色器的结果。
  3. createVariable 方法中创建一个“变量”。GPUComputationRenderer 会使用这个变量来指代我们要输出的纹理,这个纹理会根据我们的计算而在每一帧发生变化。
  4. 设置 GPGPU 的依赖关系。
  5. 初始化我们的实例。

现在我们来创建 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;

最终的效果:


http://www.ppmy.cn/devtools/117024.html

相关文章

BOE(京东方)重磅亮相世界制造业大会 科技创新引领现代化产业体系建设新未来

9月20日-23日,备受瞩目的2024世界制造业大会在合肥盛大召开,汇聚全球行业领袖、专家学者、知名企业,共同探讨现代化产业体系建设的新技术、新趋势、新机遇。作为积极推动实体经济与数字经济融合发展的产业领军企业,BOE&#xff08…

AI学习指南深度学习篇-Adadelta的Python实践

AI学习指南深度学习篇-Adadelta的Python实践 深度学习是人工智能领域的一个重要分支,近年来在各个领域都取得了显著的成就。在深度学习的模型训练中,优化算法起着至关重要的作用,其中Adadelta是一种常用的优化算法之一。本篇博客将使用Pytho…

828华为云征文|Flexus云服务器X实例实践:安装Ward服务器监控工具

828华为云征文|Flexus云服务器X实例实践:安装Ward服务器监控工具 引言一、Flexus云服务器X实例介绍1.1 Flexus云服务器X实例简介1.2 主要使用场景 二、购买Flexus云服务器X实例2.1 购买规格参考2.2 查看Flexus云服务器X实例状态 三、远程连接Flexus云服务…

获取douyin商品详情:API接口的力量

什么是DouYin商品详情API? douyin商品详情API是douyin开放平台提供的一项服务,允许开发者通过编程方式获取douyin商品的详细信息。这些信息通常包括商品的标题、价格、销量、描述、图片等。 API返回值说明 商品详情API返回的数据通常包括以下字段&…

【网页设计】前言

本专栏主要记录 “网页设计” 这一课程的相关笔记。 参考资料: 黑马程序员:黑马程序员pink老师前端入门教程,零基础必看的h5(html5)css3移动端前端视频教程_哔哩哔哩_bilibili 教材:《Adobe创意大学 Dreamweaver CS6标准教材》《…

二次记录服务器被(logrotate)木马入侵事件

现象:SSH失败、CPU满转 服务器ssh登录不上,一直处于登录中状态。 于是进入云服务器控制台,CPU打满状态,知道服务器被攻击了 腾讯云入侵检测,高危命令报警 排查过程 尝试 VNC 登录 由于SSH登录不上,进入云…

【日记】感觉自己已经魔怔了(817 字)

正文 下午装档案的时候,无意间朝外看了一眼,发现自己视力衰退了好多。感觉两只眼睛都有散光了,看东西有重影。有些担心。 兄长血检报告出来了,血红蛋白高,肌酐低。尿酸倒是正常了,但总体还是偏高。我觉得好…

鸿蒙OpenHarmony【小型系统基础内核(进程管理任务)】子系统开发

任务 基本概念 从系统的角度看,任务Task是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。 OpenHarmony 内核中使用一个任务表示一个线程。 OpenHarmony 内核中同优先级进程内的任务统一调度、运…