much faster, slightly reduced precision
This commit is contained in:
parent
bf1ca91e8b
commit
8fc9df7b89
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue