WebGL 在像素级别混合颜色。

3
我想编写一个小程序,计算例如素数。我的想法是使用webgl / threejs将代表素数(基于坐标)的所有像素颜色设置为白色,其余部分为黑色。我的问题是webgl在像素级别上会混合颜色(如果您在黑色画布中间有一个白色像素,则会与周围环境和该白色像素周围的像素混合)。由于这个问题,我无法正确地识别白色和黑色像素。
我的问题是:如何告诉webgl或canvas每个像素使用正确的颜色而不混合颜色?
这是一个演示,其中我将每个第二个像素涂成白色:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ProjectLaGPU</title>
</head>
<body>
    <script id="vertex-shader" type="x-shader/x-vertex">
        uniform float u_start;

        varying float v_start;

        void main() {
            v_start = u_start;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    </script>
    <script id="fragment-shader" type="x-shader/x-fragment">
        varying float v_start;

        void main () {
            if (int(gl_FragCoord.x)%2==0 && int(gl_FragCoord.y)%2==0) {
                gl_FragColor = vec4(1, 1, 1, 1);
            } else {
                gl_FragColor = vec4(0, 0, 0, 1);
            }
        }
    </script>
    <script src="https://threejs.org/build/three.js"></script>
    <script src="main.js"></script>
</body>
</html>

/* main.js */
async function init() {
    const scene = new THREE.Scene();
    const camera = new THREE.OrthographicCamera(-.5, .5, .5, -.5, -1, 1);
    const renderer = new THREE.WebGLRenderer({
        preserveDrawingBuffer: true,
        powerPreference: "high-performance",
        depth: false,
        stencil: false
    });

    const geometry = new THREE.PlaneGeometry();
    const material = new THREE.ShaderMaterial({
        uniforms: {
            u_start: { value: 0.5 },
        },
        vertexShader: document.getElementById("vertex-shader").innerText,
        fragmentShader: document.getElementById("fragment-shader").innerText,
      });
    const mesh = new THREE.Mesh(geometry, material);
    renderer.setSize(1000, 1000);
    document.body.appendChild(renderer.domElement);
    scene.add(mesh)
    renderer.render(scene, camera);
}

init();
2个回答

4
之前接受的答案是不正确的,只是侥幸地起作用。换句话说,如果它对你起作用了,那么你很幸运,因为你的机器/浏览器/操作系统/当前设置刚好能够工作。更改设置、切换到其他浏览器/操作系统/机器,它将无法工作。
这里有许多问题混合在一起。
1. 获取非模糊画布
要获取非模糊画布,需要做两件事。
关闭浏览器过滤功能
在 Chrome/Edge/Safari 中,您可以使用canvas { image-rendering: pixelated; },在 Firefox 中则使用canvas { image-rendering: crisp-edges; }
请注意,与crisp-edges相比,pixelated更适合此情况。根据规范,给定这个原始图像。

original-image

将其显示为默认算法的3倍大小取决于浏览器,但典型示例可能看起来像这样

enter image description here

pixelated会看起来像这样

pixelated-style

重要提示:请查看底部更新 crisp-edges 看起来会像这样

crisp-edges-style

需要注意的是,用于绘制图像的实际算法并未由规范定义,因此当前Firefox使用最近邻过滤器作为crisp-edges而不支持pixelated。因此,如果存在crisp-edges,则应该将pixelated指定为最后一次覆盖。

画布有两个大小。它的绘图缓冲区大小(画布中的像素数)和它的显示大小(由CSS定义)。但CSS大小是用CSS像素计算的,然后由当前devicePixelRatio进行缩放。这意味着,如果您没有缩放,并且在设备上具有不为1.0的devicePixelRatio,并且您的绘图缓冲区大小与显示大小相匹配,则画布将被缩放以匹配devicePixelRatio。因此,image-rendering CSS样式将用于决定如何进行缩放。

示例

const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-.5, .5, .5, -.5, -1, 1);
const renderer = new THREE.WebGLRenderer();

const geometry = new THREE.PlaneBufferGeometry();
const material = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertex-shader").innerText,
  fragmentShader: document.getElementById("fragment-shader").innerText,
});
const mesh = new THREE.Mesh(geometry, material);
document.body.appendChild(renderer.domElement);
scene.add(mesh)
renderer.render(scene, camera);
canvas { 
  image-rendering: crisp-edges;
  image-rendering: pixelated;  /* pixelated is more correct so put that last */
}
<script src="https://cdn.jsdelivr.net/npm/three@0.123/build/three.js"></script>

<script id="vertex-shader" type="x-shader/x-vertex">
        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
        void main () {
            if (int(gl_FragCoord.x)%2==0 && int(gl_FragCoord.y)%2==0) {
                gl_FragColor = vec4(1, 1, 1, 1);
            } else {
                gl_FragColor = vec4(0, 0, 0, 1);
            }
        }
