【问题标题】:WebGL – Use mesh as mask for background imageWebGL – 使用网格作为背景图像的遮罩
【发布时间】:2018-09-06 10:12:03
【问题描述】:

我有以下问题要使用 WebGL 解决:

想象一下镜头前的网格。网格实际上并没有着色,但它的“剪影”作为一个整体用于显示背景图像作为某种蒙版或模板,如果你愿意的话。所以例如您将有一个具有白色背景的输出图像和一个填充有纹理/图像的轮廓形状作为该位置的背景。

(我要使用的网格显然比球体复杂)

达到这种效果的最佳方法是什么?我考虑过任何一种投影映射,即从相机的角度将背景纹理投影到网格上。或者也许是一个模板缓冲区要走的路?从我所读到的,目前对它的支持并不是那么高。也许还有一种更简单的方法可以解决我错过的这个问题?

【问题讨论】:

  • 我知道零个不支持模板缓冲区的 WebGL 实现。你必须要求它。 var gl = someCanvas.getContext("webgl", {stencil: true});
  • Stencil 是核心规范的一部分,如果在绘图缓冲区中不可用(正如 gman 所提到的,这是一种罕见到不存在的情况),您可以创建一个带有 DEPTH_STENCIL 附件的帧缓冲区。跨度>

标签: html background webgl mask mesh


【解决方案1】:

有很多方法可以做到这一点,最适合你的取决于你。

使用模板缓冲区

将您的网格绘制到模板缓冲区中,然后使用模板测试集绘制您的图像,以便它仅在绘制网格的位置绘制

var geoVS = `
attribute vec4 position;
uniform mat4 matrix;

void main() {
  gl_Position = matrix * position;
}
`;
var geoFS = `
precision mediump float;
void main() {
  gl_FragColor = vec4(1, 0, 0, 1);  // doesn't matter. We're only using the stencil  
}
`;
var imgVS = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = position.xy * .5 + .5;  // only works if position is -1 <-> +1 quad
}
`;
var imgFS = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, v_texcoord);  
}
`;

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {stencil: true});
const geoPrgInfo = twgl.createProgramInfo(gl, [geoVS, geoFS]);
const imgPrgInfo = twgl.createProgramInfo(gl, [imgVS, imgFS]);

const geoBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

const tex = twgl.createTexture(gl, {
  src: "https://farm9.staticflickr.com/8873/18598400202_3af67ef38f_z_d.jpg",
  crossOrigin: "",
  flipY: true,
});

