WebGL смешивает цвета на уровне пикселей

Я хочу написать небольшую программу, которая вычисляет, например, простые числа. Моя идея заключалась в том, чтобы использовать 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();
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
3
0
443
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это типичный побочный эффект отсутствия настройки соотношения пикселей устройства. Это означает, что ваше приложение не обязательно работает в нативном решении. Следующее дает ожидаемый результат (я сделал скриншот и проверил значения цвета в 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/[email protected]/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>

Это неправильно и сработало только благодаря удаче.

gman 24.12.2020 08:54

"Я сделал скриншоты и проверил значения" != работает. Вот ваш пример на моей машине: i.imgur.com/HYKczZe.png

gman 06.01.2021 14:32
Ответ принят как подходящий

Ранее принятый ответ неверен и сработал только благодаря удаче ... Другими словами, если он сработал для вас, вам повезло, что ваша машина / браузер / ОС / текущие настройки оказались рабочими. Измените настройку, переключитесь на другой браузер/ОС/машину, и это не будет работать.

Здесь намешано много проблем

1. Получение не размытого холста

Есть 2 вещи, которые вы должны сделать, чтобы получить не размытый холст

отключить фильтрацию в браузере

В Chrome/Edge/Safari вы можете сделать это с помощью canvas { image-rendering: pixelated; } в Firefox canvas { image-rendering: crisp-edges; }

Обратите внимание, что pixelated более подходит для этой ситуации, чем crisp-edges. Из спецификации, учитывая это исходное изображение

И отображая его в 3 раза больше, алгоритм по умолчанию зависит от браузера, но типичный пример может выглядеть примерно так:

pixelated будет выглядеть примерно так

ВАЖНО: см. обновление внизу crisp-edges будет выглядеть примерно так

Но обратите внимание, что фактический алгоритм, используемый для рисования изображения, не определен спецификацией 🤬, поэтому на данный момент Firefox использует фильтрацию ближайшего соседа для crisp-edges и не поддерживает pixelated. Поэтому вы должны указать pixelated последним, чтобы он переопределял crisp-edges, если он существует.

Холст имеет 2 размера. Размер его буфера рисования (количество пикселей на холсте) и размер экрана (определяется CSS). Но размеры CSS вычисляются в пикселях CSS, которые затем масштабируются по текущему devicePixelRatio. Это означает, что если вы не увеличиваете или уменьшаете масштаб и используете устройство с devicePixelRatio, отличным от 1.0, и размер вашего буфера рисования соответствует размеру вашего дисплея, то холст будет масштабирован до того, что он должен быть, чтобы соответствовать 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/[email protected]/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. То, что это сработало для вас, было просто удачей. Другими словами, когда вы пытаетесь запустить свой код на другом компьютере или в другом браузере, он может не работать.

Проблема заключается в том, чтобы установить размер буфера рисования холста так, чтобы он соответствовал от 1 до 1, сколько пикселей будет использоваться для отображения холста, вы должны спросить браузер «сколько пикселей вы использовали».

Причина, например, в том, что у вас есть устройство с соотношением пикселей устройства (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/[email protected]/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/[email protected]/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 года . crisp-edges теперь означает «использовать ближайшего соседа», а pixelated означает «сохранять пиксельный вид», что можно перевести как «если вы хотите, то сделайте что-то лучше, чем ближайший сосед, который сохраняет изображение пиксельным». Смотрите этот ответ

Возможно, вы захотите добавить предложение else if (entry.contentBoxSize) {, текущий FF еще не передает его как последовательность, но в любом случае он есть.

Kaiido 26.12.2020 04:36

Отличается от contentRect? contentRect уходит?

gman 26.12.2020 06:00

На моем FF значения различаются, да, и я не вижу ничего, говорящего о том, что contentRect уйдет.

Kaiido 26.12.2020 06:04

хм, по моим тестам contentRect.width кажется более точным, чем contentBoxSize.inlineSize. Последнее кажется урезанным.

gman 26.12.2020 06:06

Для меня они действительно различались по значениям, но после 5 знаков после запятой. Что странно, так как по спецификациям они должны быть одним и тем же «прямоугольником содержимого»... И на самом деле .contentRect действительно может стать устаревшим в будущем, но я надеюсь, что когда браузеры сделают это, они также будут реализовывать devicePixelContentBoxSize или, по крайней мере, contentBoxSize как последовательность.

Kaiido 26.12.2020 07:22

Другие вопросы по теме