</script>

关闭抗锯齿。

Three.js会为您完成此操作。

2. 使画布上的1像素等于屏幕上的1像素

这就是调用setPixelRatio所试图做的事情。你之前运行代码成功只是侥幸。换句话说,当你在不同的设备或浏览器上尝试运行代码时,它可能无法正常工作。

问题在于,为了设置画布绘制缓冲区的大小,使其与将用于显示画布的像素数量匹配,必须询问浏览器“你使用了多少像素”。

原因是,例如,假设您有一个dpr(设备像素比)为3的设备。假设窗口宽度为100个设备像素。那就是33.3333个CSS像素宽。如果你查看window.innerWidth无论如何都不应该这样做),它会报告33个CSS像素。然后,你乘以你的dpr 3,你得到99,但实际上浏览器可能/会绘制100。

要询问实际使用的大小,您需要使用ResizeObserver并通过devicePixelContentBoxSize报告使用的精确大小。

const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-.5, .5, .5, -.5, -1, 1);
const renderer = new THREE.WebGLRenderer();

const geometry = new THREE.PlaneBufferGeometry();
const material = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertex-shader").innerText,
  fragmentShader: document.getElementById("fragment-shader").innerText,
});

const mesh = new THREE.Mesh(geometry, material);

function onResize(entries) {
  for (const entry of entries) {
    let width;
    let height;
    let dpr = window.devicePixelRatio;
    if (entry.devicePixelContentBoxSize) {
      // NOTE: Only this path gives the correct answer
      // The other 2 paths are an imperfect fallback
      // for browsers that don't provide anyway to do this
      width = entry.devicePixelContentBoxSize[0].inlineSize;
      height = entry.devicePixelContentBoxSize[0].blockSize;
      dpr = 1; // it's already in width and height
    } else if (entry.contentBoxSize) {
      if (entry.contentBoxSize[0]) {
        width = entry.contentBoxSize[0].inlineSize;
        height = entry.contentBoxSize[0].blockSize;
      } else {
        width = entry.contentBoxSize.inlineSize;
        height = entry.contentBoxSize.blockSize;
      }
    } else {
      width = entry.contentRect.width;
      height = entry.contentRect.height;
    }
    // IMPORTANT! You must pass false here otherwise three.js
    // will mess up the CSS!
    renderer.setSize(Math.round(width * dpr), Math.round(height * dpr), false);
    renderer.render(scene, camera);
  }
}

