sand
This commit is contained in:
parent
b8738b485c
commit
5f7ff68c9f
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Distance Field Demo</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
folk-shape {
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
background-color: rgba(119, 119, 119, 1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
folk-sand {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<folk-sand>
|
||||
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="100" y="300" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="300" y="150" width="80" height="40"></folk-shape>
|
||||
<folk-shape x="400" y="250" width="60" height="90"></folk-shape>
|
||||
<folk-shape x="200" y="400" width="100" height="100"></folk-shape>
|
||||
<folk-shape x="500" y="100" width="30" height="70"></folk-shape>
|
||||
</folk-sand>
|
||||
|
||||
<script type="module">
|
||||
import '../src/standalone/folk-shape.ts';
|
||||
import '../src/standalone/folk-sand.ts';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { TransformEvent } from './src/common/TransformEvent';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
transform: TransformEvent;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,5 @@
|
|||
export const glsl = String.raw;
|
||||
|
||||
export const vert = String.raw;
|
||||
|
||||
export const frag = String.raw;
|
||||
|
||||
export const html = String.raw;
|
||||
|
||||
export function css(strings: TemplateStringsArray, ...values: any[]) {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ export class WebGLUtils {
|
|||
static createProgram(
|
||||
gl: WebGL2RenderingContext,
|
||||
vertexShader: WebGLShader,
|
||||
fragmentShader: WebGLShader
|
||||
fragmentShader?: WebGLShader
|
||||
): WebGLProgram {
|
||||
const program = gl.createProgram()!;
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
if (fragmentShader) {
|
||||
gl.attachShader(program, fragmentShader);
|
||||
}
|
||||
gl.linkProgram(program);
|
||||
|
||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { DOMRectTransform } from './common/DOMRectTransform.ts';
|
||||
import { frag, vert } from './common/tags.ts';
|
||||
import { Point } from './common/types.ts';
|
||||
import { glsl } from './common/tags.ts';
|
||||
import { WebGLUtils } from './common/webgl.ts';
|
||||
import { FolkBaseSet } from './folk-base-set.ts';
|
||||
import { FolkShape } from './folk-shape.ts';
|
||||
|
|
@ -466,7 +466,7 @@ export class FolkDistanceField extends FolkBaseSet {
|
|||
* Vertex shader shared by multiple programs.
|
||||
* Transforms vertices to normalized device coordinates and passes texture coordinates to the fragment shader.
|
||||
*/
|
||||
const commonVertShader = vert`#version 300 es
|
||||
const commonVertShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
in vec2 a_position;
|
||||
out vec2 v_texCoord;
|
||||
|
|
@ -480,7 +480,7 @@ void main() {
|
|||
* Fragment shader for the Jump Flooding Algorithm.
|
||||
* Updates the nearest seed point and distance for each pixel by examining neighboring pixels.
|
||||
*/
|
||||
const jfaFragShader = frag`#version 300 es
|
||||
const jfaFragShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
precision mediump int;
|
||||
|
||||
|
|
@ -524,7 +524,7 @@ void main() {
|
|||
* Fragment shader for rendering the final distance field.
|
||||
* Converts distances to colors for visualization.
|
||||
*/
|
||||
const renderFragShader = frag`#version 300 es
|
||||
const renderFragShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 v_texCoord;
|
||||
|
|
@ -559,7 +559,7 @@ void main() {
|
|||
* Vertex shader for rendering seed points.
|
||||
* Outputs the shape ID to the fragment shader.
|
||||
*/
|
||||
const seedVertShader = vert`#version 300 es
|
||||
const seedVertShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec3 a_position; // x, y position and shapeID as z
|
||||
|
|
@ -574,7 +574,7 @@ void main() {
|
|||
* Fragment shader for rendering seed points.
|
||||
* Initializes the texture with seed point positions and shape IDs.
|
||||
*/
|
||||
const seedFragShader = frag`#version 300 es
|
||||
const seedFragShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
flat in float v_shapeID;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,757 @@
|
|||
import { glsl } from './common/tags.ts';
|
||||
|
||||
/** Falling sand shaders using block cellular automata with Margolus offsets.
|
||||
* Based on "Probabilistic Cellular Automata for Granular Media in Video Games" (https://arxiv.org/abs/2008.06341)
|
||||
* Code adapted from https://github.com/GelamiSalami/GPU-Falling-Sand-CA
|
||||
*/
|
||||
|
||||
const CONSTANTS = glsl`
|
||||
#define AIR 0.0
|
||||
#define SMOKE 1.0
|
||||
#define WATER 2.0
|
||||
#define LAVA 3.0
|
||||
#define SAND 4.0
|
||||
#define PLANT 5.0
|
||||
#define STONE 6.0
|
||||
#define WALL 7.0
|
||||
#define COLLISION 99.0
|
||||
|
||||
const vec3 bgColor = pow(vec3(31, 34, 36) / 255.0, vec3(2));
|
||||
`;
|
||||
|
||||
const UTILS = glsl`
|
||||
|
||||
const float EPSILON = 1e-4;
|
||||
|
||||
const float PI = acos(-1.);
|
||||
const float TAU = PI * 2.0;
|
||||
|
||||
float safeacos(float x) { return acos(clamp(x, -1.0, 1.0)); }
|
||||
|
||||
float saturate(float x) { return clamp(x, 0., 1.); }
|
||||
vec2 saturate(vec2 x) { return clamp(x, vec2(0), vec2(1)); }
|
||||
vec3 saturate(vec3 x) { return clamp(x, vec3(0), vec3(1)); }
|
||||
|
||||
float sqr(float x) { return x*x; }
|
||||
vec2 sqr(vec2 x) { return x*x; }
|
||||
vec3 sqr(vec3 x) { return x*x; }
|
||||
|
||||
float luminance(vec3 col) { return dot(col, vec3(0.2126729, 0.7151522, 0.0721750)); }
|
||||
|
||||
mat2 rot2D(float a)
|
||||
{
|
||||
float c = cos(a);
|
||||
float s = sin(a);
|
||||
return mat2(c, s, -s, c);
|
||||
}
|
||||
|
||||
// https://iquilezles.org/articles/smin/
|
||||
float smin( float d1, float d2, float k ) {
|
||||
float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
|
||||
return mix( d2, d1, h ) - k*h*(1.0-h);
|
||||
}
|
||||
|
||||
float smax( float d1, float d2, float k ) {
|
||||
float h = clamp( 0.5 - 0.5*(d2-d1)/k, 0.0, 1.0 );
|
||||
return mix( d2, d1, h ) + k*h*(1.0-h);
|
||||
}
|
||||
|
||||
// https://iquilezles.org/articles/palettes/
|
||||
vec3 palette(float t)
|
||||
{
|
||||
return .5 + .5 * cos(TAU * (vec3(1, 1, 1) * t + vec3(0, .33, .67)));
|
||||
}
|
||||
|
||||
// Hash without Sine
|
||||
// https://www.shadertoy.com/view/4djSRW
|
||||
float hash12(vec2 p)
|
||||
{
|
||||
vec3 p3 = fract(vec3(p.xyx) * .1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
float hash13(vec3 p3)
|
||||
{
|
||||
p3 = fract(p3 * .1031);
|
||||
p3 += dot(p3, p3.zyx + 31.32);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
vec2 hash22(vec2 p)
|
||||
{
|
||||
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
|
||||
p3 += dot(p3, p3.yzx+33.33);
|
||||
return fract((p3.xx+p3.yz)*p3.zy);
|
||||
}
|
||||
|
||||
vec2 hash23(vec3 p3)
|
||||
{
|
||||
p3 = fract(p3 * vec3(.1031, .1030, .0973));
|
||||
p3 += dot(p3, p3.yzx+33.33);
|
||||
return fract((p3.xx+p3.yz)*p3.zy);
|
||||
}
|
||||
|
||||
vec3 hash33(vec3 p3)
|
||||
{
|
||||
p3 = fract(p3 * vec3(.1031, .1030, .0973));
|
||||
p3 += dot(p3, p3.yxz+33.33);
|
||||
return fract((p3.xxy + p3.yxx)*p3.zyx);
|
||||
}
|
||||
|
||||
vec4 hash43(vec3 p)
|
||||
{
|
||||
vec4 p4 = fract(vec4(p.xyzx) * vec4(.1031, .1030, .0973, .1099));
|
||||
p4 += dot(p4, p4.wzxy+33.33);
|
||||
return fract((p4.xxyz+p4.yzzw)*p4.zywx);
|
||||
}
|
||||
|
||||
// https://www.chilliant.com/rgb2hsv.html
|
||||
vec3 RGBtoHCV(in vec3 RGB)
|
||||
{
|
||||
// Based on work by Sam Hocevar and Emil Persson
|
||||
vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0);
|
||||
vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx);
|
||||
float C = Q.x - min(Q.w, Q.y);
|
||||
float H = abs((Q.w - Q.y) / (6.0 * C + EPSILON) + Q.z);
|
||||
return vec3(H, C, Q.x);
|
||||
}
|
||||
|
||||
vec3 RGBtoHSV(in vec3 RGB)
|
||||
{
|
||||
vec3 HCV = RGBtoHCV(RGB);
|
||||
float S = HCV.y / (HCV.z + EPSILON);
|
||||
return vec3(HCV.x, S, HCV.z);
|
||||
}
|
||||
|
||||
vec3 RGBtoHSL(in vec3 RGB)
|
||||
{
|
||||
vec3 HCV = RGBtoHCV(RGB);
|
||||
float L = HCV.z - HCV.y * 0.5;
|
||||
float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + EPSILON);
|
||||
return vec3(HCV.x, S, L);
|
||||
}
|
||||
|
||||
vec3 HUEtoRGB(in float H)
|
||||
{
|
||||
float R = abs(H * 6.0 - 3.0) - 1.0;
|
||||
float G = 2.0 - abs(H * 6.0 - 2.0);
|
||||
float B = 2.0 - abs(H * 6.0 - 4.0);
|
||||
return saturate(vec3(R,G,B));
|
||||
}
|
||||
|
||||
vec3 HSVtoRGB(in vec3 HSV)
|
||||
{
|
||||
vec3 RGB = HUEtoRGB(HSV.x);
|
||||
return ((RGB - 1.0) * HSV.y + 1.0) * HSV.z;
|
||||
}
|
||||
|
||||
vec3 HSLtoRGB(in vec3 HSL)
|
||||
{
|
||||
vec3 RGB = HUEtoRGB(HSL.x);
|
||||
float C = (1.0 - abs(2.0 * HSL.z - 1.0)) * HSL.y;
|
||||
return (RGB - 0.5) * C + HSL.z;
|
||||
}
|
||||
|
||||
vec3 sRGBToLinear(vec3 col)
|
||||
{
|
||||
return mix(pow((col + 0.055) / 1.055, vec3(2.4)), col / 12.92, lessThan(col, vec3(0.04045)));
|
||||
}
|
||||
|
||||
vec3 linearTosRGB(vec3 col)
|
||||
{
|
||||
return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308)));
|
||||
}
|
||||
`;
|
||||
|
||||
/** Vertex shader for rendering quads */
|
||||
export const vertexShader = glsl`#version 300 es
|
||||
in vec4 aPosition;
|
||||
in vec2 aUv;
|
||||
|
||||
out vec2 outUv;
|
||||
|
||||
void main() {
|
||||
gl_Position = aPosition;
|
||||
outUv = aUv;
|
||||
}
|
||||
`;
|
||||
|
||||
export const simulationShader = glsl`#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform float time;
|
||||
uniform int frame;
|
||||
uniform vec4 mouse;
|
||||
uniform int materialType;
|
||||
uniform float brushRadius;
|
||||
uniform sampler2D tex;
|
||||
uniform sampler2D u_collisionTex;
|
||||
|
||||
in vec2 outUv;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
${CONSTANTS}
|
||||
${UTILS}
|
||||
|
||||
// https://iquilezles.org/articles/distfunctions2d/
|
||||
float sdSegment(vec2 p, vec2 a, vec2 b)
|
||||
{
|
||||
vec2 pa = p-a, ba = b-a;
|
||||
float h = clamp( dot(pa,ba) / dot(ba,ba), 0.0, 1.0 );
|
||||
return length( pa - ba*h );
|
||||
}
|
||||
|
||||
ivec2 getOffset(int frame)
|
||||
{
|
||||
int i = frame % 4;
|
||||
if (i == 0)
|
||||
return ivec2(0, 0);
|
||||
else if (i == 1)
|
||||
return ivec2(1, 1);
|
||||
else if (i == 2)
|
||||
return ivec2(0, 1);
|
||||
return ivec2(1, 0);
|
||||
}
|
||||
|
||||
vec4 getData(ivec2 p)
|
||||
{
|
||||
// Check boundaries first
|
||||
if (p.x < 0 || p.y < 0 || p.x >= int(resolution.x) || p.y >= int(resolution.y)) {
|
||||
return vec4(vec3(0.02), WALL);
|
||||
}
|
||||
|
||||
// Calculate UV coordinates for the collision texture
|
||||
vec2 collisionUv = (vec2(p) + 0.5) / resolution;
|
||||
float collisionValue = texture(u_collisionTex, collisionUv).r;
|
||||
|
||||
// If there's a collision at this position, always return COLLISION type
|
||||
if (collisionValue > 0.5) {
|
||||
return vec4(bgColor, COLLISION);
|
||||
}
|
||||
|
||||
// If no collision, get the data from the simulation texture
|
||||
vec4 data = texelFetch(tex, p, 0);
|
||||
if (data.xyz == vec3(0)) {
|
||||
data.xyz = bgColor;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void swap(inout vec4 a, inout vec4 b)
|
||||
{
|
||||
vec4 tmp = a;
|
||||
a = b;
|
||||
b = tmp;
|
||||
}
|
||||
|
||||
vec4 createParticle(float id)
|
||||
{
|
||||
if (id == AIR)
|
||||
{
|
||||
return vec4(bgColor, AIR);
|
||||
} else if (id == SMOKE)
|
||||
{
|
||||
return vec4(mix(bgColor, vec3(0.15), 0.5), SMOKE);
|
||||
} else if (id == WATER)
|
||||
{
|
||||
return vec4(mix(bgColor, vec3(0.15, 0.45, 0.9), 0.7), WATER);
|
||||
} else if (id == LAVA)
|
||||
{
|
||||
vec3 r = hash33(vec3(gl_FragCoord.xy, frame));
|
||||
vec3 color = vec3(255, 40, 20) / 255.0;
|
||||
vec3 hsl = RGBtoHSL(color);
|
||||
hsl.x += (r.z - 0.5) * 12.0 / 255.0;
|
||||
hsl.y += (r.x - 0.5) * 16.0 / 255.0;
|
||||
hsl.z *= (r.y * 80.0 / 255.0 + (255.0 - 80.0) / 255.0);
|
||||
return vec4(HSLtoRGB(hsl), LAVA);
|
||||
} else if (id == SAND)
|
||||
{
|
||||
vec3 r = hash33(vec3(gl_FragCoord.xy, frame));
|
||||
vec3 color = vec3(220, 158, 70) / 255.0;
|
||||
vec3 hsl = RGBtoHSL(color);
|
||||
hsl.x += (r.z - 0.5) * 12.0 / 255.0;
|
||||
hsl.y += (r.x - 0.5) * 16.0 / 255.0;
|
||||
hsl.z += (r.y - 0.5) * 40.0 / 255.0;
|
||||
return vec4(HSLtoRGB(hsl), SAND);
|
||||
} else if (id == PLANT)
|
||||
{
|
||||
vec3 r = hash33(vec3(gl_FragCoord.xy, frame));
|
||||
vec3 color = vec3(34, 139, 34) / 255.0;
|
||||
vec3 hsl = RGBtoHSL(color);
|
||||
hsl.x += (r.z - 0.5) * 0.1;
|
||||
hsl.y += (r.x - 0.5) * 0.2;
|
||||
hsl.z *= (r.y * 0.4 + 0.6);
|
||||
return vec4(HSLtoRGB(hsl), PLANT);
|
||||
} else if (id == STONE)
|
||||
{
|
||||
float r = hash13(vec3(gl_FragCoord.xy, frame));
|
||||
return vec4(vec3(0.08, 0.1, 0.12) * (r * 0.5 + 0.5), STONE);
|
||||
} else if (id == WALL)
|
||||
{
|
||||
float r = hash13(vec3(gl_FragCoord.xy, frame));
|
||||
return vec4(bgColor * 0.5 * (r * 0.4 + 0.6), WALL);
|
||||
}
|
||||
return vec4(bgColor, AIR);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / resolution;
|
||||
|
||||
if (frame == 0) {
|
||||
float r = hash12(gl_FragCoord.xy);
|
||||
float id = AIR;
|
||||
if (r < 0.15)
|
||||
{
|
||||
id = SAND;
|
||||
} else if (r < 0.25)
|
||||
{
|
||||
id = SMOKE;
|
||||
}
|
||||
|
||||
fragColor = createParticle(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouse.x > 0.0)
|
||||
{
|
||||
float d = sdSegment(gl_FragCoord.xy, mouse.xy, mouse.zw);
|
||||
if (d < brushRadius)
|
||||
{
|
||||
fragColor = createParticle(float(materialType));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ivec2 offset = getOffset(frame);
|
||||
ivec2 fc = ivec2(gl_FragCoord.xy) + offset;
|
||||
ivec2 p = (fc / 2) * 2 - offset;
|
||||
ivec2 xy = fc % 2;
|
||||
int i = xy.x + xy.y * 2;
|
||||
|
||||
vec4 t00 = getData(p); // top-left
|
||||
vec4 t10 = getData(p + ivec2(1, 0)); // top-right
|
||||
vec4 t01 = getData(p + ivec2(0, 1)); // bottom-left
|
||||
vec4 t11 = getData(p + ivec2(1, 1)); // bottom-right
|
||||
|
||||
vec4 tn00 = getData(p + ivec2(0, -1));
|
||||
vec4 tn10 = getData(p + ivec2(1, -1));
|
||||
|
||||
if (t00.a == t10.a && t01.a == t11.a && t00.a == t01.a)
|
||||
{
|
||||
fragColor = i == 0 ? t00 :
|
||||
i == 1 ? t10 :
|
||||
i == 2 ? t01 : t11;
|
||||
return;
|
||||
}
|
||||
|
||||
vec4 r = hash43(vec3(p, frame));
|
||||
|
||||
if ((t01.a == SMOKE && t11.a < SMOKE ||
|
||||
t01.a < SMOKE && t11.a == SMOKE) && r.x < 0.25)
|
||||
{
|
||||
swap(t01, t11);
|
||||
}
|
||||
|
||||
if (t00.a == SMOKE)
|
||||
{
|
||||
if (t01.a < t00.a && r.y < 0.25)
|
||||
{
|
||||
swap(t00, t01);
|
||||
} else if (r.z < 0.003)
|
||||
{
|
||||
t00 = vec4(bgColor, AIR);
|
||||
}
|
||||
}
|
||||
if (t10.a == SMOKE)
|
||||
{
|
||||
if (t11.a < t10.a && r.y < 0.25)
|
||||
{
|
||||
swap(t10, t11);
|
||||
} else if (r.z < 0.003)
|
||||
{
|
||||
t10 = vec4(bgColor, AIR);
|
||||
}
|
||||
}
|
||||
|
||||
if (((t01.a == SAND) && t11.a < SAND ||
|
||||
t01.a < SAND && (t11.a == SAND)) &&
|
||||
t00.a < SAND && t10.a < SAND && r.x < 0.4)
|
||||
{
|
||||
swap(t01, t11);
|
||||
}
|
||||
|
||||
if (t01.a == SAND || t01.a == STONE)
|
||||
{
|
||||
if (t00.a < SAND)
|
||||
{
|
||||
if (r.y < 0.9) swap(t01, t00);
|
||||
} else if (t11.a < SAND && t10.a < SAND)
|
||||
{
|
||||
swap(t01, t10);
|
||||
}
|
||||
}
|
||||
|
||||
if (t11.a == SAND || t11.a == STONE)
|
||||
{
|
||||
if (t10.a < SAND)
|
||||
{
|
||||
if (r.y < 0.9) swap(t11, t10);
|
||||
} else if (t01.a < SAND && t00.a < SAND)
|
||||
{
|
||||
swap(t11, t00);
|
||||
}
|
||||
}
|
||||
|
||||
bool drop = false;
|
||||
if (t01.a == WATER)
|
||||
{
|
||||
if (t00.a < t01.a && r.y < 0.95)
|
||||
{
|
||||
swap(t01, t00);
|
||||
drop = true;
|
||||
} else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.3)
|
||||
{
|
||||
swap(t01, t10);
|
||||
drop = true;
|
||||
}
|
||||
}
|
||||
if (t11.a == WATER)
|
||||
{
|
||||
if (t10.a < t11.a && r.y < 0.95)
|
||||
{
|
||||
swap(t11, t10);
|
||||
drop = true;
|
||||
} else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.3)
|
||||
{
|
||||
swap(t11, t00);
|
||||
drop = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!drop)
|
||||
{
|
||||
if ((t01.a == WATER && t11.a < WATER ||
|
||||
t01.a < WATER && t11.a == WATER) &&
|
||||
(t00.a >= WATER && t10.a >= WATER || r.w < 0.8))
|
||||
{
|
||||
swap(t01, t11);
|
||||
}
|
||||
if ((t00.a == WATER && t10.a < WATER ||
|
||||
t00.a < WATER && t10.a == WATER) &&
|
||||
(tn00.a >= WATER && tn10.a >= WATER || r.w < 0.8))
|
||||
{
|
||||
swap(t00, t10);
|
||||
}
|
||||
}
|
||||
|
||||
if (t01.a == LAVA)
|
||||
{
|
||||
if (t00.a < t01.a && r.y < 0.8)
|
||||
{
|
||||
swap(t01, t00);
|
||||
} else if (t11.a < t01.a && t10.a < t01.a && r.z < 0.2)
|
||||
{
|
||||
swap(t01, t10);
|
||||
}
|
||||
}
|
||||
if (t11.a == LAVA)
|
||||
{
|
||||
if (t10.a < t11.a && r.y < 0.8)
|
||||
{
|
||||
swap(t11, t10);
|
||||
} else if (t01.a < t11.a && t00.a < t11.a && r.z < 0.2)
|
||||
{
|
||||
swap(t11, t00);
|
||||
}
|
||||
}
|
||||
|
||||
if (t00.a == LAVA)
|
||||
{
|
||||
if (t01.a == WATER)
|
||||
{
|
||||
t00 = createParticle(STONE);
|
||||
t01 = createParticle(SMOKE);
|
||||
} else if (t10.a == WATER)
|
||||
{
|
||||
t00 = createParticle(STONE);
|
||||
t10 = createParticle(SMOKE);
|
||||
} else if (t01.a == PLANT && r.x < 1.0) // left
|
||||
{
|
||||
t00 = createParticle(LAVA);
|
||||
t01 = createParticle(SMOKE);
|
||||
} else if (t10.a == PLANT && r.x < 1.0) // right
|
||||
{
|
||||
t00 = createParticle(LAVA);
|
||||
t10 = createParticle(SMOKE);
|
||||
} else if (t00.a == PLANT && r.x < 1.0) // bottom
|
||||
{
|
||||
t00 = createParticle(LAVA);
|
||||
t00 = createParticle(SMOKE);
|
||||
}
|
||||
}
|
||||
if (t10.a == LAVA)
|
||||
{
|
||||
if (t11.a == WATER)
|
||||
{
|
||||
t10 = createParticle(STONE);
|
||||
t11 = createParticle(SMOKE);
|
||||
} else if (t00.a == WATER)
|
||||
{
|
||||
t10 = createParticle(STONE);
|
||||
t00 = createParticle(SMOKE);
|
||||
}
|
||||
|
||||
else if (t11.a == PLANT)
|
||||
{
|
||||
t10 = createParticle(LAVA);
|
||||
t11 = createParticle(SMOKE);
|
||||
} else if (t00.a == PLANT)
|
||||
{
|
||||
t10 = createParticle(LAVA);
|
||||
t00 = createParticle(SMOKE);
|
||||
}
|
||||
}
|
||||
|
||||
if ((t01.a == LAVA && t11.a < LAVA ||
|
||||
t01.a < LAVA && t11.a == LAVA) && r.x < 0.6)
|
||||
{
|
||||
swap(t01, t11);
|
||||
}
|
||||
|
||||
|
||||
fragColor = i == 0 ? t00 :
|
||||
i == 1 ? t10 :
|
||||
i == 2 ? t01 : t11;
|
||||
|
||||
if (fragColor.a == COLLISION) {
|
||||
vec2 collisionUv = gl_FragCoord.xy / resolution;
|
||||
float collisionValue = texture(u_collisionTex, collisionUv).r;
|
||||
if (collisionValue <= 0.5) {
|
||||
fragColor = vec4(bgColor, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const distanceFieldInitShader = glsl`#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform sampler2D dataTex;
|
||||
|
||||
${CONSTANTS}
|
||||
|
||||
layout(location = 0) out vec4 fragColorR;
|
||||
layout(location = 1) out vec4 fragColorG;
|
||||
layout(location = 2) out vec4 fragColorB;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 uv = gl_FragCoord.xy / resolution;
|
||||
|
||||
vec4 data = texture(dataTex, uv);
|
||||
|
||||
fragColorR = vec4(-1, -1, 0, 0);
|
||||
fragColorG = vec4(-1, -1, 0, 0);
|
||||
fragColorB = vec4(-1, -1, 0, 0);
|
||||
|
||||
if (data.a <= LAVA)
|
||||
{
|
||||
fragColorR.xy = gl_FragCoord.xy;
|
||||
fragColorG.xy = gl_FragCoord.xy;
|
||||
fragColorB.xy = gl_FragCoord.xy;
|
||||
}
|
||||
if (data.a == SMOKE)
|
||||
{
|
||||
fragColorR.w = 6.0;
|
||||
fragColorG.w = 6.0;
|
||||
fragColorB.w = 6.0;
|
||||
} else if (data.a == WATER)
|
||||
{
|
||||
fragColorR.w = 9.0;
|
||||
fragColorG.w = 6.0;
|
||||
fragColorB.w = 4.0;
|
||||
} else if (data.a == LAVA)
|
||||
{
|
||||
fragColorR.w = 0.0;
|
||||
fragColorG.w = 11.0;
|
||||
fragColorB.w = 14.0;
|
||||
} else if (data.a == PLANT)
|
||||
{
|
||||
fragColorR.w = 4.0 - data.r * 4.0;
|
||||
fragColorG.w = 4.0 - data.g * 4.0;
|
||||
fragColorB.w = 4.0 - data.b * 4.0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const distanceFieldPropagationShader = glsl`#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform float stepSize;
|
||||
uniform vec2 resolution;
|
||||
uniform sampler2D texR;
|
||||
uniform sampler2D texG;
|
||||
uniform sampler2D texB;
|
||||
|
||||
uniform int passCount;
|
||||
uniform int passIndex;
|
||||
|
||||
layout(location = 0) out vec4 fragColorR;
|
||||
layout(location = 1) out vec4 fragColorG;
|
||||
layout(location = 2) out vec4 fragColorB;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 fc = gl_FragCoord.xy;
|
||||
|
||||
vec4 bestR = vec4(0,0,1e3,0);
|
||||
vec4 bestG = vec4(0,0,1e3,0);
|
||||
vec4 bestB = vec4(0,0,1e3,0);
|
||||
|
||||
for (int x = -1; x <= 1; x++)
|
||||
{
|
||||
for (int y = -1; y <= 1; y++)
|
||||
{
|
||||
vec2 p = fc + vec2(x, y) * stepSize;
|
||||
|
||||
vec4 dataR = texture(texR, p / resolution);
|
||||
vec4 dataG = texture(texG, p / resolution);
|
||||
vec4 dataB = texture(texB, p / resolution);
|
||||
|
||||
if (dataR.xy != vec2(-1) && dataR.xy == clamp(dataR.xy, vec2(0.5), resolution-0.5))
|
||||
{
|
||||
float dist = distance(fc, dataR.xy) + dataR.w;
|
||||
if (dist < bestR.z)
|
||||
{
|
||||
bestR = dataR;
|
||||
bestR.z = dist;
|
||||
}
|
||||
}
|
||||
if (dataG.xy != vec2(-1) && dataG.xy == clamp(dataG.xy, vec2(0.5), resolution-0.5))
|
||||
{
|
||||
float dist = distance(fc, dataG.xy) + dataG.w;
|
||||
if (dist < bestG.z)
|
||||
{
|
||||
bestG = dataG;
|
||||
bestG.z = dist;
|
||||
}
|
||||
}
|
||||
if (dataB.xy != vec2(-1) && dataB.xy == clamp(dataB.xy, vec2(0.5), resolution-0.5))
|
||||
{
|
||||
float dist = distance(fc, dataB.xy) + dataB.w;
|
||||
if (dist < bestB.z)
|
||||
{
|
||||
bestB = dataB;
|
||||
bestB.z = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragColorR = vec4(bestR.xy, bestR.z != 1e3 ? bestR.z : 1e3, bestR.w);
|
||||
fragColorG = vec4(bestG.xy, bestG.z != 1e3 ? bestG.z : 1e3, bestG.w);
|
||||
fragColorB = vec4(bestB.xy, bestB.z != 1e3 ? bestB.z : 1e3, bestB.w);
|
||||
|
||||
if (passIndex == passCount - 1)
|
||||
{
|
||||
if (bestR.xy == vec2(-1))
|
||||
fragColorR.z = 1e3;
|
||||
if (bestG.xy == vec2(-1))
|
||||
fragColorG.z = 1e3;
|
||||
if (bestB.xy == vec2(-1))
|
||||
fragColorB.z = 1e3;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const visualizationShader = glsl`#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 texResolution;
|
||||
uniform float texScale;
|
||||
uniform vec2 resolution;
|
||||
uniform sampler2D tex;
|
||||
uniform sampler2D shadowTexR;
|
||||
uniform sampler2D shadowTexG;
|
||||
uniform sampler2D shadowTexB;
|
||||
uniform sampler2D u_collisionTex;
|
||||
uniform float scale;
|
||||
|
||||
${CONSTANTS}
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
vec2 getCoordsAA(vec2 uv)
|
||||
{
|
||||
float w = 1.5; // 1.5
|
||||
vec2 fl = floor(uv + 0.5);
|
||||
vec2 fr = fract(uv + 0.5);
|
||||
vec2 aa = fwidth(uv) * w * 0.5;
|
||||
fr = smoothstep(0.5 - aa, 0.5 + aa, fr);
|
||||
|
||||
return fl + fr - 0.5;
|
||||
}
|
||||
|
||||
vec3 linearTosRGB(vec3 col)
|
||||
{
|
||||
return mix(1.055 * pow(col, vec3(1.0 / 2.4)) - 0.055, col * 12.92, lessThan(col, vec3(0.0031308)));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / (texResolution * texScale);
|
||||
|
||||
uv -= 0.5;
|
||||
uv *= scale;
|
||||
uv += 0.5;
|
||||
|
||||
vec2 fc = uv * texResolution;
|
||||
|
||||
vec4 data = texture(tex, getCoordsAA(fc) / texResolution);
|
||||
vec4 dataUp = texture(tex, getCoordsAA(fc + vec2(0, 1)) / texResolution);
|
||||
vec4 dataDown = texture(tex, getCoordsAA(fc - vec2(0, 1)) / texResolution);
|
||||
|
||||
float hig = float(data.a > dataUp.a);
|
||||
float dropSha = 1.0 - float(data.a > dataDown.a);
|
||||
|
||||
vec3 color = data.rgb == vec3(0) ? bgColor : data.rgb;
|
||||
|
||||
vec4 shaDataR = texture(shadowTexR, uv);
|
||||
vec4 shaDataG = texture(shadowTexG, uv);
|
||||
vec4 shaDataB = texture(shadowTexB, uv);
|
||||
|
||||
float shaR = shaDataR.xy != vec2(-1) ? shaDataR.z : 16.0;
|
||||
float shaG = shaDataG.xy != vec2(-1) ? shaDataG.z : 16.0;
|
||||
float shaB = shaDataB.xy != vec2(-1) ? shaDataB.z : 16.0;
|
||||
|
||||
vec3 sha = clamp(1.0 - vec3(shaR, shaG, shaB) / 16.0, vec3(0.0), vec3(1.0));
|
||||
sha *= sha;
|
||||
|
||||
color *= 0.5 * max(hig, dropSha) + 0.5;
|
||||
color *= sha * 1.0 + 0.2;
|
||||
color += color * 0.4 * hig;
|
||||
|
||||
fragColor = vec4(linearTosRGB(color), 1);
|
||||
}
|
||||
`;
|
||||
|
||||
export const collisionVertexShader = glsl`#version 300 es
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) in vec2 aPosition;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||
}`;
|
||||
|
||||
export const collisionFragmentShader = glsl`#version 300 es
|
||||
precision highp float;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0); // red represents solid
|
||||
}`;
|
||||
|
|
@ -0,0 +1,712 @@
|
|||
import { WebGLUtils } from './common/webgl';
|
||||
import {
|
||||
collisionFragmentShader,
|
||||
collisionVertexShader,
|
||||
simulationShader,
|
||||
distanceFieldInitShader,
|
||||
distanceFieldPropagationShader,
|
||||
visualizationShader,
|
||||
vertexShader,
|
||||
} from './folk-sand.glsl.ts';
|
||||
import { FolkShape } from './folk-shape.ts';
|
||||
|
||||
export class FolkSand extends HTMLElement {
|
||||
static tagName = 'folk-sand';
|
||||
|
||||
private canvas!: HTMLCanvasElement;
|
||||
private gl!: WebGL2RenderingContext;
|
||||
|
||||
private program!: WebGLProgram;
|
||||
private blitProgram!: WebGLProgram;
|
||||
private jfaShadowProgram!: WebGLProgram;
|
||||
private jfaInitProgram!: WebGLProgram;
|
||||
|
||||
private vao!: WebGLVertexArrayObject;
|
||||
private posBuffer!: WebGLBuffer;
|
||||
|
||||
private bufferWidth!: number;
|
||||
private bufferHeight!: number;
|
||||
|
||||
private fbo: WebGLFramebuffer[] = [];
|
||||
private tex: WebGLTexture[] = [];
|
||||
|
||||
private shadowFbo: WebGLFramebuffer[] = [];
|
||||
private shadowTexR: WebGLTexture[] = [];
|
||||
private shadowTexG: WebGLTexture[] = [];
|
||||
private shadowTexB: WebGLTexture[] = [];
|
||||
|
||||
private pointer = {
|
||||
x: -1,
|
||||
y: -1,
|
||||
prevX: -1,
|
||||
prevY: -1,
|
||||
down: false,
|
||||
};
|
||||
|
||||
private materialType = 4;
|
||||
private brushRadius = 5;
|
||||
|
||||
private shapes: NodeListOf<FolkShape> = document.querySelectorAll('folk-shape');
|
||||
|
||||
private frames = 0;
|
||||
private swap = 0;
|
||||
private shadowSwap = 0;
|
||||
|
||||
private PIXELS_PER_PARTICLE = 4;
|
||||
private PIXEL_RATIO = window.devicePixelRatio || 1;
|
||||
|
||||
private collisionProgram!: WebGLProgram;
|
||||
private collisionFbo!: WebGLFramebuffer;
|
||||
private collisionTex!: WebGLTexture;
|
||||
private shapeVao!: WebGLVertexArrayObject;
|
||||
private shapePositionBuffer!: WebGLBuffer;
|
||||
private shapeIndexBuffer!: WebGLBuffer;
|
||||
private shapeIndexCount = 0;
|
||||
|
||||
static define() {
|
||||
if (customElements.get(this.tagName)) return;
|
||||
FolkShape.define();
|
||||
customElements.define(this.tagName, this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.setupCanvas();
|
||||
this.initializeWebGL();
|
||||
this.initializeSimulation();
|
||||
this.initializeCollisionDetection();
|
||||
|
||||
// Collect all FolkShape elements
|
||||
this.shapes = document.querySelectorAll('folk-shape');
|
||||
|
||||
// Attach event listeners to shapes
|
||||
this.shapes.forEach((shape) => {
|
||||
shape.addEventListener('transform', this.handleShapeTransform);
|
||||
});
|
||||
|
||||
// Initialize collision texture with current shapes
|
||||
this.collectShapeData();
|
||||
this.updateCollisionTexture();
|
||||
|
||||
this.attachEventListeners();
|
||||
requestAnimationFrame(this.render.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.detachEventListeners();
|
||||
|
||||
// Remove event listeners from shapes
|
||||
this.shapes.forEach((shape) => {
|
||||
shape.removeEventListener('transform', this.handleShapeTransform);
|
||||
});
|
||||
}
|
||||
|
||||
private setupCanvas() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.id = 'main-canvas';
|
||||
this.canvas.style.width = '100%';
|
||||
this.canvas.style.height = '100%';
|
||||
this.canvas.style.display = 'block';
|
||||
this.style.display = 'block';
|
||||
this.style.width = '100%';
|
||||
this.style.height = '100%';
|
||||
this.appendChild(this.canvas);
|
||||
}
|
||||
|
||||
private initializeWebGL() {
|
||||
this.gl = this.canvas.getContext('webgl2')!;
|
||||
if (!this.gl) {
|
||||
console.error('WebGL2 context not available!');
|
||||
}
|
||||
|
||||
if (!this.gl.getExtension('EXT_color_buffer_float')) {
|
||||
console.error('need EXT_color_buffer_float');
|
||||
}
|
||||
|
||||
if (!this.gl.getExtension('OES_texture_float_linear')) {
|
||||
console.error('need OES_texture_float_linear');
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSimulation() {
|
||||
const gl = this.gl;
|
||||
|
||||
// Create shaders and programs
|
||||
this.program = this.createProgramFromStrings({
|
||||
vertex: vertexShader,
|
||||
fragment: simulationShader,
|
||||
})!;
|
||||
this.blitProgram = this.createProgramFromStrings({
|
||||
vertex: vertexShader,
|
||||
fragment: visualizationShader,
|
||||
})!;
|
||||
this.jfaShadowProgram = this.createProgramFromStrings({
|
||||
vertex: vertexShader,
|
||||
fragment: distanceFieldPropagationShader,
|
||||
})!;
|
||||
this.jfaInitProgram = this.createProgramFromStrings({
|
||||
vertex: vertexShader,
|
||||
fragment: distanceFieldInitShader,
|
||||
})!;
|
||||
|
||||
// Setup buffers and vertex arrays
|
||||
this.setupBuffers();
|
||||
|
||||
// Initialize framebuffers and textures
|
||||
this.initializeFramebuffers();
|
||||
}
|
||||
|
||||
private initializeCollisionDetection() {
|
||||
const gl = this.gl;
|
||||
|
||||
const collisionProgram = this.createProgramFromStrings({
|
||||
vertex: collisionVertexShader,
|
||||
fragment: collisionFragmentShader,
|
||||
});
|
||||
|
||||
if (!collisionProgram) {
|
||||
console.error('Failed to create collision program');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create collision shader program
|
||||
this.collisionProgram = collisionProgram!;
|
||||
|
||||
// Create collision texture
|
||||
this.collisionTex = gl.createTexture()!;
|
||||
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.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_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
// Create collision framebuffer
|
||||
this.collisionFbo = gl.createFramebuffer()!;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.collisionTex, 0);
|
||||
|
||||
// Initialize shape buffers with larger initial sizes
|
||||
this.shapeVao = gl.createVertexArray()!;
|
||||
gl.bindVertexArray(this.shapeVao);
|
||||
|
||||
this.shapePositionBuffer = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer);
|
||||
// Allocate space for up to 100 shapes (400 vertices)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, 4 * 2 * 100 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);
|
||||
|
||||
this.shapeIndexBuffer = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer);
|
||||
// Allocate space for up to 100 shapes (600 indices)
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 6 * 100 * Uint16Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);
|
||||
|
||||
// Set up vertex attributes
|
||||
const posAttribLoc = gl.getAttribLocation(this.collisionProgram, 'aPosition');
|
||||
gl.enableVertexAttribArray(posAttribLoc);
|
||||
gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
// Initial collection and render of shape data
|
||||
this.collectShapeData();
|
||||
this.updateCollisionTexture();
|
||||
}
|
||||
|
||||
private setupBuffers() {
|
||||
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];
|
||||
|
||||
this.posBuffer = gl.createBuffer()!;
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(quad), gl.STATIC_DRAW);
|
||||
|
||||
this.vao = gl.createVertexArray()!;
|
||||
|
||||
gl.bindVertexArray(this.vao);
|
||||
|
||||
const posAttribLoc = gl.getAttribLocation(this.program, 'aPosition');
|
||||
const uvAttribLoc = gl.getAttribLocation(this.program, 'aUv');
|
||||
|
||||
gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 16, 0);
|
||||
gl.enableVertexAttribArray(posAttribLoc);
|
||||
|
||||
gl.vertexAttribPointer(uvAttribLoc, 2, gl.FLOAT, false, 16, 8);
|
||||
gl.enableVertexAttribArray(uvAttribLoc);
|
||||
}
|
||||
|
||||
private initializeFramebuffers() {
|
||||
const gl = this.gl;
|
||||
|
||||
this.resizeCanvas();
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE);
|
||||
this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE);
|
||||
|
||||
// Initialize framebuffers and textures for simulation
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// Create textures
|
||||
this.tex[i] = gl.createTexture()!;
|
||||
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
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);
|
||||
|
||||
// Create framebuffers
|
||||
this.fbo[i] = gl.createFramebuffer()!;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.tex[i], 0);
|
||||
|
||||
// Setup shadow textures
|
||||
this.shadowTexR[i] = gl.createTexture()!;
|
||||
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
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);
|
||||
|
||||
this.shadowTexG[i] = gl.createTexture()!;
|
||||
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
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);
|
||||
|
||||
this.shadowTexB[i] = gl.createTexture()!;
|
||||
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
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);
|
||||
|
||||
// Setup shadow framebuffers
|
||||
this.shadowFbo[i] = gl.createFramebuffer()!;
|
||||
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_ATTACHMENT1, gl.TEXTURE_2D, this.shadowTexG[i], 0);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.shadowTexB[i], 0);
|
||||
|
||||
// Set up draw buffers for the shadow FBO
|
||||
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
|
||||
}
|
||||
}
|
||||
|
||||
private attachEventListeners() {
|
||||
this.handlePointerDown = this.handlePointerDown.bind(this);
|
||||
this.handlePointerMove = this.handlePointerMove.bind(this);
|
||||
this.handlePointerUp = this.handlePointerUp.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
|
||||
this.canvas.addEventListener('pointerdown', this.handlePointerDown);
|
||||
this.canvas.addEventListener('pointermove', this.handlePointerMove);
|
||||
this.canvas.addEventListener('pointerup', this.handlePointerUp);
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private detachEventListeners() {
|
||||
this.canvas.removeEventListener('pointerdown', this.handlePointerDown);
|
||||
this.canvas.removeEventListener('pointermove', this.handlePointerMove);
|
||||
this.canvas.removeEventListener('pointerup', this.handlePointerUp);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handlePointerMove(event: PointerEvent) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Update previous position before setting new position
|
||||
this.pointer.prevX = this.pointer.x;
|
||||
this.pointer.prevY = this.pointer.y;
|
||||
|
||||
// Scale coordinates relative to canvas size
|
||||
this.pointer.x = (x / rect.width) * this.canvas.width;
|
||||
this.pointer.y = (y / rect.height) * this.canvas.height;
|
||||
}
|
||||
|
||||
private handlePointerDown(event: PointerEvent) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Scale coordinates relative to canvas size
|
||||
this.pointer.x = (x / rect.width) * this.canvas.width;
|
||||
this.pointer.y = (y / rect.height) * this.canvas.height;
|
||||
this.pointer.prevX = this.pointer.x;
|
||||
this.pointer.prevY = this.pointer.y;
|
||||
this.pointer.down = true;
|
||||
}
|
||||
|
||||
private handlePointerUp() {
|
||||
this.pointer.down = false;
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const key = parseInt(event.key);
|
||||
if (!isNaN(key)) {
|
||||
this.setMaterialType(key);
|
||||
}
|
||||
}
|
||||
|
||||
private setMaterialType(type: number) {
|
||||
this.materialType = Math.min(Math.max(type, 0), 9);
|
||||
}
|
||||
|
||||
private resizeCanvas() {
|
||||
const width = (this.canvas.clientWidth * this.PIXEL_RATIO) | 0;
|
||||
const height = (this.canvas.clientHeight * this.PIXEL_RATIO) | 0;
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private render(time: number) {
|
||||
if (this.resizeCanvas()) {
|
||||
this.processResize();
|
||||
}
|
||||
|
||||
this.simulationPass(time);
|
||||
this.shadowPass();
|
||||
this.jfaPass();
|
||||
this.renderPass(time);
|
||||
|
||||
this.pointer.prevX = this.pointer.x;
|
||||
this.pointer.prevY = this.pointer.y;
|
||||
|
||||
requestAnimationFrame(this.render.bind(this));
|
||||
}
|
||||
|
||||
private renderPass(time: number) {
|
||||
const gl = this.gl;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.drawBuffers([gl.BACK]);
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(this.blitProgram);
|
||||
gl.bindVertexArray(this.vao);
|
||||
|
||||
const timeLoc = gl.getUniformLocation(this.blitProgram, 'time');
|
||||
const resLoc = gl.getUniformLocation(this.blitProgram, 'resolution');
|
||||
const texLoc = gl.getUniformLocation(this.blitProgram, 'tex');
|
||||
const shadowTexLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexR');
|
||||
const shadowTexGLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexG');
|
||||
const shadowTexBLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexB');
|
||||
const scaleLoc = gl.getUniformLocation(this.blitProgram, 'scale');
|
||||
const texResLoc = gl.getUniformLocation(this.blitProgram, 'texResolution');
|
||||
const texScaleLoc = gl.getUniformLocation(this.blitProgram, 'texScale');
|
||||
|
||||
gl.uniform1f(timeLoc, time * 0.001);
|
||||
gl.uniform2f(resLoc, gl.canvas.width, gl.canvas.height);
|
||||
gl.uniform2f(texResLoc, this.bufferWidth, this.bufferHeight);
|
||||
gl.uniform1f(texScaleLoc, this.PIXELS_PER_PARTICLE);
|
||||
gl.uniform1f(scaleLoc, 1.0);
|
||||
|
||||
gl.uniform1i(texLoc, 0);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]);
|
||||
|
||||
gl.uniform1i(shadowTexLoc, 1);
|
||||
gl.uniform1i(shadowTexGLoc, 2);
|
||||
gl.uniform1i(shadowTexBLoc, 3);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]);
|
||||
gl.activeTexture(gl.TEXTURE2);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]);
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
}
|
||||
|
||||
private simulationPass(time: number) {
|
||||
const gl = this.gl;
|
||||
gl.useProgram(this.program);
|
||||
gl.bindVertexArray(this.vao);
|
||||
|
||||
const timeLoc = gl.getUniformLocation(this.program, 'time');
|
||||
const frameLoc = gl.getUniformLocation(this.program, 'frame');
|
||||
const resLoc = gl.getUniformLocation(this.program, 'resolution');
|
||||
const texLoc = gl.getUniformLocation(this.program, 'tex');
|
||||
const mouseLoc = gl.getUniformLocation(this.program, 'mouse');
|
||||
const materialTypeLoc = gl.getUniformLocation(this.program, 'materialType');
|
||||
const brushRadiusLoc = gl.getUniformLocation(this.program, 'brushRadius');
|
||||
const collisionTexLoc = gl.getUniformLocation(this.program, 'u_collisionTex');
|
||||
if (!collisionTexLoc) {
|
||||
console.error('Could not find u_collisionTex uniform 1');
|
||||
}
|
||||
if (collisionTexLoc !== null) {
|
||||
gl.uniform1i(collisionTexLoc, 5); // Use texture unit 5
|
||||
gl.activeTexture(gl.TEXTURE5);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.collisionTex);
|
||||
}
|
||||
|
||||
let mx = (this.pointer.x / gl.canvas.width) * this.bufferWidth;
|
||||
let my = (1.0 - this.pointer.y / gl.canvas.height) * this.bufferHeight;
|
||||
let mpx = (this.pointer.prevX / gl.canvas.width) * this.bufferWidth;
|
||||
let mpy = (1.0 - this.pointer.prevY / gl.canvas.height) * this.bufferHeight;
|
||||
|
||||
let pressed = false;
|
||||
|
||||
gl.uniform1f(timeLoc, time * 0.001);
|
||||
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight);
|
||||
gl.uniform1i(materialTypeLoc, this.materialType);
|
||||
gl.uniform1f(brushRadiusLoc, this.brushRadius);
|
||||
|
||||
if (this.pointer.down || pressed) gl.uniform4f(mouseLoc, mx, my, mpx, mpy);
|
||||
else gl.uniform4f(mouseLoc, -mx, -my, -mpx, -mpy);
|
||||
|
||||
const PASSES = 3;
|
||||
for (let i = 0; i < PASSES; i++) {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[this.swap]);
|
||||
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight);
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.uniform1i(frameLoc, this.frames * PASSES + i);
|
||||
|
||||
gl.uniform1i(texLoc, 0);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.tex[1 - this.swap]);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
|
||||
this.swap = 1 - this.swap;
|
||||
}
|
||||
|
||||
this.frames++;
|
||||
}
|
||||
|
||||
private jfaPass() {
|
||||
const gl = this.gl;
|
||||
const JFA_PASSES = 5;
|
||||
|
||||
gl.useProgram(this.jfaShadowProgram);
|
||||
|
||||
const resLoc = gl.getUniformLocation(this.jfaShadowProgram, 'resolution');
|
||||
const texLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texR');
|
||||
const texGLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texG');
|
||||
const texBLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texB');
|
||||
const stepSizeLoc = gl.getUniformLocation(this.jfaShadowProgram, 'stepSize');
|
||||
const passCountLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passCount');
|
||||
const passIdxLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passIndex');
|
||||
|
||||
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight);
|
||||
gl.uniform1i(texLoc, 0);
|
||||
gl.uniform1i(texGLoc, 1);
|
||||
gl.uniform1i(texBLoc, 2);
|
||||
gl.uniform1i(passCountLoc, JFA_PASSES);
|
||||
|
||||
for (let i = 0; i < JFA_PASSES; i++) {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]);
|
||||
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight);
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const stepSize = Math.pow(2, JFA_PASSES - i - 1);
|
||||
|
||||
gl.uniform1f(stepSizeLoc, stepSize);
|
||||
gl.uniform1i(passIdxLoc, i);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]);
|
||||
gl.activeTexture(gl.TEXTURE2);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
|
||||
this.shadowSwap = 1 - this.shadowSwap;
|
||||
}
|
||||
}
|
||||
|
||||
private shadowPass() {
|
||||
const gl = this.gl;
|
||||
this.shadowSwap = 0;
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]);
|
||||
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight);
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(this.jfaInitProgram);
|
||||
gl.bindVertexArray(this.vao);
|
||||
|
||||
const resLoc = gl.getUniformLocation(this.jfaInitProgram, 'resolution');
|
||||
const texLoc = gl.getUniformLocation(this.jfaInitProgram, 'dataTex');
|
||||
|
||||
gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight);
|
||||
|
||||
gl.uniform1i(texLoc, 0);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
|
||||
this.shadowSwap = 1 - this.shadowSwap;
|
||||
}
|
||||
|
||||
private processResize() {
|
||||
const gl = this.gl;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE);
|
||||
this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE);
|
||||
|
||||
// Update collision texture size
|
||||
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);
|
||||
|
||||
// Re-render collision data after resize
|
||||
this.handleShapeTransform();
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]);
|
||||
|
||||
const newTex = gl.createTexture();
|
||||
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
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.bindTexture(gl.TEXTURE_2D, newTex);
|
||||
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.deleteTexture(this.tex[i]);
|
||||
if (!this.tex[i]) {
|
||||
throw new Error('Failed to create texture1');
|
||||
}
|
||||
if (!newTex) {
|
||||
throw new Error('Failed to create texture2');
|
||||
}
|
||||
this.tex[i] = newTex;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 2; 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.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.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);
|
||||
}
|
||||
}
|
||||
|
||||
private createProgramFromStrings({
|
||||
vertex,
|
||||
fragment,
|
||||
}: {
|
||||
vertex: string;
|
||||
fragment: string;
|
||||
}): WebGLProgram | undefined {
|
||||
const vertexShader = WebGLUtils.createShader(this.gl, this.gl.VERTEX_SHADER, vertex);
|
||||
const fragmentShader = WebGLUtils.createShader(this.gl, this.gl.FRAGMENT_SHADER, fragment);
|
||||
|
||||
if (!vertexShader || !fragmentShader) {
|
||||
console.error('Failed to create shaders');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return WebGLUtils.createProgram(this.gl, vertexShader, fragmentShader);
|
||||
}
|
||||
|
||||
private collectShapeData() {
|
||||
const positions: number[] = [];
|
||||
const indices: number[] = [];
|
||||
let vertexOffset = 0;
|
||||
|
||||
this.shapes.forEach((shape) => {
|
||||
const rect = shape.getTransformDOMRect();
|
||||
if (!rect) return;
|
||||
|
||||
// Get the transformed vertices in parent space
|
||||
const transformedPoints = rect.vertices().map((point) => rect.toParentSpace(point));
|
||||
|
||||
// Convert the transformed points to buffer coordinates
|
||||
const bufferPoints = transformedPoints.map((point) => this.convertToBufferCoordinates(point.x, point.y));
|
||||
|
||||
// Add vertices
|
||||
bufferPoints.forEach((point) => {
|
||||
positions.push(point.x, point.y);
|
||||
});
|
||||
|
||||
// Add indices for two triangles
|
||||
indices.push(vertexOffset, vertexOffset + 1, vertexOffset + 2, vertexOffset, vertexOffset + 2, vertexOffset + 3);
|
||||
|
||||
vertexOffset += 4;
|
||||
});
|
||||
|
||||
const gl = this.gl;
|
||||
|
||||
// Update buffers with new data
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.DYNAMIC_DRAW);
|
||||
|
||||
this.shapeIndexCount = indices.length;
|
||||
}
|
||||
|
||||
private convertToBufferCoordinates(x: number, y: number) {
|
||||
return {
|
||||
x: (x / this.clientWidth) * 2 - 1,
|
||||
y: -((y / this.clientHeight) * 2 - 1), // Flip Y coordinate
|
||||
};
|
||||
}
|
||||
|
||||
private updateCollisionTexture() {
|
||||
const gl = this.gl;
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo);
|
||||
gl.viewport(0, 0, this.bufferWidth, this.bufferHeight);
|
||||
|
||||
// Clear with transparent black (no collision)
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// Disable depth testing and blending
|
||||
gl.disable(gl.DEPTH_TEST);
|
||||
gl.disable(gl.BLEND);
|
||||
|
||||
// Use collision shader program
|
||||
gl.useProgram(this.collisionProgram);
|
||||
gl.bindVertexArray(this.shapeVao);
|
||||
|
||||
// Draw all shapes
|
||||
if (this.shapeIndexCount > 0) {
|
||||
gl.drawElements(gl.TRIANGLES, this.shapeIndexCount, gl.UNSIGNED_SHORT, 0);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
gl.bindVertexArray(null);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
}
|
||||
|
||||
private handleShapeTransform = () => {
|
||||
// Recollect and update all shape data when any shape changes
|
||||
// TODO: do this more piecemeal
|
||||
this.collectShapeData();
|
||||
this.updateCollisionTexture();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { FolkSand } from '../folk-sand';
|
||||
|
||||
FolkSand.define();
|
||||
|
||||
export { FolkSand };
|
||||
|
|
@ -14,5 +14,5 @@
|
|||
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
|
||||
"types": ["@webgpu/types", "@types/node", "bun-types"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "demo/**/*.ts", "vite.config.ts"]
|
||||
"include": ["src/**/*.ts", "demo/**/*.ts", "vite.config.ts", "globals.d.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue