WebGL 在像素级混合颜色

WebGL mixes colors on pixel level

我想写一个小程序来计算例如素数。我的想法是使用 webgl/threejs 将所有代表素数(基于坐标)的像素着色为白色,其余为黑色。我的问题是 webgl 以某种方式混合了像素级别的颜色(如果黑色中间有一个白色像素 canvas 它会与环境混合,并且那个白色像素周围的像素会变成灰色)。由于这个问题,我无法正确识别白色和黑色像素。

我的问题:如何让 webgl 或 canvas 在不混合的情况下为每个像素使用正确的颜色?

这是一个演示,其中我将每 2 个像素涂成白色:

<!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();

这是不配置设备像素比的典型副作用。这意味着您的应用不一定 运行 在原生解决方案中。以下产生了预期的结果(我制作了一个屏幕截图并检查了 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>

接受的答案不正确,只能靠运气...换句话说,如果它对你有用,你很幸运,你的 machine/browser/OS/current-settings 碰巧似乎有用。改个设置,换个browser/os/machine也不行。

这里有很多问题被混淆了

1。变得不模糊 canvas

要获得不模糊的效果,您需要做 2 件事 canvas

关闭浏览器的过滤

在 Chrome/Edge/Safari 中,您可以在 firefox canvas { image-rendering: crisp-edges; }

上使用 canvas { image-rendering: pixelated; } 执行此操作

请注意,对于这种情况,pixelatedcrisp-edges 更正确。根据规范,给定这张原始图片

并根据浏览器显示默认算法大小的 3 倍,但典型示例可能如下所示

pixelated 看起来像这样

crisp-edges 看起来像这样

但请注意,用于绘制图像的实际算法并未由规范定义,因此目前 firefox 恰好对 crisp-edges 使用最近邻过滤并且不支持 pixelated。因此,您应该最后指定 pixelated,以便它覆盖 crisp-edges(如果存在)

A canvas 有 2 种尺寸。它的绘图缓冲区大小(canvas 中的像素数)及其显示大小(由 CSS 定义)。但是 CSS 大小是以 CSS 像素计算的,然后按当前 devicePixelRatio 缩放。这意味着如果你没有放大或缩小,并且你的设备上的 devicePixelRatio 不是 1.0,并且你的绘图缓冲区大小与你的显示大小匹配,那么 canvas 将被缩放到它需要的大小匹配 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。在 canvas 中获取 1 个像素 = 在屏幕上获取 1 个像素

这就是调用 setPixelRatio 的目的。它似乎对你有用只是运气。换句话说,当您尝试 运行 您的代码在不同的机器或不同的浏览器上时,它可能无法工作。

问题是为了设置 canvas 的绘图缓冲区的大小,使其 1 比 1 将使用多少像素来显示 canvas 你必须问浏览器“您使用了多少像素”。

原因是,例如,假设您有一个设备像素比 (dpr) 为 3 的设备。让我们 window 以 100 个设备像素宽。那 33.3333 CSS 像素宽。如果您查看 window.innerWidth (which you should never do anyway),它将报告 33 CSS 像素。如果你然后乘以你的 dpr 3 你得到 99 但实际上浏览器 may/will 画 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>

尝试放大浏览器或尝试不同的设备。