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

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

  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 类的完整代码:

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;

最终的效果:

猜你喜欢

转载自blog.csdn.net/u013929284/article/details/142144561