feat(renderer): render image textures
This commit is contained in:
173
renderer/image-textures.ts
Normal file
173
renderer/image-textures.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { Asset } from "@core/asset";
|
||||
import type { ScreenRect, WebGlRendererContext } from "./types";
|
||||
|
||||
export type ImageTextureRenderer = {
|
||||
render(asset: Asset, rect: ScreenRect): boolean;
|
||||
dispose(): void;
|
||||
};
|
||||
|
||||
type TextureEntry =
|
||||
| { status: "loading"; image: HTMLImageElement }
|
||||
| { status: "ready"; texture: WebGLTexture }
|
||||
| { status: "error" };
|
||||
|
||||
export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer {
|
||||
const { gl } = context;
|
||||
const program = createProgram(gl);
|
||||
const positionLocation = gl.getAttribLocation(program, "a_position");
|
||||
const texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
|
||||
const samplerLocation = gl.getUniformLocation(program, "u_image");
|
||||
const positionBuffer = gl.createBuffer();
|
||||
const texCoordBuffer = gl.createBuffer();
|
||||
const textures = new Map<string, TextureEntry>();
|
||||
|
||||
if (!positionBuffer || !texCoordBuffer || !samplerLocation) throw new Error("Failed to create image texture renderer");
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
|
||||
|
||||
return {
|
||||
render(asset, rect) {
|
||||
const entry = getTextureEntry(context, textures, asset, invalidate);
|
||||
if (entry.status !== "ready") return false;
|
||||
|
||||
gl.disable(gl.SCISSOR_TEST);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.useProgram(program);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
||||
gl.uniform1i(samplerLocation, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, rect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
||||
gl.enableVertexAttribArray(texCoordLocation);
|
||||
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
gl.disable(gl.BLEND);
|
||||
gl.enable(gl.SCISSOR_TEST);
|
||||
return true;
|
||||
},
|
||||
dispose() {
|
||||
for (const entry of textures.values()) {
|
||||
if (entry.status === "ready") gl.deleteTexture(entry.texture);
|
||||
}
|
||||
gl.deleteBuffer(positionBuffer);
|
||||
gl.deleteBuffer(texCoordBuffer);
|
||||
gl.deleteProgram(program);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getTextureEntry(
|
||||
context: WebGlRendererContext,
|
||||
textures: Map<string, TextureEntry>,
|
||||
asset: Asset,
|
||||
invalidate: () => void,
|
||||
): TextureEntry {
|
||||
const cached = textures.get(asset.id);
|
||||
if (cached) return cached;
|
||||
|
||||
const image = new Image();
|
||||
textures.set(asset.id, { status: "loading", image });
|
||||
image.onload = () => {
|
||||
const texture = createTexture(context.gl, image);
|
||||
textures.set(asset.id, { status: "ready", texture });
|
||||
invalidate();
|
||||
};
|
||||
image.onerror = () => {
|
||||
textures.set(asset.id, { status: "error" });
|
||||
invalidate();
|
||||
};
|
||||
image.src = asset.source;
|
||||
|
||||
return { status: "loading", image };
|
||||
}
|
||||
|
||||
function createTexture(gl: WebGL2RenderingContext, image: HTMLImageElement) {
|
||||
const texture = gl.createTexture();
|
||||
if (!texture) throw new Error("Failed to create image texture");
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
function rectVertices(canvas: HTMLCanvasElement, rect: ScreenRect) {
|
||||
const x1 = (rect.x / canvas.width) * 2 - 1;
|
||||
const x2 = ((rect.x + rect.w) / canvas.width) * 2 - 1;
|
||||
const y1 = 1 - (rect.y / canvas.height) * 2;
|
||||
const y2 = 1 - ((rect.y + rect.h) / canvas.height) * 2;
|
||||
|
||||
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;
|
||||
in vec2 a_texCoord;
|
||||
out vec2 v_texCoord;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
v_texCoord = a_texCoord;
|
||||
}`,
|
||||
);
|
||||
const fragmentShader = compileShader(
|
||||
gl,
|
||||
gl.FRAGMENT_SHADER,
|
||||
`#version 300 es
|
||||
precision mediump float;
|
||||
uniform sampler2D u_image;
|
||||
in vec2 v_texCoord;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
outColor = texture(u_image, v_texCoord);
|
||||
}`,
|
||||
);
|
||||
const program = gl.createProgram();
|
||||
if (!program) throw new Error("Failed to create image 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 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