function render(time) {
  time *= 0.001;
  
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
  
  var fov = Math.PI * .25;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.1;
  var zFar = 10;
  var mat = m4.perspective(fov, aspect, zNear, zFar);
  mat = m4.translate(mat, [0, 0, -3]);
  mat = m4.rotateX(mat, time * 0.81);
  mat = m4.rotateZ(mat, time * 0.77);
  
  // draw geometry to generate stencil
  gl.useProgram(geoPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, geoPrgInfo, geoBufferInfo);
  twgl.setUniforms(geoPrgInfo, {
    matrix: mat,
  });

  // write 1 to stencil
  gl.enable(gl.STENCIL_TEST);
  gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
  gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
  gl.drawElements(gl.TRIANGLES, geoBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
  
  // draw image where stencil is set
  gl.useProgram(imgPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, imgPrgInfo, quadBufferInfo);
  twgl.setUniforms(imgPrgInfo, {
    tex: tex,
  });
  
  gl.stencilFunc(gl.EQUAL, 1, 0xFF);
  gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
  gl.drawElements(gl.TRIANGLES, quadBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
   
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>

使用深度缓冲区

将您的网格绘制到深度缓冲区中,然后使用设置的深度函数绘制您的图像,以便它仅在绘制网格的位置绘制。

var geoVS = `
attribute vec4 position;
uniform mat4 matrix;

void main() {
  gl_Position = matrix * position;
}
`;
var geoFS = `
precision mediump float;
void main() {
  gl_FragColor = vec4(1, 0, 0, 1);  // doesn't matter. We're only using the stencil  
}
`;
var imgVS = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = position.xy * .5 + .5;  // only works if position is -1 <-> +1 quad
}
`;
var imgFS = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, v_texcoord);  
}
`;

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {stencil: true});
const geoPrgInfo = twgl.createProgramInfo(gl, [geoVS, geoFS]);
const imgPrgInfo = twgl.createProgramInfo(gl, [imgVS, imgFS]);

const geoBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

const tex = twgl.createTexture(gl, {
  src: "https://farm9.staticflickr.com/8873/18598400202_3af67ef38f_z_d.jpg",
  crossOrigin: "",
  flipY: true,
});

function render(time) {
  time *= 0.001;
  
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.clearDepth(0);  // clear depth to 0 (normally it's 1)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  
  var fov = Math.PI * .25;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.1;
  var zFar = 10;
  var mat = m4.perspective(fov, aspect, zNear, zFar);
  mat = m4.translate(mat, [0, 0, -3]);
  mat = m4.rotateX(mat, time * 0.81);
  mat = m4.rotateZ(mat, time * 0.77);
  
  // draw geometry to generate depth
  gl.useProgram(geoPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, geoPrgInfo, geoBufferInfo);
  twgl.setUniforms(geoPrgInfo, {
    matrix: mat,
  });

  gl.depthFunc(gl.ALWAYS);  // we only care about silhouette
  gl.drawElements(gl.TRIANGLES, geoBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
  
  // draw image where depth is set
  gl.useProgram(imgPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, imgPrgInfo, quadBufferInfo);
  twgl.setUniforms(imgPrgInfo, {
    tex: tex,
  });

  gl.depthFunc(gl.LESS);
  // this quad is drawn at z = 0 which is in the middle Z wize. Should probably
  // make it 1 so it's in the back but it's working as is so too lazy to
  // change
  gl.drawElements(gl.TRIANGLES, quadBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
   
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>

使用 CSS

将画布的 CSS 背景设置为图像。将画布清除为某种颜色,用 0,0,0,0 绘制网格以切出一个洞。

var geoVS = `
attribute vec4 position;
uniform mat4 matrix;

void main() {
  gl_Position = matrix * position;
}
`;
var geoFS = `
precision mediump float;
void main() {
  gl_FragColor = vec4(0);   
}
`;

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {stencil: true});
const geoPrgInfo = twgl.createProgramInfo(gl, [geoVS, geoFS]);

const geoBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);

function render(time) {
  time *= 0.001;
  
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.clearColor(1, 1, 1, 1); // clear to white
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  var fov = Math.PI * .25;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.1;
  var zFar = 10;
  var mat = m4.perspective(fov, aspect, zNear, zFar);
  mat = m4.translate(mat, [0, 0, -3]);
  mat = m4.rotateX(mat, time * 0.81);
  mat = m4.rotateZ(mat, time * 0.77);
  
  // draw in 0,0,0,0 to cut a whole in the canvas to the HTML/CSS
  // defined background
  gl.useProgram(geoPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, geoPrgInfo, geoBufferInfo);
  twgl.setUniforms(geoPrgInfo, {
    matrix: mat,
  });

  gl.drawElements(gl.TRIANGLES, geoBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
     
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; 
  background-image: url(https://farm9.staticflickr.com/8873/18598400202_3af67ef38f_z_d.jpg);
  background-size: 100% 100%;
}
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>

生成纹理蒙版

通过帧缓冲区将网格绘制到纹理中,以在纹理中生成轮廓。将该纹理用作另一个着色器的输入作为遮罩

var geoVS = `
attribute vec4 position;
uniform mat4 matrix;

void main() {
  gl_Position = matrix * position;
}
`;
var geoFS = `
precision mediump float;
void main() {
  gl_FragColor = vec4(1); 
}
`;
var imgVS = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = position.xy * .5 + .5;  // only works if position is -1 <-> +1 quad
}
`;
var imgFS = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D colorTex;
uniform sampler2D maskTex;
void main() {
  vec4 color = texture2D(colorTex, v_texcoord);
  vec4 mask = texture2D(maskTex, v_texcoord);
  gl_FragColor = color * mask;  
}
`;

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl", {stencil: true});
const geoPrgInfo = twgl.createProgramInfo(gl, [geoVS, geoFS]);
const imgPrgInfo = twgl.createProgramInfo(gl, [imgVS, imgFS]);

const geoBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

const tex = twgl.createTexture(gl, {
  src: "https://farm9.staticflickr.com/8873/18598400202_3af67ef38f_z_d.jpg",
  crossOrigin: "",
  flipY: true,
});

// with no options creates a framebuffer with an RGBA8 texture
// and depth buffer
const fbi = twgl.createFramebufferInfo(gl);

function render(time) {
  time *= 0.001;
  
  if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
    // with no argument will resize to the canvas size
    twgl.resizeFramebufferInfo(gl, fbi);
  }

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, fbi);
  
  // first draw the geometry to the texture
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  var fov = Math.PI * .25;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.1;
  var zFar = 10;
  var mat = m4.perspective(fov, aspect, zNear, zFar);
  mat = m4.translate(mat, [0, 0, -3]);
  mat = m4.rotateX(mat, time * 0.81);
  mat = m4.rotateZ(mat, time * 0.77);
  
  gl.useProgram(geoPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, geoPrgInfo, geoBufferInfo);
  twgl.setUniforms(geoPrgInfo, {
    matrix: mat,
  });

  gl.drawElements(gl.TRIANGLES, geoBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
  // the texture now is black (0,0,0,0) where there's nothing and (1,1,1,1)
  // where are geometry was drawn

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, null);
  
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    
  // draw image using our texture as a mask
  gl.useProgram(imgPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, imgPrgInfo, quadBufferInfo);
  twgl.setUniforms(imgPrgInfo, {
    colorTex: tex,
    maskTex: fbi.attachments[0],
  });
  
  gl.drawElements(gl.TRIANGLES, quadBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
   
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>

我个人可能会使用最后一个,因为它更灵活。您可以使用任何技术来生成遮罩。蒙版将具有级别(您可以将其设置为 0.5 以获得 50/50 的混合)。这意味着您可以根据需要对每个进行抗锯齿处理。您可以屏蔽每种颜色的单独数量。您可以轻松地在 2 张图像之间进行混合,等等。您可以在最终通道中添加其他效果,例如 displacement maps 等。

这是一个以灰色阴影渲染立方体并使用结果混合 2 个图像的示例。

var geoVS = `
attribute vec4 position;
attribute vec3 normal;
uniform mat4 matrix;
varying vec3 v_normal;

void main() {
  gl_Position = matrix * position;
  v_normal = (matrix * vec4(normal, 0)).xyz;
}
`;
var geoFS = `
precision mediump float;
uniform vec3 u_lightDir;
varying vec3 v_normal;
void main() {
  gl_FragColor = vec4(dot(normalize(v_normal), u_lightDir) * .5 + .5); 
}
`;
var imgVS = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = position.xy * .5 + .5;  // only works if position is -1 <-> +1 quad
}
`;
var imgFS = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D color1Tex;
uniform sampler2D color2Tex;
uniform sampler2D maskTex;
void main() {
  // it probably doesn't make sense to use the same
  // texcoords for all 3 textures but I'm lazy
  vec4 color1 = texture2D(color1Tex, v_texcoord);
  vec4 color2 = texture2D(color2Tex, v_texcoord);
  vec4 mask = texture2D(maskTex, v_texcoord);
  gl_FragColor = mix(color1, color2, mask);  
}
`;

const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.querySelector("canvas").getContext("webgl", {stencil: true});
const geoPrgInfo = twgl.createProgramInfo(gl, [geoVS, geoFS]);
const imgPrgInfo = twgl.createProgramInfo(gl, [imgVS, imgFS]);

const geoBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);
const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

const textures = twgl.createTextures(gl, {
  tex1: {
    src: "https://farm9.staticflickr.com/8873/18598400202_3af67ef38f_z_d.jpg",
    crossOrigin: "",
    flipY: true,
  },
  tex2: {
    src: "https://farm1.staticflickr.com/339/18414821420_e3d0a8ec5f_z_d.jpg",
    crossOrigin: "",
    flipY: true,
  },
});

// with no options creates a framebuffer with an RGBA8 texture
// and depth buffer
const fbi = twgl.createFramebufferInfo(gl);

function render(time) {
  time *= 0.001;
  
  if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
    // with no argument will resize to the canvas size
    twgl.resizeFramebufferInfo(gl, fbi);
  }

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, fbi);
  
  // first draw the geometry to the texture
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  
  var fov = Math.PI * .25;
  var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  var zNear = 0.1;
  var zFar = 10;
  var mat = m4.perspective(fov, aspect, zNear, zFar);
  mat = m4.translate(mat, [0, 0, -3]);
  mat = m4.rotateX(mat, time * 0.81);
  mat = m4.rotateZ(mat, time * 0.77);
  
  gl.useProgram(geoPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, geoPrgInfo, geoBufferInfo);
  twgl.setUniforms(geoPrgInfo, {
    matrix: mat,
    u_lightDir: v3.normalize([1, 2, 3]),
  });

  gl.drawElements(gl.TRIANGLES, geoBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
  // the texture now is black (0,0,0,0) where there's nothing and (1,1,1,1)
  // where are geometry was drawn

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, null);
  
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    
  // draw image using our texture as a mask
  gl.useProgram(imgPrgInfo.program);
  
  twgl.setBuffersAndAttributes(gl, imgPrgInfo, quadBufferInfo);
  twgl.setUniforms(imgPrgInfo, {
    color1Tex: textures.tex1,
    color2Tex: textures.tex2,
    maskTex: fbi.attachments[0],
  });
  
  gl.drawElements(gl.TRIANGLES, quadBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
   
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas>

一般来说,您可以使用纹理蒙版实现更多效果,但这实际上取决于您的目标。

【讨论】:

  • 嗨@gman,感谢您的回答。它让我朝着正确的方向走得更远,但你所描述的会与我试图实现的目标相反(或者我可能会误解)。我希望背景纹理只绘制网格所在的位置。我做了一点解释图片here
  • 试一试,你会发现这正是你想要的。
  • 我建议的每一种方法都将完全按照您的图像描述
【解决方案2】:

两种方法都可以。

“投影”可能更有效、更直接。它只需一次通过即可。您只需在顶点着色器中将经典的 UV 坐标替换为顶点的屏幕坐标即可。

varying vec2 vTexCoord;

void main( void ){
  // whatever how gl_Position is compute
  gl_Position    = uMVP * vec4(aPosition, 1.0);

  // vTexCoord = aTexCoord;
  // replace the standard UVs by the vertex screen position
  vTexCoord = .5 * ( gl_Position.xy / gl_Position.w ) + .5;
}

您仍然需要调整这些纹理坐标以尊重屏幕/纹理纵横比、比例等。

【讨论】:

    猜你喜欢
    • 2012-09-04
    • 2021-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-29
    • 2016-01-07
    相关资源
    最近更新 更多