use private fields

This commit is contained in:
“chrisshank” 2024-12-19 01:12:54 -08:00
parent 09b0fed6ec
commit e036e9859f
2 changed files with 377 additions and 378 deletions

View File

@ -13,21 +13,20 @@ export class FolkDistanceField extends FolkBaseSet {
static readonly MAX_DISTANCE = 99999.0; static readonly MAX_DISTANCE = 99999.0;
private canvas!: HTMLCanvasElement; #canvas!: HTMLCanvasElement;
private glContext!: WebGL2RenderingContext; #glContext!: WebGL2RenderingContext;
private framebuffer!: WebGLFramebuffer; #framebuffer!: WebGLFramebuffer;
private fullscreenQuadVAO!: WebGLVertexArrayObject; #fullscreenQuadVAO!: WebGLVertexArrayObject;
#jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm
private jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm #renderProgram!: WebGLProgram; // Shader program for final rendering
private renderProgram!: WebGLProgram; // Shader program for final rendering #seedProgram!: WebGLProgram; // Shader program for rendering seed points
private seedProgram!: WebGLProgram; // Shader program for rendering seed points
/** /**
* Groups data for handling different sets of shapes. * Groups data for handling different sets of shapes.
* 'mergeA' and 'mergeB' shapes will have their distance fields merged in rendering, * 'mergeA' and 'mergeB' shapes will have their distance fields merged in rendering,
* while 'others' will be processed separately. * while 'others' will be processed separately.
*/ */
private groups: { #groups: {
[groupName: string]: { [groupName: string]: {
textures: WebGLTexture[]; textures: WebGLTexture[];
isPingTexture: boolean; isPingTexture: boolean;
@ -37,7 +36,7 @@ export class FolkDistanceField extends FolkBaseSet {
} = {}; } = {};
// Add class property to store Float32Arrays // Add class property to store Float32Arrays
private groupBuffers: { #groupBuffers: {
[groupName: string]: Float32Array; [groupName: string]: Float32Array;
} = {}; } = {};
@ -45,7 +44,7 @@ export class FolkDistanceField extends FolkBaseSet {
super.connectedCallback(); super.connectedCallback();
// Initialize groups for 'mergeA', 'mergeB', and 'others' // Initialize groups for 'mergeA', 'mergeB', and 'others'
this.groups = { this.#groups = {
mergeA: { mergeA: {
textures: [], textures: [],
isPingTexture: true, isPingTexture: true,
@ -66,35 +65,35 @@ export class FolkDistanceField extends FolkBaseSet {
}, },
}; };
this.initWebGL(); this.#initWebGL();
this.initShaders(); this.#initShaders();
this.initPingPongTextures(); this.#initPingPongTextures();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.#handleResize);
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.#handleResize);
this.cleanupWebGLResources(); this.#cleanupWebGLResources();
} }
private initWebGL() { #initWebGL() {
const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight); const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight);
if (!gl || !canvas) { if (!gl || !canvas) {
throw new Error('Failed to initialize WebGL context.'); throw new Error('Failed to initialize WebGL context.');
} }
this.canvas = canvas; this.#canvas = canvas;
this.renderRoot.prepend(canvas); this.renderRoot.prepend(canvas);
this.glContext = gl; this.#glContext = gl;
// Create framebuffer object // Create framebuffer object
this.framebuffer = gl.createFramebuffer(); this.#framebuffer = gl.createFramebuffer()!;
if (!this.framebuffer) { if (!this.#framebuffer) {
throw new Error('Failed to create framebuffer.'); throw new Error('Failed to create framebuffer.');
} }
} }
@ -107,38 +106,38 @@ export class FolkDistanceField extends FolkBaseSet {
if (this.sourcesMap.size !== this.sourceElements.size) return; if (this.sourcesMap.size !== this.sourceElements.size) return;
this.populateSeedPoints(); this.#populateSeedPoints();
this.runJumpFloodingAlgorithm(); this.#runJumpFloodingAlgorithm();
} }
/** /**
* Initializes all shader programs used in rendering. * Initializes all shader programs used in rendering.
*/ */
private initShaders() { #initShaders() {
this.jfaProgram = WebGLUtils.createShaderProgram(this.glContext, commonVertShader, jfaFragShader); this.#jfaProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, jfaFragShader);
this.renderProgram = WebGLUtils.createShaderProgram(this.glContext, commonVertShader, renderFragShader); this.#renderProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, renderFragShader);
this.seedProgram = WebGLUtils.createShaderProgram(this.glContext, seedVertShader, seedFragShader); this.#seedProgram = WebGLUtils.createShaderProgram(this.#glContext, seedVertShader, seedFragShader);
} }
/** /**
* Initializes textures and framebuffer for ping-pong rendering. * Initializes textures and framebuffer for ping-pong rendering.
* Supports separate textures for 'mergeA', 'mergeB', and 'others' groups. * Supports separate textures for 'mergeA', 'mergeB', and 'others' groups.
*/ */
private initPingPongTextures() { #initPingPongTextures() {
// Initialize textures for each group // Initialize textures for each group
for (const groupName in this.groups) { for (const groupName in this.#groups) {
this.groups[groupName].textures = this.createPingPongTextures(); this.#groups[groupName].textures = this.#createPingPongTextures();
this.groups[groupName].isPingTexture = true; this.#groups[groupName].isPingTexture = true;
} }
} }
/** /**
* Utility method to create ping-pong textures. * Utility method to create ping-pong textures.
*/ */
private createPingPongTextures(): WebGLTexture[] { #createPingPongTextures(): WebGLTexture[] {
const gl = this.glContext; const gl = this.#glContext;
const width = this.canvas.width; const width = this.#canvas.width;
const height = this.canvas.height; const height = this.#canvas.height;
const textures: WebGLTexture[] = []; const textures: WebGLTexture[] = [];
// Enable the EXT_color_buffer_half_float extension for high-precision floating-point textures // Enable the EXT_color_buffer_half_float extension for high-precision floating-point textures
@ -171,8 +170,8 @@ export class FolkDistanceField extends FolkBaseSet {
* Populates seed points and assigns shapes to 'mergeA', 'mergeB', or 'others' groups. * Populates seed points and assigns shapes to 'mergeA', 'mergeB', or 'others' groups.
* Shapes with index 0 and 1 are assigned to 'mergeA' and 'mergeB' respectively. * Shapes with index 0 and 1 are assigned to 'mergeA' and 'mergeB' respectively.
*/ */
private populateSeedPoints() { #populateSeedPoints() {
const gl = this.glContext; const gl = this.#glContext;
const groupPositions: { [groupName: string]: number[] } = { const groupPositions: { [groupName: string]: number[] } = {
mergeA: [], mergeA: [],
mergeB: [], mergeB: [],
@ -252,7 +251,7 @@ export class FolkDistanceField extends FolkBaseSet {
// Initialize buffers and VAOs for each group // Initialize buffers and VAOs for each group
for (const groupName in groupPositions) { for (const groupName in groupPositions) {
const positions = groupPositions[groupName]; const positions = groupPositions[groupName];
const group = this.groups[groupName]; const group = this.#groups[groupName];
if (!group.shapeVAO) { if (!group.shapeVAO) {
// First time initialization // First time initialization
@ -261,23 +260,23 @@ export class FolkDistanceField extends FolkBaseSet {
group.positionBuffer = gl.createBuffer()!; group.positionBuffer = gl.createBuffer()!;
// Create and store the Float32Array // Create and store the Float32Array
this.groupBuffers[groupName] = new Float32Array(positions); this.#groupBuffers[groupName] = new Float32Array(positions);
gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.groupBuffers[groupName], gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, this.#groupBuffers[groupName], gl.DYNAMIC_DRAW);
const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position'); const positionLocation = gl.getAttribLocation(this.#seedProgram, 'a_position');
gl.enableVertexAttribArray(positionLocation); gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null); gl.bindVertexArray(null);
} else { } else {
// Reuse existing Float32Array if size hasn't changed // Reuse existing Float32Array if size hasn't changed
const existingArray = this.groupBuffers[groupName]; const existingArray = this.#groupBuffers[groupName];
if (positions.length !== existingArray.length) { if (positions.length !== existingArray.length) {
// Only create new array if size changed // Only create new array if size changed
this.groupBuffers[groupName] = new Float32Array(positions); this.#groupBuffers[groupName] = new Float32Array(positions);
gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer!); gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer!);
gl.bufferData(gl.ARRAY_BUFFER, this.groupBuffers[groupName], gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, this.#groupBuffers[groupName], gl.DYNAMIC_DRAW);
} else { } else {
// Reuse existing array // Reuse existing array
existingArray.set(positions); existingArray.set(positions);
@ -291,9 +290,9 @@ export class FolkDistanceField extends FolkBaseSet {
for (const groupName in groupPositions) { for (const groupName in groupPositions) {
const positions = groupPositions[groupName]; const positions = groupPositions[groupName];
const vertexCount = positions.length / 3; const vertexCount = positions.length / 3;
this.renderSeedPointsForGroup( this.#renderSeedPointsForGroup(
this.groups[groupName].shapeVAO, this.#groups[groupName].shapeVAO,
this.groups[groupName].textures[this.groups[groupName].isPingTexture ? 0 : 1], this.#groups[groupName].textures[this.#groups[groupName].isPingTexture ? 0 : 1],
vertexCount, vertexCount,
); );
} }
@ -302,24 +301,24 @@ export class FolkDistanceField extends FolkBaseSet {
/** /**
* Utility method to render seed points for a given group. * Utility method to render seed points for a given group.
*/ */
private renderSeedPointsForGroup(vao: WebGLVertexArrayObject, seedTexture: WebGLTexture, vertexCount: number) { #renderSeedPointsForGroup(vao: WebGLVertexArrayObject, seedTexture: WebGLTexture, vertexCount: number) {
const gl = this.glContext; const gl = this.#glContext;
// Bind framebuffer to render to the seed texture // Bind framebuffer to render to the seed texture
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);
// Clear the texture with a large initial distance // Clear the texture with a large initial distance
gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
gl.clearColor(0.0, 0.0, 0.0, FolkDistanceField.MAX_DISTANCE); gl.clearColor(0.0, 0.0, 0.0, FolkDistanceField.MAX_DISTANCE);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
// Use the seed shader program // Use the seed shader program
gl.useProgram(this.seedProgram); gl.useProgram(this.#seedProgram);
// Set the canvas size uniform // Set the canvas size uniform
const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize'); const canvasSizeLocation = gl.getUniformLocation(this.#seedProgram, 'u_canvasSize');
gl.uniform2f(canvasSizeLocation, this.canvas.width, this.canvas.height); gl.uniform2f(canvasSizeLocation, this.#canvas.width, this.#canvas.height);
// Bind VAO and draw shapes // Bind VAO and draw shapes
gl.bindVertexArray(vao); gl.bindVertexArray(vao);
@ -334,18 +333,18 @@ export class FolkDistanceField extends FolkBaseSet {
* Executes the Jump Flooding Algorithm (JFA) for each group separately. * Executes the Jump Flooding Algorithm (JFA) for each group separately.
* 'mergeA' and 'mergeB' groups will have their distance fields merged in rendering. * 'mergeA' and 'mergeB' groups will have their distance fields merged in rendering.
*/ */
private runJumpFloodingAlgorithm() { #runJumpFloodingAlgorithm() {
// Compute initial step size // Compute initial step size
let stepSize = 1 << Math.floor(Math.log2(Math.max(this.canvas.width, this.canvas.height))); let stepSize = 1 << Math.floor(Math.log2(Math.max(this.#canvas.width, this.#canvas.height)));
// Perform passes with decreasing step sizes for each group // Perform passes with decreasing step sizes for each group
for (const groupName in this.groups) { for (const groupName in this.#groups) {
const group = this.groups[groupName]; const group = this.#groups[groupName];
const textures = group.textures; const textures = group.textures;
let isPingTexture = group.isPingTexture; let isPingTexture = group.isPingTexture;
for (let size = stepSize; size >= 1; size >>= 1) { for (let size = stepSize; size >= 1; size >>= 1) {
this.renderPass(size, textures, isPingTexture); this.#renderPass(size, textures, isPingTexture);
isPingTexture = !isPingTexture; isPingTexture = !isPingTexture;
} }
@ -353,38 +352,38 @@ export class FolkDistanceField extends FolkBaseSet {
} }
// Render the final result to the screen // Render the final result to the screen
this.renderToScreen(); this.#renderToScreen();
} }
/** /**
* Performs a single pass of the Jump Flooding Algorithm with a given step size for a specific distance field. * Performs a single pass of the Jump Flooding Algorithm with a given step size for a specific distance field.
*/ */
private renderPass(stepSize: number, textures: WebGLTexture[], isPingTexture: boolean) { #renderPass(stepSize: number, textures: WebGLTexture[], isPingTexture: boolean) {
const gl = this.glContext; const gl = this.#glContext;
// Swap textures for ping-pong rendering // Swap textures for ping-pong rendering
const inputTexture = isPingTexture ? textures[0] : textures[1]; const inputTexture = isPingTexture ? textures[0] : textures[1];
const outputTexture = isPingTexture ? textures[1] : textures[0]; const outputTexture = isPingTexture ? textures[1] : textures[0];
// Bind framebuffer to output texture // Bind framebuffer to output texture
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0);
// Use the JFA shader program // Use the JFA shader program
gl.useProgram(this.jfaProgram); gl.useProgram(this.#jfaProgram);
// Compute and set the offsets uniform for neighboring pixels // Compute and set the offsets uniform for neighboring pixels
const offsets = this.computeOffsets(stepSize); const offsets = this.#computeOffsets(stepSize);
const offsetsLocation = gl.getUniformLocation(this.jfaProgram, 'u_offsets'); const offsetsLocation = gl.getUniformLocation(this.#jfaProgram, 'u_offsets');
gl.uniform2fv(offsetsLocation, offsets); gl.uniform2fv(offsetsLocation, offsets);
// Bind input texture containing the previous step's results // Bind input texture containing the previous step's results
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, inputTexture); gl.bindTexture(gl.TEXTURE_2D, inputTexture);
gl.uniform1i(gl.getUniformLocation(this.jfaProgram, 'u_previousTexture'), 0); gl.uniform1i(gl.getUniformLocation(this.#jfaProgram, 'u_previousTexture'), 0);
// Draw a fullscreen quad to process all pixels // Draw a fullscreen quad to process all pixels
this.drawFullscreenQuad(); this.#drawFullscreenQuad();
// Unbind framebuffer // Unbind framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
@ -394,44 +393,44 @@ export class FolkDistanceField extends FolkBaseSet {
* Renders the final distance field to the screen using the render shader program. * Renders the final distance field to the screen using the render shader program.
* Merges 'mergeA' and 'mergeB' distance fields during rendering, while 'others' are not merged. * Merges 'mergeA' and 'mergeB' distance fields during rendering, while 'others' are not merged.
*/ */
private renderToScreen() { #renderToScreen() {
const gl = this.glContext; const gl = this.#glContext;
// Unbind framebuffer to render directly to the canvas // Unbind framebuffer to render directly to the canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
// Use the render shader program // Use the render shader program
gl.useProgram(this.renderProgram); gl.useProgram(this.#renderProgram);
// Bind the final textures from each group // Bind the final textures from each group
let textureUnit = 0; let textureUnit = 0;
for (const groupName in this.groups) { for (const groupName in this.#groups) {
const group = this.groups[groupName]; const group = this.#groups[groupName];
const finalTexture = group.textures[group.isPingTexture ? 0 : 1]; const finalTexture = group.textures[group.isPingTexture ? 0 : 1];
gl.activeTexture(gl.TEXTURE0 + textureUnit); gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, finalTexture); gl.bindTexture(gl.TEXTURE_2D, finalTexture);
gl.uniform1i(gl.getUniformLocation(this.renderProgram, `u_texture_${groupName}`), textureUnit); gl.uniform1i(gl.getUniformLocation(this.#renderProgram, `u_texture_${groupName}`), textureUnit);
textureUnit++; textureUnit++;
} }
// Draw a fullscreen quad to display the result // Draw a fullscreen quad to display the result
this.drawFullscreenQuad(); this.#drawFullscreenQuad();
} }
/** /**
* Draws a fullscreen quad to cover the entire canvas. * Draws a fullscreen quad to cover the entire canvas.
* This is used in shader passes where every pixel needs to be processed. * This is used in shader passes where every pixel needs to be processed.
*/ */
private drawFullscreenQuad() { #drawFullscreenQuad() {
const gl = this.glContext; const gl = this.#glContext;
// Initialize the quad geometry if it hasn't been done yet // Initialize the quad geometry if it hasn't been done yet
if (!this.fullscreenQuadVAO) { if (!this.#fullscreenQuadVAO) {
this.initFullscreenQuad(); this.#initFullscreenQuad();
} }
gl.bindVertexArray(this.fullscreenQuadVAO); gl.bindVertexArray(this.#fullscreenQuadVAO);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.bindVertexArray(null); gl.bindVertexArray(null);
} }
@ -439,20 +438,20 @@ export class FolkDistanceField extends FolkBaseSet {
/** /**
* Initializes the geometry and buffers for the fullscreen quad. * Initializes the geometry and buffers for the fullscreen quad.
*/ */
private initFullscreenQuad() { #initFullscreenQuad() {
const gl = this.glContext; const gl = this.#glContext;
// Define positions for a quad covering the entire screen // Define positions for a quad covering the entire screen
const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
this.fullscreenQuadVAO = gl.createVertexArray()!; this.#fullscreenQuadVAO = gl.createVertexArray()!;
gl.bindVertexArray(this.fullscreenQuadVAO); gl.bindVertexArray(this.#fullscreenQuadVAO);
const positionBuffer = gl.createBuffer()!; const positionBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionAttributeLocation = gl.getAttribLocation(this.jfaProgram, 'a_position'); const positionAttributeLocation = gl.getAttribLocation(this.#jfaProgram, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation); gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer( gl.vertexAttribPointer(
positionAttributeLocation, positionAttributeLocation,
@ -470,24 +469,24 @@ export class FolkDistanceField extends FolkBaseSet {
* Handles window resize events by updating canvas size, re-initializing textures and seed points, * Handles window resize events by updating canvas size, re-initializing textures and seed points,
* and rerunning the Jump Flooding Algorithm. * and rerunning the Jump Flooding Algorithm.
*/ */
private handleResize = () => { #handleResize = () => {
const gl = this.glContext; const gl = this.#glContext;
// Update canvas size to match the container instead of window // Update canvas size to match the container instead of window
this.canvas.width = this.clientWidth; this.#canvas.width = this.clientWidth;
this.canvas.height = this.clientHeight; this.#canvas.height = this.clientHeight;
// Update the viewport // Update the viewport
gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
// Re-initialize textures with the new dimensions // Re-initialize textures with the new dimensions
this.initPingPongTextures(); this.#initPingPongTextures();
// Re-initialize seed point rendering to update positions // Re-initialize seed point rendering to update positions
this.populateSeedPoints(); this.#populateSeedPoints();
// Rerun the Jump Flooding Algorithm with the new sizes // Rerun the Jump Flooding Algorithm with the new sizes
this.runJumpFloodingAlgorithm(); this.#runJumpFloodingAlgorithm();
}; };
/** /**
@ -496,13 +495,13 @@ export class FolkDistanceField extends FolkBaseSet {
* @param stepSize The current step size for neighbor sampling. * @param stepSize The current step size for neighbor sampling.
* @returns A Float32Array of offsets. * @returns A Float32Array of offsets.
*/ */
private computeOffsets(stepSize: number): Float32Array { #computeOffsets(stepSize: number): Float32Array {
const aspectRatio = this.canvas.width / this.canvas.height; const aspectRatio = this.#canvas.width / this.#canvas.height;
const offsets: number[] = []; const offsets: number[] = [];
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++) {
// Adjust x offset by aspect ratio to maintain uniform distances // Adjust x offset by aspect ratio to maintain uniform distances
offsets.push((x * stepSize * aspectRatio) / this.canvas.width, (y * stepSize) / this.canvas.height); offsets.push((x * stepSize * aspectRatio) / this.#canvas.width, (y * stepSize) / this.#canvas.height);
} }
} }
return new Float32Array(offsets); return new Float32Array(offsets);
@ -512,12 +511,12 @@ export class FolkDistanceField extends FolkBaseSet {
* Cleans up WebGL resources to prevent memory leaks. * Cleans up WebGL resources to prevent memory leaks.
* This is called when the element is disconnected from the DOM. * This is called when the element is disconnected from the DOM.
*/ */
private cleanupWebGLResources() { #cleanupWebGLResources() {
const gl = this.glContext; const gl = this.#glContext;
// Delete resources for each group // Delete resources for each group
for (const groupName in this.groups) { for (const groupName in this.#groups) {
const group = this.groups[groupName]; const group = this.#groups[groupName];
// Delete textures // Delete textures
group.textures.forEach((texture) => gl.deleteTexture(texture)); group.textures.forEach((texture) => gl.deleteTexture(texture));
@ -535,27 +534,27 @@ export class FolkDistanceField extends FolkBaseSet {
} }
// Delete framebuffer // Delete framebuffer
if (this.framebuffer) { if (this.#framebuffer) {
gl.deleteFramebuffer(this.framebuffer); gl.deleteFramebuffer(this.#framebuffer);
} }
// Delete fullscreen quad VAO // Delete fullscreen quad VAO
if (this.fullscreenQuadVAO) { if (this.#fullscreenQuadVAO) {
gl.deleteVertexArray(this.fullscreenQuadVAO); gl.deleteVertexArray(this.#fullscreenQuadVAO);
} }
// Delete shader programs // Delete shader programs
if (this.jfaProgram) { if (this.#jfaProgram) {
gl.deleteProgram(this.jfaProgram); gl.deleteProgram(this.#jfaProgram);
} }
if (this.renderProgram) { if (this.#renderProgram) {
gl.deleteProgram(this.renderProgram); gl.deleteProgram(this.#renderProgram);
} }
if (this.seedProgram) { if (this.#seedProgram) {
gl.deleteProgram(this.seedProgram); gl.deleteProgram(this.#seedProgram);
} }
this.groupBuffers = {}; this.#groupBuffers = {};
} }
} }

View File

@ -25,29 +25,29 @@ export class FolkSand extends FolkBaseSet {
`, `,
]; ];
private canvas = document.createElement('canvas'); #canvas = document.createElement('canvas');
private gl!: WebGL2RenderingContext; #gl!: WebGL2RenderingContext;
private program!: WebGLProgram; #program!: WebGLProgram;
private blitProgram!: WebGLProgram; #blitProgram!: WebGLProgram;
private jfaShadowProgram!: WebGLProgram; #jfaShadowProgram!: WebGLProgram;
private jfaInitProgram!: WebGLProgram; #jfaInitProgram!: WebGLProgram;
private vao!: WebGLVertexArrayObject; #vao!: WebGLVertexArrayObject;
private posBuffer!: WebGLBuffer; #posBuffer!: WebGLBuffer;
private bufferWidth!: number; #bufferWidth!: number;
private bufferHeight!: number; #bufferHeight!: number;
private fbo: WebGLFramebuffer[] = []; #fbo: WebGLFramebuffer[] = [];
private tex: WebGLTexture[] = []; #tex: WebGLTexture[] = [];
private shadowFbo: WebGLFramebuffer[] = []; #shadowFbo: WebGLFramebuffer[] = [];
private shadowTexR: WebGLTexture[] = []; #shadowTexR: WebGLTexture[] = [];
private shadowTexG: WebGLTexture[] = []; #shadowTexG: WebGLTexture[] = [];
private shadowTexB: WebGLTexture[] = []; #shadowTexB: WebGLTexture[] = [];
private pointer = { #pointer = {
x: -1, x: -1,
y: -1, y: -1,
prevX: -1, prevX: -1,
@ -55,88 +55,88 @@ export class FolkSand extends FolkBaseSet {
down: false, down: false,
}; };
private materialType = 4; #materialType = 4;
private brushRadius = 5; #brushRadius = 5;
private frames = 0; #frames = 0;
private swap = 0; #swap = 0;
private shadowSwap = 0; #shadowSwap = 0;
private PIXELS_PER_PARTICLE = 4; #PIXELS_PER_PARTICLE = 4;
private PIXEL_RATIO = window.devicePixelRatio || 1; #PIXEL_RATIO = window.devicePixelRatio || 1;
private collisionProgram!: WebGLProgram; #collisionProgram!: WebGLProgram;
private collisionFbo!: WebGLFramebuffer; #collisionFbo!: WebGLFramebuffer;
private collisionTex!: WebGLTexture; #collisionTex!: WebGLTexture;
private shapeVao!: WebGLVertexArrayObject; #shapeVao!: WebGLVertexArrayObject;
private shapePositionBuffer!: WebGLBuffer; #shapePositionBuffer!: WebGLBuffer;
private shapeIndexBuffer!: WebGLBuffer; #shapeIndexBuffer!: WebGLBuffer;
private shapeIndexCount = 0; #shapeIndexCount = 0;
onMaterialChange?: (type: number) => void; onMaterialChange?: (type: number) => void;
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.renderRoot.appendChild(this.canvas); this.renderRoot.appendChild(this.#canvas);
this.initializeWebGL(); this.#initializeWebGL();
this.initializeSimulation(); this.#initializeSimulation();
this.initializeCollisionDetection(); this.#initializeCollisionDetection();
this.attachEventListeners(); this.#attachEventListeners();
this.handleShapeTransform(); this.#handleShapeTransform();
this.render(); this.#render();
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.detachEventListeners(); this.#detachEventListeners();
} }
private initializeWebGL() { #initializeWebGL() {
this.gl = this.canvas.getContext('webgl2')!; this.#gl = this.#canvas.getContext('webgl2')!;
if (!this.gl) { if (!this.#gl) {
console.error('WebGL2 context not available!'); console.error('WebGL2 context not available!');
} }
if (!this.gl.getExtension('EXT_color_buffer_float')) { if (!this.#gl.getExtension('EXT_color_buffer_float')) {
console.error('need EXT_color_buffer_float'); console.error('need EXT_color_buffer_float');
} }
if (!this.gl.getExtension('OES_texture_float_linear')) { if (!this.#gl.getExtension('OES_texture_float_linear')) {
console.error('need OES_texture_float_linear'); console.error('need OES_texture_float_linear');
} }
} }
private initializeSimulation() { #initializeSimulation() {
// Create shaders and programs // Create shaders and programs
this.program = this.createProgramFromStrings({ this.#program = this.#createProgramFromStrings({
vertex: vertexShader, vertex: vertexShader,
fragment: simulationShader, fragment: simulationShader,
})!; })!;
this.blitProgram = this.createProgramFromStrings({ this.#blitProgram = this.#createProgramFromStrings({
vertex: vertexShader, vertex: vertexShader,
fragment: visualizationShader, fragment: visualizationShader,
})!; })!;
this.jfaShadowProgram = this.createProgramFromStrings({ this.#jfaShadowProgram = this.#createProgramFromStrings({
vertex: vertexShader, vertex: vertexShader,
fragment: distanceFieldPropagationShader, fragment: distanceFieldPropagationShader,
})!; })!;
this.jfaInitProgram = this.createProgramFromStrings({ this.#jfaInitProgram = this.#createProgramFromStrings({
vertex: vertexShader, vertex: vertexShader,
fragment: distanceFieldInitShader, fragment: distanceFieldInitShader,
})!; })!;
// Setup buffers and vertex arrays // Setup buffers and vertex arrays
this.setupBuffers(); this.#setupBuffers();
// Initialize framebuffers and textures // Initialize framebuffers and textures
this.initializeFramebuffers(); this.#initializeFramebuffers();
} }
private initializeCollisionDetection() { #initializeCollisionDetection() {
const gl = this.gl; const gl = this.#gl;
const collisionProgram = this.createProgramFromStrings({ const collisionProgram = this.#createProgramFromStrings({
vertex: collisionVertexShader, vertex: collisionVertexShader,
fragment: collisionFragmentShader, fragment: collisionFragmentShader,
}); });
@ -147,59 +147,59 @@ export class FolkSand extends FolkBaseSet {
} }
// Create collision shader program // Create collision shader program
this.collisionProgram = collisionProgram!; this.#collisionProgram = collisionProgram!;
// Create collision texture // Create collision texture
this.collisionTex = gl.createTexture()!; this.#collisionTex = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 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_WRAP_T, gl.CLAMP_TO_EDGE);
// Create collision framebuffer // Create collision framebuffer
this.collisionFbo = gl.createFramebuffer()!; this.#collisionFbo = gl.createFramebuffer()!;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#collisionFbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.collisionTex, 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#collisionTex, 0);
// Initialize shape buffers with larger initial sizes // Initialize shape buffers with larger initial sizes
this.shapeVao = gl.createVertexArray()!; this.#shapeVao = gl.createVertexArray()!;
gl.bindVertexArray(this.shapeVao); gl.bindVertexArray(this.#shapeVao);
this.shapePositionBuffer = gl.createBuffer()!; this.#shapePositionBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, this.#shapePositionBuffer);
// Allocate space for up to 100 shapes (400 vertices) // Allocate space for up to 100 shapes (400 vertices)
gl.bufferData(gl.ARRAY_BUFFER, 4 * 2 * 100 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, 4 * 2 * 100 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);
this.shapeIndexBuffer = gl.createBuffer()!; this.#shapeIndexBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#shapeIndexBuffer);
// Allocate space for up to 100 shapes (600 indices) // Allocate space for up to 100 shapes (600 indices)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 6 * 100 * Uint16Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 6 * 100 * Uint16Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);
// Set up vertex attributes // Set up vertex attributes
const posAttribLoc = gl.getAttribLocation(this.collisionProgram, 'aPosition'); const posAttribLoc = gl.getAttribLocation(this.#collisionProgram, 'aPosition');
gl.enableVertexAttribArray(posAttribLoc); gl.enableVertexAttribArray(posAttribLoc);
gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null); gl.bindVertexArray(null);
} }
private setupBuffers() { #setupBuffers() {
const gl = this.gl; const gl = this.#gl;
const quad = [-1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0]; const quad = [-1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0];
this.posBuffer = gl.createBuffer()!; this.#posBuffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(quad), gl.STATIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(quad), gl.STATIC_DRAW);
this.vao = gl.createVertexArray()!; this.#vao = gl.createVertexArray()!;
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.#vao);
const posAttribLoc = gl.getAttribLocation(this.program, 'aPosition'); const posAttribLoc = gl.getAttribLocation(this.#program, 'aPosition');
const uvAttribLoc = gl.getAttribLocation(this.program, 'aUv'); const uvAttribLoc = gl.getAttribLocation(this.#program, 'aUv');
gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 16, 0); gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(posAttribLoc); gl.enableVertexAttribArray(posAttribLoc);
@ -208,21 +208,21 @@ export class FolkSand extends FolkBaseSet {
gl.enableVertexAttribArray(uvAttribLoc); gl.enableVertexAttribArray(uvAttribLoc);
} }
private initializeFramebuffers() { #initializeFramebuffers() {
const gl = this.gl; const gl = this.#gl;
this.resizeCanvas(); this.#resizeCanvas();
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE); this.#bufferWidth = Math.ceil(gl.canvas.width / this.#PIXELS_PER_PARTICLE);
this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE); this.#bufferHeight = Math.ceil(gl.canvas.height / this.#PIXELS_PER_PARTICLE);
// Initialize framebuffers and textures for simulation // Initialize framebuffers and textures for simulation
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
// Create textures // Create textures
this.tex[i] = gl.createTexture()!; this.#tex[i] = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, this.tex[i]); gl.bindTexture(gl.TEXTURE_2D, this.#tex[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
@ -230,133 +230,133 @@ export class FolkSand extends FolkBaseSet {
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);
// Create framebuffers // Create framebuffers
this.fbo[i] = gl.createFramebuffer()!; this.#fbo[i] = gl.createFramebuffer()!;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[i]);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.tex[i], 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#tex[i], 0);
// Setup shadow textures // Setup shadow textures
this.shadowTexR[i] = gl.createTexture()!; this.#shadowTexR[i] = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 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_WRAP_T, gl.CLAMP_TO_EDGE);
this.shadowTexG[i] = gl.createTexture()!; this.#shadowTexG[i] = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 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_WRAP_T, gl.CLAMP_TO_EDGE);
this.shadowTexB[i] = gl.createTexture()!; this.#shadowTexB[i] = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 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_WRAP_T, gl.CLAMP_TO_EDGE);
// Setup shadow framebuffers // Setup shadow framebuffers
this.shadowFbo[i] = gl.createFramebuffer()!; this.#shadowFbo[i] = gl.createFramebuffer()!;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[i]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[i]);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.shadowTexR[i], 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#shadowTexR[i], 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.shadowTexG[i], 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.#shadowTexG[i], 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.shadowTexB[i], 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.#shadowTexB[i], 0);
// Set up draw buffers for the shadow FBO // Set up draw buffers for the shadow FBO
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]); gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
} }
} }
private attachEventListeners() { #attachEventListeners() {
this.canvas.addEventListener('pointerdown', this.handlePointerDown); this.#canvas.addEventListener('pointerdown', this.#handlePointerDown);
this.canvas.addEventListener('pointermove', this.handlePointerMove); this.#canvas.addEventListener('pointermove', this.#handlePointerMove);
this.canvas.addEventListener('pointerup', this.handlePointerUp); this.#canvas.addEventListener('pointerup', this.#handlePointerUp);
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.#handleKeyDown);
} }
private detachEventListeners() { #detachEventListeners() {
this.canvas.removeEventListener('pointerdown', this.handlePointerDown); this.#canvas.removeEventListener('pointerdown', this.#handlePointerDown);
this.canvas.removeEventListener('pointermove', this.handlePointerMove); this.#canvas.removeEventListener('pointermove', this.#handlePointerMove);
this.canvas.removeEventListener('pointerup', this.handlePointerUp); this.#canvas.removeEventListener('pointerup', this.#handlePointerUp);
document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keydown', this.#handleKeyDown);
} }
private handlePointerMove = (event: PointerEvent) => { #handlePointerMove = (event: PointerEvent) => {
const rect = this.canvas.getBoundingClientRect(); const rect = this.#canvas.getBoundingClientRect();
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
// Update previous position before setting new position // Update previous position before setting new position
this.pointer.prevX = this.pointer.x; this.#pointer.prevX = this.#pointer.x;
this.pointer.prevY = this.pointer.y; this.#pointer.prevY = this.#pointer.y;
// Scale coordinates relative to canvas size // Scale coordinates relative to canvas size
this.pointer.x = (x / rect.width) * this.canvas.width; this.#pointer.x = (x / rect.width) * this.#canvas.width;
this.pointer.y = (y / rect.height) * this.canvas.height; this.#pointer.y = (y / rect.height) * this.#canvas.height;
}; };
private handlePointerDown = (event: PointerEvent) => { #handlePointerDown = (event: PointerEvent) => {
const rect = this.canvas.getBoundingClientRect(); const rect = this.#canvas.getBoundingClientRect();
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
// Scale coordinates relative to canvas size // Scale coordinates relative to canvas size
this.pointer.x = (x / rect.width) * this.canvas.width; this.#pointer.x = (x / rect.width) * this.#canvas.width;
this.pointer.y = (y / rect.height) * this.canvas.height; this.#pointer.y = (y / rect.height) * this.#canvas.height;
this.pointer.prevX = this.pointer.x; this.#pointer.prevX = this.#pointer.x;
this.pointer.prevY = this.pointer.y; this.#pointer.prevY = this.#pointer.y;
this.pointer.down = true; this.#pointer.down = true;
}; };
private handlePointerUp = () => { #handlePointerUp = () => {
this.pointer.down = false; this.#pointer.down = false;
}; };
private handleKeyDown = (event: KeyboardEvent) => { #handleKeyDown = (event: KeyboardEvent) => {
const key = parseInt(event.key); const key = parseInt(event.key);
if (!isNaN(key)) { if (!isNaN(key)) {
this.setMaterialType(key); this.#setMaterialType(key);
} }
}; };
private setMaterialType(type: number) { #setMaterialType(type: number) {
this.materialType = Math.min(Math.max(type, 0), 9); this.#materialType = Math.min(Math.max(type, 0), 9);
this.onMaterialChange?.(this.materialType); this.onMaterialChange?.(this.#materialType);
} }
private resizeCanvas() { #resizeCanvas() {
const width = (this.canvas.clientWidth * this.PIXEL_RATIO) | 0; const width = (this.#canvas.clientWidth * this.#PIXEL_RATIO) | 0;
const height = (this.canvas.clientHeight * this.PIXEL_RATIO) | 0; const height = (this.#canvas.clientHeight * this.#PIXEL_RATIO) | 0;
if (this.canvas.width !== width || this.canvas.height !== height) { if (this.#canvas.width !== width || this.#canvas.height !== height) {
this.canvas.width = width; this.#canvas.width = width;
this.canvas.height = height; this.#canvas.height = height;
return true; return true;
} }
return false; return false;
} }
private render = (time: number = performance.now()) => { #render = (time: number = performance.now()) => {
if (this.resizeCanvas()) { if (this.#resizeCanvas()) {
this.processResize(); this.#processResize();
} }
this.simulationPass(time); this.#simulationPass(time);
this.shadowPass(); this.#shadowPass();
this.jfaPass(); this.#jfaPass();
this.renderPass(time); this.#renderPass(time);
this.pointer.prevX = this.pointer.x; this.#pointer.prevX = this.#pointer.x;
this.pointer.prevY = this.pointer.y; this.#pointer.prevY = this.#pointer.y;
requestAnimationFrame(this.render); requestAnimationFrame(this.#render);
}; };
private renderPass(time: number) { #renderPass(time: number) {
const gl = this.gl; const gl = this.#gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.drawBuffers([gl.BACK]); gl.drawBuffers([gl.BACK]);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
@ -364,127 +364,127 @@ export class FolkSand extends FolkBaseSet {
gl.clearColor(0, 0, 0, 1); gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(this.blitProgram); gl.useProgram(this.#blitProgram);
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.#vao);
const timeLoc = gl.getUniformLocation(this.blitProgram, 'time'); const timeLoc = gl.getUniformLocation(this.#blitProgram, 'time');
const resLoc = gl.getUniformLocation(this.blitProgram, 'resolution'); const resLoc = gl.getUniformLocation(this.#blitProgram, 'resolution');
const texLoc = gl.getUniformLocation(this.blitProgram, 'tex'); const texLoc = gl.getUniformLocation(this.#blitProgram, 'tex');
const shadowTexLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexR'); const shadowTexLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexR');
const shadowTexGLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexG'); const shadowTexGLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexG');
const shadowTexBLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexB'); const shadowTexBLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexB');
const scaleLoc = gl.getUniformLocation(this.blitProgram, 'scale'); const scaleLoc = gl.getUniformLocation(this.#blitProgram, 'scale');
const texResLoc = gl.getUniformLocation(this.blitProgram, 'texResolution'); const texResLoc = gl.getUniformLocation(this.#blitProgram, 'texResolution');
const texScaleLoc = gl.getUniformLocation(this.blitProgram, 'texScale'); const texScaleLoc = gl.getUniformLocation(this.#blitProgram, 'texScale');
gl.uniform1f(timeLoc, time * 0.001); gl.uniform1f(timeLoc, time * 0.001);
gl.uniform2f(resLoc, gl.canvas.width, gl.canvas.height); gl.uniform2f(resLoc, gl.canvas.width, gl.canvas.height);
gl.uniform2f(texResLoc, this.bufferWidth, this.bufferHeight); gl.uniform2f(texResLoc, this.#bufferWidth, this.#bufferHeight);
gl.uniform1f(texScaleLoc, this.PIXELS_PER_PARTICLE); gl.uniform1f(texScaleLoc, this.#PIXELS_PER_PARTICLE);
gl.uniform1f(scaleLoc, 1.0); gl.uniform1f(scaleLoc, 1.0);
gl.uniform1i(texLoc, 0); gl.uniform1i(texLoc, 0);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]); gl.bindTexture(gl.TEXTURE_2D, this.#tex[this.#swap]);
gl.uniform1i(shadowTexLoc, 1); gl.uniform1i(shadowTexLoc, 1);
gl.uniform1i(shadowTexGLoc, 2); gl.uniform1i(shadowTexGLoc, 2);
gl.uniform1i(shadowTexBLoc, 3); gl.uniform1i(shadowTexBLoc, 3);
gl.activeTexture(gl.TEXTURE1); gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[1 - this.#shadowSwap]);
gl.activeTexture(gl.TEXTURE2); gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[1 - this.#shadowSwap]);
gl.activeTexture(gl.TEXTURE3); gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[1 - this.#shadowSwap]);
gl.activeTexture(gl.TEXTURE4); gl.activeTexture(gl.TEXTURE4);
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
} }
private simulationPass(time: number) { #simulationPass(time: number) {
const gl = this.gl; const gl = this.#gl;
gl.useProgram(this.program); gl.useProgram(this.#program);
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.#vao);
const timeLoc = gl.getUniformLocation(this.program, 'time'); const timeLoc = gl.getUniformLocation(this.#program, 'time');
const frameLoc = gl.getUniformLocation(this.program, 'frame'); const frameLoc = gl.getUniformLocation(this.#program, 'frame');
const resLoc = gl.getUniformLocation(this.program, 'resolution'); const resLoc = gl.getUniformLocation(this.#program, 'resolution');
const texLoc = gl.getUniformLocation(this.program, 'tex'); const texLoc = gl.getUniformLocation(this.#program, 'tex');
const mouseLoc = gl.getUniformLocation(this.program, 'mouse'); const mouseLoc = gl.getUniformLocation(this.#program, 'mouse');
const materialTypeLoc = gl.getUniformLocation(this.program, 'materialType'); const materialTypeLoc = gl.getUniformLocation(this.#program, 'materialType');
const brushRadiusLoc = gl.getUniformLocation(this.program, 'brushRadius'); const brushRadiusLoc = gl.getUniformLocation(this.#program, 'brushRadius');
const collisionTexLoc = gl.getUniformLocation(this.program, 'u_collisionTex'); const collisionTexLoc = gl.getUniformLocation(this.#program, 'u_collisionTex');
if (!collisionTexLoc) { if (!collisionTexLoc) {
console.error('Could not find u_collisionTex uniform 1'); console.error('Could not find u_collisionTex uniform 1');
} }
if (collisionTexLoc !== null) { if (collisionTexLoc !== null) {
gl.uniform1i(collisionTexLoc, 5); // Use texture unit 5 gl.uniform1i(collisionTexLoc, 5); // Use texture unit 5
gl.activeTexture(gl.TEXTURE5); gl.activeTexture(gl.TEXTURE5);
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex);
} }
let mx = (this.pointer.x / gl.canvas.width) * this.bufferWidth; let mx = (this.#pointer.x / gl.canvas.width) * this.#bufferWidth;
let my = (1.0 - this.pointer.y / gl.canvas.height) * this.bufferHeight; let my = (1.0 - this.#pointer.y / gl.canvas.height) * this.#bufferHeight;
let mpx = (this.pointer.prevX / gl.canvas.width) * this.bufferWidth; let mpx = (this.#pointer.prevX / gl.canvas.width) * this.#bufferWidth;
let mpy = (1.0 - this.pointer.prevY / gl.canvas.height) * this.bufferHeight; let mpy = (1.0 - this.#pointer.prevY / gl.canvas.height) * this.#bufferHeight;
let pressed = false; let pressed = false;
gl.uniform1f(timeLoc, time * 0.001); gl.uniform1f(timeLoc, time * 0.001);
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight);
gl.uniform1i(materialTypeLoc, this.materialType); gl.uniform1i(materialTypeLoc, this.#materialType);
gl.uniform1f(brushRadiusLoc, this.brushRadius); gl.uniform1f(brushRadiusLoc, this.#brushRadius);
if (this.pointer.down || pressed) gl.uniform4f(mouseLoc, mx, my, mpx, mpy); if (this.#pointer.down || pressed) gl.uniform4f(mouseLoc, mx, my, mpx, mpy);
else gl.uniform4f(mouseLoc, -mx, -my, -mpx, -mpy); else gl.uniform4f(mouseLoc, -mx, -my, -mpx, -mpy);
const PASSES = 3; const PASSES = 3;
for (let i = 0; i < PASSES; i++) { for (let i = 0; i < PASSES; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[this.swap]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[this.#swap]);
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight);
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1i(frameLoc, this.frames * PASSES + i); gl.uniform1i(frameLoc, this.#frames * PASSES + i);
gl.uniform1i(texLoc, 0); gl.uniform1i(texLoc, 0);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex[1 - this.swap]); gl.bindTexture(gl.TEXTURE_2D, this.#tex[1 - this.#swap]);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
this.swap = 1 - this.swap; this.#swap = 1 - this.#swap;
} }
this.frames++; this.#frames++;
} }
private jfaPass() { #jfaPass() {
const gl = this.gl; const gl = this.#gl;
const JFA_PASSES = 5; const JFA_PASSES = 5;
gl.useProgram(this.jfaShadowProgram); gl.useProgram(this.#jfaShadowProgram);
const resLoc = gl.getUniformLocation(this.jfaShadowProgram, 'resolution'); const resLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'resolution');
const texLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texR'); const texLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texR');
const texGLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texG'); const texGLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texG');
const texBLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texB'); const texBLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texB');
const stepSizeLoc = gl.getUniformLocation(this.jfaShadowProgram, 'stepSize'); const stepSizeLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'stepSize');
const passCountLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passCount'); const passCountLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'passCount');
const passIdxLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passIndex'); const passIdxLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'passIndex');
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight);
gl.uniform1i(texLoc, 0); gl.uniform1i(texLoc, 0);
gl.uniform1i(texGLoc, 1); gl.uniform1i(texGLoc, 1);
gl.uniform1i(texBLoc, 2); gl.uniform1i(texBLoc, 2);
gl.uniform1i(passCountLoc, JFA_PASSES); gl.uniform1i(passCountLoc, JFA_PASSES);
for (let i = 0; i < JFA_PASSES; i++) { for (let i = 0; i < JFA_PASSES; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[this.#shadowSwap]);
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight);
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
@ -495,65 +495,65 @@ export class FolkSand extends FolkBaseSet {
gl.uniform1i(passIdxLoc, i); gl.uniform1i(passIdxLoc, i);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[1 - this.#shadowSwap]);
gl.activeTexture(gl.TEXTURE1); gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[1 - this.#shadowSwap]);
gl.activeTexture(gl.TEXTURE2); gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[1 - this.#shadowSwap]);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
this.shadowSwap = 1 - this.shadowSwap; this.#shadowSwap = 1 - this.#shadowSwap;
} }
} }
private shadowPass() { #shadowPass() {
const gl = this.gl; const gl = this.#gl;
this.shadowSwap = 0; this.#shadowSwap = 0;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[this.#shadowSwap]);
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight);
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(this.jfaInitProgram); gl.useProgram(this.#jfaInitProgram);
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.#vao);
const resLoc = gl.getUniformLocation(this.jfaInitProgram, 'resolution'); const resLoc = gl.getUniformLocation(this.#jfaInitProgram, 'resolution');
const texLoc = gl.getUniformLocation(this.jfaInitProgram, 'dataTex'); const texLoc = gl.getUniformLocation(this.#jfaInitProgram, 'dataTex');
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight);
gl.uniform1i(texLoc, 0); gl.uniform1i(texLoc, 0);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]); gl.bindTexture(gl.TEXTURE_2D, this.#tex[this.#swap]);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
this.shadowSwap = 1 - this.shadowSwap; this.#shadowSwap = 1 - this.#shadowSwap;
} }
private processResize() { #processResize() {
const gl = this.gl; const gl = this.#gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE); this.#bufferWidth = Math.ceil(gl.canvas.width / this.#PIXELS_PER_PARTICLE);
this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE); this.#bufferHeight = Math.ceil(gl.canvas.height / this.#PIXELS_PER_PARTICLE);
// Update collision texture size // Update collision texture size
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// Re-render collision data after resize // Re-render collision data after resize
this.handleShapeTransform(); this.#handleShapeTransform();
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[i]);
const newTex = gl.createTexture(); const newTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, newTex); gl.bindTexture(gl.TEXTURE_2D, newTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
@ -561,51 +561,51 @@ export class FolkSand extends FolkBaseSet {
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);
gl.bindTexture(gl.TEXTURE_2D, newTex); gl.bindTexture(gl.TEXTURE_2D, newTex);
gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, this.bufferWidth, this.bufferHeight); gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, this.#bufferWidth, this.#bufferHeight);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTex, 0); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTex, 0);
gl.deleteTexture(this.tex[i]); gl.deleteTexture(this.#tex[i]);
if (!this.tex[i]) { if (!this.#tex[i]) {
throw new Error('Failed to create texture1'); throw new Error('Failed to create texture1');
} }
if (!newTex) { if (!newTex) {
throw new Error('Failed to create texture2'); throw new Error('Failed to create texture2');
} }
this.tex[i] = newTex; this.#tex[i] = newTex;
} }
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[i]); gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[i]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null);
} }
} }
private createProgramFromStrings({ #createProgramFromStrings({
vertex, vertex,
fragment, fragment,
}: { }: {
vertex: string; vertex: string;
fragment: string; fragment: string;
}): WebGLProgram | undefined { }): WebGLProgram | undefined {
const vertexShader = WebGLUtils.createShader(this.gl, this.gl.VERTEX_SHADER, vertex); const vertexShader = WebGLUtils.createShader(this.#gl, this.#gl.VERTEX_SHADER, vertex);
const fragmentShader = WebGLUtils.createShader(this.gl, this.gl.FRAGMENT_SHADER, fragment); const fragmentShader = WebGLUtils.createShader(this.#gl, this.#gl.FRAGMENT_SHADER, fragment);
if (!vertexShader || !fragmentShader) { if (!vertexShader || !fragmentShader) {
console.error('Failed to create shaders'); console.error('Failed to create shaders');
return undefined; return undefined;
} }
return WebGLUtils.createProgram(this.gl, vertexShader, fragmentShader); return WebGLUtils.createProgram(this.#gl, vertexShader, fragmentShader);
} }
private collectShapeData() { #collectShapeData() {
const positions: number[] = []; const positions: number[] = [];
const indices: number[] = []; const indices: number[] = [];
let vertexOffset = 0; let vertexOffset = 0;
@ -626,7 +626,7 @@ export class FolkSand extends FolkBaseSet {
} }
// Convert the transformed points to buffer coordinates // Convert the transformed points to buffer coordinates
const bufferPoints = transformedPoints.map((point) => this.convertToBufferCoordinates(point.x, point.y)); const bufferPoints = transformedPoints.map((point) => this.#convertToBufferCoordinates(point.x, point.y));
// Add vertices // Add vertices
bufferPoints.forEach((point) => { bufferPoints.forEach((point) => {
@ -639,30 +639,30 @@ export class FolkSand extends FolkBaseSet {
vertexOffset += 4; vertexOffset += 4;
}); });
const gl = this.gl; const gl = this.#gl;
// Update buffers with new data // Update buffers with new data
gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, this.#shapePositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#shapeIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.DYNAMIC_DRAW); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.DYNAMIC_DRAW);
this.shapeIndexCount = indices.length; this.#shapeIndexCount = indices.length;
} }
private convertToBufferCoordinates(x: number, y: number) { #convertToBufferCoordinates(x: number, y: number) {
return { return {
x: (x / this.clientWidth) * 2 - 1, x: (x / this.clientWidth) * 2 - 1,
y: -((y / this.clientHeight) * 2 - 1), // Flip Y coordinate y: -((y / this.clientHeight) * 2 - 1), // Flip Y coordinate
}; };
} }
private updateCollisionTexture() { #updateCollisionTexture() {
const gl = this.gl; const gl = this.#gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo); gl.bindFramebuffer(gl.FRAMEBUFFER, this.#collisionFbo);
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight);
// Clear with transparent black (no collision) // Clear with transparent black (no collision)
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
@ -673,12 +673,12 @@ export class FolkSand extends FolkBaseSet {
gl.disable(gl.BLEND); gl.disable(gl.BLEND);
// Use collision shader program // Use collision shader program
gl.useProgram(this.collisionProgram); gl.useProgram(this.#collisionProgram);
gl.bindVertexArray(this.shapeVao); gl.bindVertexArray(this.#shapeVao);
// Draw all shapes // Draw all shapes
if (this.shapeIndexCount > 0) { if (this.#shapeIndexCount > 0) {
gl.drawElements(gl.TRIANGLES, this.shapeIndexCount, gl.UNSIGNED_SHORT, 0); gl.drawElements(gl.TRIANGLES, this.#shapeIndexCount, gl.UNSIGNED_SHORT, 0);
} }
// Cleanup // Cleanup
@ -691,13 +691,13 @@ export class FolkSand extends FolkBaseSet {
if (this.sourcesMap.size !== this.sourceElements.size) return; if (this.sourcesMap.size !== this.sourceElements.size) return;
this.handleShapeTransform(); this.#handleShapeTransform();
} }
private handleShapeTransform() { #handleShapeTransform() {
// Recollect and update all shape data when any shape changes // Recollect and update all shape data when any shape changes
// TODO: do this more piecemeal // TODO: do this more piecemeal
this.collectShapeData(); this.#collectShapeData();
this.updateCollisionTexture(); this.#updateCollisionTexture();
} }
} }