perf(renderer): draw checkerboard with shader
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user