much faster, slightly reduced precision

This commit is contained in:
Orion Reed 2024-12-01 22:16:30 -05:00
parent bf1ca91e8b
commit 8fc9df7b89
2 changed files with 36 additions and 52 deletions

View File

@ -10,7 +10,6 @@ export class DistanceField extends HTMLElement {
static tagName = 'distance-field'; static tagName = 'distance-field';
private textures: WebGLTexture[] = []; private textures: WebGLTexture[] = [];
private pingPongIndex: number = 0;
private shapes!: NodeListOf<Element>; private shapes!: NodeListOf<Element>;
private canvas!: HTMLCanvasElement; private canvas!: HTMLCanvasElement;
@ -18,7 +17,6 @@ export class DistanceField extends HTMLElement {
private framebuffer!: WebGLFramebuffer; private framebuffer!: WebGLFramebuffer;
private fullscreenQuadVAO!: WebGLVertexArrayObject; private fullscreenQuadVAO!: WebGLVertexArrayObject;
private shapeVAO!: WebGLVertexArrayObject; private shapeVAO!: WebGLVertexArrayObject;
private offsetCache: Map<number, Float32Array> = new Map();
private jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm private jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm
private renderProgram!: WebGLProgram; // Shader program for final rendering private renderProgram!: WebGLProgram; // Shader program for final rendering
@ -26,6 +24,10 @@ export class DistanceField extends HTMLElement {
private static readonly MAX_DISTANCE = 99999.0; private static readonly MAX_DISTANCE = 99999.0;
private positionBuffer: WebGLBuffer | null = null;
private isPingTexture: boolean = true;
static define() { static define() {
customElements.define(this.tagName, this); customElements.define(this.tagName, this);
} }
@ -105,10 +107,10 @@ export class DistanceField extends HTMLElement {
} }
this.textures = []; this.textures = [];
// Enable the EXT_color_buffer_float extension for high-precision floating-point textures // Enable the EXT_color_buffer_half_float extension for high-precision floating-point textures
const ext = gl.getExtension('EXT_color_buffer_float'); const ext = gl.getExtension('EXT_color_buffer_half_float');
if (!ext) { if (!ext) {
console.error('EXT_color_buffer_float extension is not supported.'); console.error('EXT_color_buffer_half_float extension is not supported.');
return; return;
} }
@ -124,17 +126,7 @@ export class DistanceField extends HTMLElement {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Use high-precision format for accurate distance calculations // Use high-precision format for accurate distance calculations
gl.texImage2D( gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
gl.TEXTURE_2D,
0,
gl.RGBA32F, // Internal format: 32-bit floating point per channel
width,
height,
0,
gl.RGBA, // Format
gl.FLOAT, // Type
null
);
this.textures.push(texture); this.textures.push(texture);
} }
@ -158,15 +150,9 @@ export class DistanceField extends HTMLElement {
*/ */
private initSeedPointRendering() { private initSeedPointRendering() {
const gl = this.glContext; const gl = this.glContext;
const positions: number[] = [];
// Set up Vertex Array Object (VAO) and buffer for shapes
this.shapeVAO = gl.createVertexArray()!;
gl.bindVertexArray(this.shapeVAO);
const positionBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Collect positions and assign unique IDs to all shapes // Collect positions and assign unique IDs to all shapes
const positions: number[] = [];
this.shapes.forEach((geometry, index) => { this.shapes.forEach((geometry, index) => {
const rect = geometry.getBoundingClientRect(); const rect = geometry.getBoundingClientRect();
@ -202,16 +188,21 @@ export class DistanceField extends HTMLElement {
); );
}); });
// Upload positions to the GPU if (!this.shapeVAO) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); this.shapeVAO = gl.createVertexArray()!;
gl.bindVertexArray(this.shapeVAO);
this.positionBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
// Configure vertex attributes const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position');
gl.useProgram(this.seedProgram); gl.enableVertexAttribArray(positionLocation);
const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position'); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation); gl.bindVertexArray(null);
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0); } else {
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer!);
gl.bindVertexArray(null); gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(positions));
}
// Render the seed points into the texture // Render the seed points into the texture
this.renderSeedPoints(); this.renderSeedPoints();
@ -225,7 +216,7 @@ export class DistanceField extends HTMLElement {
const gl = this.glContext; const gl = this.glContext;
// Bind framebuffer to render to the seed texture // Bind framebuffer to render to the seed texture
const seedTexture = this.textures[this.pingPongIndex % 2]; const seedTexture = this.textures[this.isPingTexture ? 0 : 1];
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, seedTexture, 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, seedTexture, 0);
@ -275,8 +266,8 @@ export class DistanceField extends HTMLElement {
const gl = this.glContext; const gl = this.glContext;
// Swap textures for ping-pong rendering // Swap textures for ping-pong rendering
const inputTexture = this.textures[this.pingPongIndex % 2]; const inputTexture = this.isPingTexture ? this.textures[0] : this.textures[1];
const outputTexture = this.textures[(this.pingPongIndex + 1) % 2]; const outputTexture = this.isPingTexture ? this.textures[1] : this.textures[0];
// Bind framebuffer to output texture // Bind framebuffer to output texture
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
@ -298,8 +289,8 @@ export class DistanceField extends HTMLElement {
// Draw a fullscreen quad to process all pixels // Draw a fullscreen quad to process all pixels
this.drawFullscreenQuad(); this.drawFullscreenQuad();
// Swap ping-pong index for the next pass // Toggle the flag
this.pingPongIndex++; this.isPingTexture = !this.isPingTexture;
} }
/** /**
@ -316,7 +307,7 @@ export class DistanceField extends HTMLElement {
gl.useProgram(this.renderProgram); gl.useProgram(this.renderProgram);
// Bind the final texture containing the computed distance field // Bind the final texture containing the computed distance field
const finalTexture = this.textures[this.pingPongIndex % 2]; const finalTexture = this.textures[this.isPingTexture ? 0 : 1];
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, finalTexture); gl.bindTexture(gl.TEXTURE_2D, finalTexture);
gl.uniform1i(gl.getUniformLocation(this.renderProgram, 'u_texture'), 0); gl.uniform1i(gl.getUniformLocation(this.renderProgram, 'u_texture'), 0);
@ -403,19 +394,13 @@ export class DistanceField extends HTMLElement {
* @returns A Float32Array of offsets. * @returns A Float32Array of offsets.
*/ */
private computeOffsets(stepSize: number): Float32Array { private computeOffsets(stepSize: number): Float32Array {
if (this.offsetCache.has(stepSize)) {
return this.offsetCache.get(stepSize)!;
}
const offsets = []; const offsets = [];
for (let y = -1; y <= 1; y++) { for (let y = -1; y <= 1; y++) {
for (let x = -1; x <= 1; x++) { for (let x = -1; x <= 1; x++) {
offsets.push((x * stepSize) / this.canvas.width, (y * stepSize) / this.canvas.height); offsets.push((x * stepSize) / this.canvas.width, (y * stepSize) / this.canvas.height);
} }
} }
const offsetArray = new Float32Array(offsets); return new Float32Array(offsets);
this.offsetCache.set(stepSize, offsetArray);
return offsetArray;
} }
/** /**
@ -463,7 +448,7 @@ export class DistanceField extends HTMLElement {
* Transforms vertices to normalized device coordinates and passes texture coordinates to the fragment shader. * Transforms vertices to normalized device coordinates and passes texture coordinates to the fragment shader.
*/ */
const commonVertShader = vert`#version 300 es const commonVertShader = vert`#version 300 es
precision highp float; precision mediump float;
in vec2 a_position; in vec2 a_position;
out vec2 v_texCoord; out vec2 v_texCoord;
@ -477,7 +462,7 @@ void main() {
* Updates the nearest seed point and distance for each pixel by examining neighboring pixels. * Updates the nearest seed point and distance for each pixel by examining neighboring pixels.
*/ */
const jfaFragShader = frag`#version 300 es const jfaFragShader = frag`#version 300 es
precision highp float; precision mediump float;
precision mediump int; precision mediump int;
in vec2 v_texCoord; in vec2 v_texCoord;
@ -525,7 +510,7 @@ void main() {
* Converts distances to colors for visualization. * Converts distances to colors for visualization.
*/ */
const renderFragShader = frag`#version 300 es const renderFragShader = frag`#version 300 es
precision highp float; precision mediump float;
in vec2 v_texCoord; in vec2 v_texCoord;
out vec4 outColor; out vec4 outColor;
@ -560,7 +545,7 @@ void main() {
* Outputs the shape ID to the fragment shader. * Outputs the shape ID to the fragment shader.
*/ */
const seedVertShader = vert`#version 300 es const seedVertShader = vert`#version 300 es
precision highp float; precision mediump float;
in vec3 a_position; // x, y position and shapeID as z in vec3 a_position; // x, y position and shapeID as z
flat out float v_shapeID; flat out float v_shapeID;
@ -575,7 +560,7 @@ void main() {
* Initializes the texture with seed point positions and shape IDs. * Initializes the texture with seed point positions and shape IDs.
*/ */
const seedFragShader = frag`#version 300 es const seedFragShader = frag`#version 300 es
precision highp float; precision mediump float;
flat in float v_shapeID; flat in float v_shapeID;
uniform vec2 u_canvasSize; uniform vec2 u_canvasSize;

View File

@ -47,8 +47,7 @@ export class WebGLUtils {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
// Initialize WebGL2 context const gl = canvas.getContext('webgl2', { antialias: true });
const gl = canvas.getContext('webgl2');
if (!gl) { if (!gl) {
console.error('WebGL2 is not available.'); console.error('WebGL2 is not available.');
return { gl: undefined, canvas }; return { gl: undefined, canvas };