const canvas = renderer.domElement;
const resizeObserver = new ResizeObserver(onResize);
try {
  // because some browers don't support this yet
  resizeObserver.observe(canvas, {box: 'device-pixel-content-box'});
} catch (e) {
  resizeObserver.observe(canvas, {box: 'content-box'});
}
document.body.appendChild(renderer.domElement);
scene.add(mesh)
renderer.render(scene, camera);
html, body, canvas {
  margin: 0;
  width: 100%;
  height: 100%;
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.123/build/three.js"></script>

<script id="vertex-shader" type="x-shader/x-vertex">
        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
        void main () {
            if (int(gl_FragCoord.x)%2==0 && int(gl_FragCoord.y)%2==0) {
                gl_FragColor = vec4(1, 1, 1, 1);
            } else {
                gl_FragColor = vec4(0, 0, 0, 1);
            }
        }
</script>

任何其他方法只有在某些情况下才有效。为了证明这一点,以下是使用setPixelRatio和window.innerWidth与使用ResizeObserver的方法。

function test(parent, fn) {
  const scene = new THREE.Scene();
  const camera = new THREE.OrthographicCamera(-.5, .5, .5, -.5, -1, 1);
  const renderer = new THREE.WebGLRenderer();

  const geometry = new THREE.PlaneBufferGeometry();
  const material = new THREE.ShaderMaterial({
    vertexShader: document.getElementById("vertex-shader").innerText,
    fragmentShader: document.getElementById("fragment-shader").innerText,
  });

  const mesh = new THREE.Mesh(geometry, material);
  fn(renderer, scene, camera);
  parent.appendChild(renderer.domElement);
  scene.add(mesh)
  renderer.render(scene, camera);
}

function resizeOberserverInit(renderer, scene, camera) {
  function onResize(entries) {
    for (const entry of entries) {
      let width;
      let height;
      let dpr = window.devicePixelRatio;
      if (entry.devicePixelContentBoxSize) {
      // NOTE: Only this path gives the correct answer
      // The other 2 paths are an imperfect fallback
      // for browsers that don't provide anyway to do this
        width = entry.devicePixelContentBoxSize[0].inlineSize;
        height = entry.devicePixelContentBoxSize[0].blockSize;
        dpr = 1; // it's already in width and height
      } else if (entry.contentBoxSize) {
        if (entry.contentBoxSize[0]) {
          width = entry.contentBoxSize[0].inlineSize;
          height = entry.contentBoxSize[0].blockSize;
        } else {
          width = entry.contentBoxSize.inlineSize;
          height = entry.contentBoxSize.blockSize;
        }
      } else {
        width = entry.contentRect.width;
        height = entry.contentRect.height;
      }
      // IMPORTANT! You must pass false here otherwise three.js
      // will mess up the CSS!
      renderer.setSize(Math.round(width * dpr), Math.round(height * dpr), false);
      renderer.render(scene, camera);
    }
  }

  const canvas = renderer.domElement;
  const resizeObserver = new ResizeObserver(onResize);
  try {
    // because some browers don't support this yet
    resizeObserver.observe(canvas, {box: 'device-pixel-content-box'});
  } catch (e) {
    resizeObserver.observe(canvas, {box: 'content-box'});
  }
}

function innerWidthDPRInit(renderer, scene, camera) {
  renderer.setPixelRatio(window.deivcePixelRatio);
  renderer.setSize(window.innerWidth, 70);
  window.addEventListener('resize', () => {
    renderer.setPixelRatio(window.deivcePixelRatio);
    renderer.setSize(window.innerWidth, 70);
    renderer.render(scene, camera);
  });
}

test(document.querySelector('#innerWidth-dpr'), innerWidthDPRInit);
test(document.querySelector('#resizeObserver'), resizeOberserverInit);
body {
  margin: 0;
}
canvas {
  height: 70px;
  display: block;
}
#resizeObserver,
#resizeObserver>canvas {
  width: 100%;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.123/build/three.js"></script>

<script id="vertex-shader" type="x-shader/x-vertex">
        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
        void main () {
            if (int(gl_FragCoord.x)%2==0 && int(gl_FragCoord.y)%2==0) {
                gl_FragColor = vec4(1, 1, 1, 1);
            } else {
                gl_FragColor = vec4(0, 0, 0, 1);
            }
        }
</script>
<div>via window.innerWidth and setPixelRatio</div>
<div id="innerWidth-dpr"></div>
<div>via ResizeObserver</div>
<div id="resizeObserver"></div>

尝试在浏览器中缩放或尝试使用不同的设备。

更新

规范在2021年2月发生了变化。现在crisp-edges表示“使用最近邻”,pixelated表示“保持像素化”,这可以翻译为“如果您希望采取比最近邻更好的措施来保持图像像素化”。请参见此答案


你可能想要添加一个 else if (entry.contentBoxSize) { 条件语句,因为当前版本的 FF 尚未将其作为序列传递,但它确实存在。 - Kaiido
它与contentRect不同吗?contentRect会消失吗? - gman
在我的 FF 上,这些值确实有所不同,并且我没有看到任何表示 contentRect 会离开的东西。 - Kaiido
在我的测试中,contentRect.width 似乎比 contentBoxSize.inlineSize 更准确。后者似乎被截断了。 - gman
对我来说,它们的值确实有所不同,但是只有在小数点后五位之后。这很奇怪,因为根据规格,它们应该是相同的“content rect”... 实际上,.contentRect 可能会在未来被弃用,但我希望当浏览器这样做时,他们也会按顺序实现 devicePixelContentBoxSize 或至少 contentBoxSize - Kaiido

1
这是未配置设备像素比的典型副作用。这意味着您的应用程序不一定在本机解决方案中运行。以下内容会产生预期的结果(我已经截取了屏幕截图并在Gimp中检查了颜色值)。

const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-.5, .5, .5, -.5, -1, 1);
const renderer = new THREE.WebGLRenderer();

const geometry = new THREE.PlaneBufferGeometry();
const material = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertex-shader").innerText,
  fragmentShader: document.getElementById("fragment-shader").innerText,
});
const mesh = new THREE.Mesh(geometry, material);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(mesh)
renderer.render(scene, camera);
body {
      margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.123/build/three.js"></script>

<script id="vertex-shader" type="x-shader/x-vertex">
        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
        void main () {
            if (int(gl_FragCoord.x)%2==0 && int(gl_FragCoord.y)%2==0) {
                gl_FragColor = vec4(1, 1, 1, 1);
            } else {
                gl_FragColor = vec4(0, 0, 0, 1);
            }
        }
</script>


1
这不是正确的,只是靠运气工作。 - gman
1
“我已经截图并检查了数值” != 它有效。这是在我的机器上的示例:https://i.imgur.com/HYKczZe.png - gman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接