perf(renderer): draw checkerboard with shader

This commit is contained in:
syntaxbullet
2026-07-04 15:43:50 +02:00
parent ef0ce15bb5
commit a030f2065a
4 changed files with 283 additions and 24 deletions

View File

@@ -1,26 +1,148 @@
import { clearScreenRect } from "./clear-rect";
import type { ScreenRect, WebGlRendererContext } from "./types";
import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
const darkChecker = [0.82, 0.82, 0.86, 1] as const;
const lightChecker = [0.94, 0.94, 0.97, 1] as const;
const darkChecker: RgbaColor = [0.82, 0.82, 0.86, 1];
const lightChecker: RgbaColor = [0.94, 0.94, 0.97, 1];
export function renderCheckerboard(context: WebGlRendererContext, rect: ScreenRect, squareSize: number) {
const clampedSquareSize = Math.max(1, squareSize);
const canvasWidth = context.canvas.width;
const canvasHeight = context.canvas.height;
export type CheckerboardRenderer = {
render(rect: ScreenRect, squareSize: number): void;
dispose(): void;
};
for (let py = rect.y; py < rect.y + rect.h; py += clampedSquareSize) {
for (let px = rect.x; px < rect.x + rect.w; px += clampedSquareSize) {
const x = Math.max(0, px);
const y = Math.max(0, py);
const w = Math.min(px + clampedSquareSize, rect.x + rect.w, canvasWidth) - x;
const h = Math.min(py + clampedSquareSize, rect.y + rect.h, canvasHeight) - y;
if (w <= 0 || h <= 0) continue;
export function createCheckerboardRenderer(context: WebGlRendererContext): CheckerboardRenderer {
const { gl } = context;
const program = createProgram(gl);
const positionLocation = gl.getAttribLocation(program, "a_position");
const canvasSizeLocation = gl.getUniformLocation(program, "u_canvasSize");
const originLocation = gl.getUniformLocation(program, "u_origin");
const squareSizeLocation = gl.getUniformLocation(program, "u_squareSize");
const darkColorLocation = gl.getUniformLocation(program, "u_darkColor");
const lightColorLocation = gl.getUniformLocation(program, "u_lightColor");
const positionBuffer = gl.createBuffer();
const checker =
(Math.floor((px - rect.x) / clampedSquareSize) + Math.floor((py - rect.y) / clampedSquareSize)) % 2 === 0;
clearScreenRect(context, { x, y, w, h }, checker ? darkChecker : lightChecker);
}
if (
positionLocation < 0 ||
!canvasSizeLocation ||
!originLocation ||
!squareSizeLocation ||
!darkColorLocation ||
!lightColorLocation ||
!positionBuffer
) {
if (positionBuffer) gl.deleteBuffer(positionBuffer);
gl.deleteProgram(program);
throw new Error("Failed to create checkerboard renderer");
}
return {
render(rect, squareSize) {
const drawRect = intersectScreenRectWithCanvas(rect, context.canvas);
if (!drawRect) return;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
gl.disable(gl.BLEND);
gl.useProgram(program);
gl.uniform2f(canvasSizeLocation, context.canvas.width, context.canvas.height);
gl.uniform2f(originLocation, rect.x, rect.y);
gl.uniform1f(squareSizeLocation, Math.max(1, squareSize));
gl.uniform4fv(darkColorLocation, darkChecker);
gl.uniform4fv(lightColorLocation, lightChecker);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, screenRectVertices(drawRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disableVertexAttribArray(positionLocation);
},
dispose() {
gl.deleteBuffer(positionBuffer);
gl.deleteProgram(program);
},
};
}
function intersectScreenRectWithCanvas(rect: ScreenRect, canvas: HTMLCanvasElement): ScreenRect | undefined {
const x1 = Math.max(0, rect.x);
const y1 = Math.max(0, rect.y);
const x2 = Math.min(canvas.width, rect.x + rect.w);
const y2 = Math.min(canvas.height, rect.y + rect.h);
if (x2 <= x1 || y2 <= y1) return undefined;
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
function screenRectVertices(rect: ScreenRect) {
const x1 = rect.x;
const x2 = rect.x + rect.w;
const y1 = rect.y;
const y2 = rect.y + rect.h;
return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]);
}
function createProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
uniform vec2 u_canvasSize;
out vec2 v_screenPosition;
void main() {
vec2 clip = vec2((a_position.x / u_canvasSize.x) * 2.0 - 1.0, 1.0 - (a_position.y / u_canvasSize.y) * 2.0);
gl_Position = vec4(clip, 0.0, 1.0);
v_screenPosition = a_position;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision highp float;
uniform vec2 u_origin;
uniform float u_squareSize;
uniform vec4 u_darkColor;
uniform vec4 u_lightColor;
in vec2 v_screenPosition;
out vec4 outColor;
void main() {
vec2 cell = floor((v_screenPosition - u_origin) / max(1.0, u_squareSize));
float parity = mod(cell.x + cell.y, 2.0);
outColor = mix(u_darkColor, u_lightColor, parity);
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create checkerboard shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown checkerboard program link error";
gl.deleteProgram(program);
throw new Error(message);
}
return program;
}
function compileShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) throw new Error("Failed to create shader");
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const message = gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
gl.deleteShader(shader);
throw new Error(message);
}
return shader;
}