Я хочу написать небольшую программу, которая вычисляет, например, простые числа. Моя идея заключалась в том, чтобы использовать 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();
Это типичный побочный эффект отсутствия настройки соотношения пикселей устройства. Это означает, что ваше приложение не обязательно работает в нативном решении. Следующее дает ожидаемый результат (я сделал скриншот и проверил значения цвета в 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>
"Я сделал скриншоты и проверил значения" != работает. Вот ваш пример на моей машине: i.imgur.com/HYKczZe.png
Ранее принятый ответ неверен и сработал только благодаря удаче ... Другими словами, если он сработал для вас, вам повезло, что ваша машина / браузер / ОС / текущие настройки оказались рабочими. Измените настройку, переключитесь на другой браузер/ОС/машину, и это не будет работать.
Здесь намешано много проблем
Есть 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 сделает это за вас
Это то, что пытался сделать звонок 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 еще не передает его как последовательность, но в любом случае он есть.
Отличается от contentRect
? contentRect
уходит?
На моем FF значения различаются, да, и я не вижу ничего, говорящего о том, что contentRect
уйдет.
хм, по моим тестам contentRect.width
кажется более точным, чем contentBoxSize.inlineSize
. Последнее кажется урезанным.
Для меня они действительно различались по значениям, но после 5 знаков после запятой. Что странно, так как по спецификациям они должны быть одним и тем же «прямоугольником содержимого»... И на самом деле .contentRect
действительно может стать устаревшим в будущем, но я надеюсь, что когда браузеры сделают это, они также будут реализовывать devicePixelContentBoxSize или, по крайней мере, contentBoxSize как последовательность.
Это неправильно и сработало только благодаря удаче.