You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebGL如何永久保留鼠标交互产生的纹理扭曲效果

WebGL 永久保留纹理扭曲效果实现方案

现有代码无法保留扭曲状态的核心原因:

  • 片段着色器始终采样初始原图,鼠标触发的扭曲仅在单帧生效,变形结果没有持久化存储
  • FBO绑定、纹理读写逻辑错误,没有形成「变形结果作为下一次变形输入」的循环,copyTexImage2D的调用时机和操作目标均不满足需求

核心实现逻辑

要让扭曲效果永久叠加不回弹,采用双纹理乒乓渲染方案即可,流程如下:

  • 初始化两张同分辨率纹理:第一张加载初始原图作为初始采样源,第二张作为空的渲染目标
  • 每次鼠标触发扭曲时:
    1. 绑定FBO离屏帧缓冲,将空纹理挂载为颜色附件
    2. 着色器采样上一帧存了变形结果的纹理,叠加当前鼠标位置的扭曲计算,将结果渲染到离屏缓冲绑定的纹理上
    3. 解绑FBO回到默认屏幕缓冲,把刚渲染好的变形纹理绘制到画布上
    4. 交换两张纹理的身份:本次存了新变形结果的纹理作为下一次绘制的采样源,原来的采样源纹理清空作为下一次的渲染目标
  • 循环上述流程,所有扭曲效果会逐次叠加,永远不会回弹到初始状态

具体代码修改点

1. 片元着色器修正

原扭曲逻辑的坐标计算存在偏差,调整为笔刷范围内做坐标偏移,范围外直接采样原纹理对应位置,固定偏移量可以后续改成鼠标移动增量实现拖拽扭曲效果:

precision mediump float;

uniform sampler2D u_image;
uniform vec2 u_mouse;
uniform float u_brushRadius;
varying vec2 v_texCoord;

void main() {
  vec2 sampleCoord = v_texCoord;
  float dist = distance(sampleCoord, u_mouse);
  if (dist < u_brushRadius) {
    float strength = 1.0 - smoothstep(0.0, u_brushRadius, dist);
    // 偏移量可根据效果调整,替换为鼠标移动向量即可实现跟手拖拽扭曲
    vec2 offset = vec2(0.035, 0.035) * strength;
    sampleCoord -= offset;
  }
  gl_FragColor = texture2D(u_image, sampleCoord);
}

2. JS端资源初始化与渲染逻辑调整

替换原有单纹理、错误FBO绑定的逻辑,实现双纹理乒乓交换:

// 全局维护双纹理、FBO、纹理尺寸
let texRead, texWrite, renderFbo;
const texConfig = {
  width: image.width,
  height: image.height
};

// 纹理创建工具方法
function createTexture(gl, width, height, initialData = null) {
  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  // 必须设置边缘截断,避免采样黑边
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, initialData);
  return tex;
}

// 在原render函数初始化完顶点、纹理坐标缓冲后,替换原有纹理/FBO创建逻辑
// 读纹理初始加载原图
texRead = createTexture(gl, texConfig.width, texConfig.height, image);
// 写纹理初始为空
texWrite = createTexture(gl, texConfig.width, texConfig.height);
// 创建离屏FBO
renderFbo = gl.createFramebuffer();
// 获取uniform位置
const brushRadiusLoc = gl.getUniformLocation(prog, "u_brushRadius");
const imageSamplerLoc = gl.getUniformLocation(prog, "u_image");

// 初始绘制一次原图到屏幕
gl.bindTexture(gl.TEXTURE_2D, texRead);
gl.drawArrays(gl.TRIANGLES, 0, 6);

// 替换原有mousemove事件逻辑
document.getElementById('glCanvas').addEventListener('mousemove', function(e) {
  const canvasRect = e.target.getBoundingClientRect();
  // 坐标转纹理坐标,注意y轴翻转
  const mouseX = (e.clientX - canvasRect.left) / canvasRect.width;
  const mouseY = 1.0 - (e.clientY - canvasRect.top) / canvasRect.height;
  gl.uniform2f(mouseLocation, mouseX, mouseY);
  gl.uniform1f(brushRadiusLoc, 0.1);

  // 第一步:离屏渲染,将叠加扭曲后的结果写入写纹理
  gl.bindFramebuffer(gl.FRAMEBUFFER, renderFbo);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texWrite, 0);
  // 切换视口匹配纹理尺寸
  gl.viewport(0, 0, texConfig.width, texConfig.height);
  // 绑定读纹理为采样源
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texRead);
  gl.uniform1i(imageSamplerLoc, 0);
  gl.drawArrays(gl.TRIANGLES, 0, 6);

  // 第二步:渲染变形结果到屏幕
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.bindTexture(gl.TEXTURE_2D, texWrite);
  gl.drawArrays(gl.TRIANGLES, 0, 6);

  // 第三步:交换读写纹理,下次绘制基于本次变形结果继续叠加
  [texRead, texWrite] = [texWrite, texRead];
});

避坑提示

  • 不要将初始原图纹理直接绑定到FBO做写入操作,会覆盖原始像素数据
  • 每次切换帧缓冲(FBO/默认屏幕缓冲)后必须重新调用gl.viewport设置对应渲染目标的尺寸,否则会出现绘制拉伸、裁切问题
  • 如果需要实现跟手拖拽的扭曲效果,新增变量记录上一帧鼠标坐标,将片元着色器里的固定偏移量替换为当前帧和上一帧鼠标的坐标差即可
  • 不需要使用copyTexImage2D,直接通过FBO把渲染结果写入纹理的性能远高于拷贝操作

内容的提问来源于stack exchange,提问作者Zac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:09:39