Skip to content

Commit f357a0c

Browse files
authored
add particle sim (#13)
* Add the ability to add a compute pass * Add particles gameobject (does nothing yet) * feat: Add particle system with WebGPU rendering and basic movement * refactor: Update particle rendering and shader structure * Rendering one particle!! * feat: Add DrawInstanced method to RenderPass for instanced rendering support * refactor: Implement Draw using DrawInstanced with single instance * feat: Add parent pointer to GameObject and set parent references in get_gameobjects * refactor: Implement recursive game object traversal with proper parent handling * Reorder * refactor: Split CalculateMVP into separate model, view, and projection functions * feat: Add MVP() method to GameObject for easy matrix calculation * feat: Add parent transform inheritance to GameObject MVP calculation * use MVP * feat: Add VertexBufferLayouts template for composing multiple vertex buffer layouts * refactor: Update RenderPipeline to support multiple vertex buffer layouts * tweak * refactor: Fix VertexBufferLayouts to handle move-only VertexBufferInfo * hmm * Instancing works! * confetti!! * Initial compute shader * refactor: Update BindGroupLayout to include buffer count in getId and setEntry methods * fix: Resolve type conversion and narrowing issues in BindGroupLayout * feat: Add WorldInfo struct to pass deltaTime and mouse position to compute shader * feat: Add worldInfo uniform buffer to Particles constructor * feat: Add gravitational mouse attraction to particle simulation * play with gravity * increase workgroup size * feat: Add SceneGeometry class for managing 3D scene geometry * refactor: Extract scene geometry computation to SceneGeometry class * refactor: Split computeSceneGeometry into separate wall and invisibility path functions * refactor: Inline getFlattenedBounds and computeInvisibilityPaths methods * simplify * refactor: Simplify visibility computation method signature * feat: Add BVH acceleration structure for line intersection tests in wall paths * fix: Remove duplicate AABB constructor and improve comments * feat: Add line intersection method to BVHNode for efficient line segment intersection checks * refactor: Move BVH implementation to separate source file * feat: Add ray intersection method to BVH for efficient ray-segment intersection * refactor: Remove unused `cull_frontfaces` flag in visibility polygon computation * refactor: Modify visibility polygon computation to use BVH acceleration structure * Use BVH acceleration structure when computing FoW * feat: Add BVH debug visualization with recursive AABB drawing * Flattenable-BVH * feat: Add shader include processing functionality * Rearrange * More compact BVHNode * weird * Can't believe that was it * bring force back * refactor: Add normal to SegmentIntersection struct for improved ray tracing * feat: Implement realistic particle wall bouncing with reflection physics * update buffer * only spawn particles when p is pressed * buffer upload resize should be based on capacity * push rather than emplace * hmm
1 parent 45484b7 commit f357a0c

37 files changed

+1634
-284
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,7 @@ res_path.hpp
405405

406406
build_emscripten
407407
build_analysis
408+
.aider*
409+
.env
410+
411+
build_xcode

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@
8989
"print": "cpp",
9090
"filesystem": "cpp",
9191
"forward_list": "cpp",
92-
"ranges": "cpp"
92+
"ranges": "cpp",
93+
"cfenv": "cpp",
94+
"regex": "cpp",
95+
"valarray": "cpp"
9396
},
9497
"files.exclude": {
9598
"build": true,

OpenGL/res/shaders/particles.wgsl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
struct Particle {
2+
@location(1) position: vec2<f32>, // World space position
3+
@location(2) velocity: vec2<f32>,
4+
@location(3) color: vec4<f32>,
5+
};
6+
7+
struct VertexInput {
8+
@location(0) position: vec2<f32>, // Local vertex position (relative to particle center)
9+
};
10+
11+
// Separate matrices for clearer transform chain
12+
struct VertexUniforms {
13+
world_to_clip: mat4x4<f32>, // View-Projection matrix only
14+
};
15+
16+
@group(0) @binding(0)
17+
var<uniform> vertexUniforms: VertexUniforms;
18+
19+
struct VertexOutput {
20+
@builtin(position) Position: vec4<f32>,
21+
@location(0) color: vec4<f32>,
22+
};
23+
24+
struct FragmentOutput {
25+
@location(0) color: vec4<f32>,
26+
};
27+
28+
@vertex
29+
fn vertex_main(vertex: VertexInput, particle: Particle) -> VertexOutput {
30+
var output: VertexOutput;
31+
32+
// Transform the local vertex position to world space relative to particle position
33+
let world_pos = vertex.position + particle.position;
34+
35+
// Transform from world space to clip space using world_to_clip matrix
36+
output.Position = vertexUniforms.world_to_clip * vec4<f32>(world_pos, 0.0, 1.0);
37+
output.color = particle.color;
38+
return output;
39+
}
40+
41+
@fragment
42+
fn fragment_main(input: VertexOutput) -> FragmentOutput {
43+
var output: FragmentOutput;
44+
output.color = input.color;
45+
return output;
46+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
struct WorldInfo {
2+
deltaTime : f32,
3+
mousePos : vec2<f32>,
4+
};
5+
6+
struct Particle {
7+
position : vec2<f32>, // World space position
8+
velocity : vec2<f32>,
9+
color : vec4<f32>,
10+
};
11+
12+
struct Segment {
13+
start : vec2<f32>,
14+
end : vec2<f32>,
15+
};
16+
17+
struct BvhNode {
18+
leftTypeCount : u32, // leftType :1, leftCount :31
19+
leftOffset : u32,
20+
21+
rightTypeCount : u32, // rightType :1, rightCount :31
22+
rightOffset : u32,
23+
24+
leftBBoxMin : vec2<f32>,
25+
leftBBoxMax : vec2<f32>,
26+
27+
rightBBoxMin : vec2<f32>,
28+
rightBBoxMax : vec2<f32>,
29+
};
30+
31+
struct AABB {
32+
min : vec2<f32>,
33+
max : vec2<f32>,
34+
};
35+
36+
// Helper functions to unpack BVH Node data
37+
fn getLeftType(node : BvhNode) -> u32 {
38+
return node.leftTypeCount >> 31u;
39+
}
40+
41+
fn getLeftCount(node : BvhNode) -> u32 {
42+
return node.leftTypeCount & 0x7FFFFFFFu;
43+
}
44+
45+
fn getRightType(node : BvhNode) -> u32 {
46+
return node.rightTypeCount >> 31u;
47+
}
48+
49+
fn getRightCount(node : BvhNode) -> u32 {
50+
return node.rightTypeCount & 0x7FFFFFFFu;
51+
}
52+
53+
fn isLeafNode(node : BvhNode, isLeft : bool) -> bool {
54+
if isLeft {
55+
return getLeftType(node) == 1u;
56+
} else {
57+
return getRightType(node) == 1u;
58+
}
59+
}
60+
61+
fn getChildOffset(node : BvhNode, isLeft : bool) -> u32 {
62+
if isLeft {
63+
return node.leftOffset;
64+
} else {
65+
return node.rightOffset;
66+
}
67+
}
68+
69+
fn getChildCount(node : BvhNode, isLeft : bool) -> u32 {
70+
if isLeft {
71+
return getLeftCount(node);
72+
} else {
73+
return getRightCount(node);
74+
}
75+
}
76+
77+
fn getChildBBox(node : BvhNode, isLeft : bool) -> AABB {
78+
if isLeft {
79+
return AABB(node.leftBBoxMin, node.leftBBoxMax);
80+
} else {
81+
return AABB(node.rightBBoxMin, node.rightBBoxMax);
82+
}
83+
}
84+
85+
// Utility functions
86+
fn cross2D(a : vec2<f32>, b : vec2<f32>) -> f32 {
87+
return a.x * b.y - a.y * b.x;
88+
}
89+
90+
fn dot2D(a : vec2<f32>, b : vec2<f32>) -> f32 {
91+
return a.x * b.x + a.y * b.y;
92+
}
93+
94+
struct Ray {
95+
origin : vec2<f32>,
96+
direction : vec2<f32>,
97+
};
98+
99+
struct SegmentIntersection {
100+
hit : bool,
101+
position : vec2<f32>,
102+
normal : vec2<f32>,
103+
t : f32,
104+
};
105+
106+
// Buffer bindings
107+
@group(0) @binding(0) var<storage, read_write> particleBuffer : array<Particle>;
108+
@group(0) @binding(1) var<uniform> world : WorldInfo;
109+
@group(0) @binding(2) var<storage, read> segments : array<Segment>;
110+
@group(0) @binding(3) var<storage, read> bvhNodes : array<BvhNode>;
111+
112+
// Constants
113+
const G : f32 = 30.0;
114+
const MIN_DISTANCE_SQUARED : f32 = 1.0; // Prevent division by zero
115+
116+
// Compute shader entry point
117+
@compute @workgroup_size(256)
118+
fn compute_main(@builtin(global_invocation_id) id : vec3<u32>) {
119+
let index = id.x;
120+
121+
let particle = &particleBuffer[index];
122+
123+
// Calculate direction to mouse
124+
let toMouse = world.mousePos - particle.position;
125+
let distanceSquared = max(dot2D(toMouse, toMouse), MIN_DISTANCE_SQUARED);
126+
127+
// Calculate gravitational force (F = G * m1 * m2 / r^2)
128+
// Since mass is uniform we can simplify
129+
let force = normalize(toMouse) * G / distanceSquared;
130+
131+
// Update velocity (a = F/m, simplified since mass = 1)
132+
particleBuffer[index].velocity += force * world.deltaTime;
133+
134+
// New position based on velocity
135+
let newPosition = particle.position + particleBuffer[index].velocity * world.deltaTime;
136+
137+
// Check for collision with walls
138+
let intersection = findWallCollision(particle.position, newPosition);
139+
140+
if (intersection.hit) {
141+
// Bounce coefficient (1.0 = perfect bounce, 0.0 = full stop)
142+
let bounce = 0.8;
143+
144+
// Calculate reflection vector
145+
let v = particleBuffer[index].velocity;
146+
let n = intersection.normal;
147+
let reflected = v - 2.0 * dot2D(v, n) * n;
148+
149+
// Update velocity with bounce effect
150+
let newVelocity = reflected * bounce;
151+
particleBuffer[index].velocity = newVelocity;
152+
153+
// Place particle at intersection point
154+
particleBuffer[index].position = intersection.position + newVelocity * world.deltaTime;
155+
} else {
156+
// No collision, update particle position normally
157+
particleBuffer[index].position = newPosition;
158+
}
159+
}
160+
161+
fn findWallCollision(particlePosition : vec2<f32>, newPosition : vec2<f32>) -> SegmentIntersection {
162+
var closest : SegmentIntersection;
163+
closest.hit = false;
164+
closest.t = 999999.0;
165+
166+
let particlePath = Segment(particlePosition, newPosition);
167+
168+
for (var i: u32 = 0; i < arrayLength(&segments); i++) {
169+
let wall = segments[i];
170+
let intersection = segmentIntersection(wall, particlePath);
171+
172+
if (intersection.hit && intersection.t < closest.t) {
173+
closest = intersection;
174+
}
175+
}
176+
177+
return closest;
178+
}
179+
180+
fn segmentIntersection(s1 : Segment, s2 : Segment) -> SegmentIntersection {
181+
var result : SegmentIntersection;
182+
result.hit = false;
183+
184+
let p = s1.start;
185+
let r = s1.end - s1.start;
186+
let q = s2.start;
187+
let s = s2.end - s2.start;
188+
189+
let r_cross_s = cross2D(r, s);
190+
let q_p = q - p;
191+
192+
if (abs(r_cross_s) < 1e-8) {
193+
return result; // Lines are parallel
194+
}
195+
196+
let t = cross2D(q_p, s) / r_cross_s;
197+
let u = cross2D(q_p, r) / r_cross_s;
198+
199+
if (t >= 0.0 && t <= 1.0 && u >= 0.0 && u <= 1.0) {
200+
result.hit = true;
201+
result.t = t;
202+
result.position = p + t * r;
203+
result.normal = normalize(vec2<f32>(-r.y, r.x)); // Perpendicular to wall
204+
return result;
205+
}
206+
207+
return result;
208+
}
209+

OpenGL/res/shaders/stars.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ fn starColor(seed: f32) -> vec3<f32> {
116116
fn main_1() {
117117
var uv_2: vec2<f32>;
118118
var aspectRatio: f32;
119-
var numStars: i32 = 502i;
119+
var numStars: i32 = 2i;
120120
var finalColor: vec3<f32> = vec3(0f);
121121
var i: i32 = 0i;
122122
var seed_2: f32;

0 commit comments

Comments
 (0)