Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions apps/typegpu-docs/src/examples/simulation/cloth-mesh/cloth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { d, std, tgpu } from 'typegpu';
import { meshes } from '@typegpu/geometry';

export const segments = 32;
export const timeStep = 1 / 720;

const stride = segments + 1;
const size = 2.4;
const hangingWidth = 2;
const spacing = size / segments;

export const sheet = meshes.parametric(
{
at: (u, v) => {
'use gpu';
return d.vec3f((u - 0.5) * hangingWidth, (0.5 - v) * size, 0.08 * std.sin(u * 8 * Math.PI));
},
},
{ cols: segments, rows: segments },
);

export const Camera = d.struct({
position: d.vec4f,
viewProjection: d.mat4x4f,
viewProjectionInverse: d.mat4x4f,
});
export const Pointer = d.struct({ position: d.vec2f, radius: d.vec2f });
export const Params = d.struct({ time: d.f32, wind: d.f32, stiffness: d.f32 });
export const Grab = d.struct({
index: d.i32,
depth: d.f32,
offset: d.vec3f,
candidate: d.atomic(d.u32),
});
export const Vertices = d.arrayOf(meshes.Surface, sheet.vertexCount);
export const Velocities = d.arrayOf(d.vec3f, sheet.vertexCount);
export const Forces = d.arrayOf(d.vec3f, sheet.vertexCount);

export const verticesAccess = tgpu.mutableAccessor(Vertices);
export const velocityAccess = tgpu.mutableAccessor(Velocities);
export const forceAccess = tgpu.mutableAccessor(Forces);
export const grabAccess = tgpu.mutableAccessor(Grab);
export const cameraAccess = tgpu.accessor(Camera);
export const pointerAccess = tgpu.accessor(Pointer);
export const paramsAccess = tgpu.accessor(Params);

const noCandidate = 0xffffffff;
const indexBits = Math.ceil(Math.log2(sheet.vertexCount));
const indexMask = (1 << indexBits) - 1;
const depthScale = (1 << (32 - indexBits)) - 1;

export const released = { index: -1, depth: 0, offset: d.vec3f(), candidate: noCandidate };

const neighbors = tgpu.const(d.arrayOf(d.vec2i, 12), [
d.vec2i(-1, 0),
d.vec2i(1, 0),
d.vec2i(0, -1),
d.vec2i(0, 1),

d.vec2i(-1, -1),
d.vec2i(1, -1),
d.vec2i(-1, 1),
d.vec2i(1, 1),

d.vec2i(-2, 0),
d.vec2i(2, 0),
d.vec2i(0, -2),
d.vec2i(0, 2),
]);

export function isPinned(index: number) {
'use gpu';
return index <= segments && index % 8 === 0;
}

function toClip(position: d.v3f) {
'use gpu';
return cameraAccess.$.viewProjection * d.vec4f(position, 1);
}

function toWorld(ndc: d.v2f, depth: number) {
'use gpu';
const world = cameraAccess.$.viewProjectionInverse * d.vec4f(ndc, depth, 1);
return world.xyz / world.w;
}

export const pick = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const i = gid.x;
if (i >= sheet.vertexCount || isPinned(i)) return;

const clip = toClip(verticesAccess.$[i].position);
if (clip.w <= 0) return;

const ndc = clip.xyz / clip.w;
const pointer = pointerAccess.$;
if (std.length((ndc.xy - pointer.position) / pointer.radius) > 1) return;

const depth = d.u32(std.saturate(ndc.z) * depthScale);
std.atomicMin(grabAccess.$.candidate, (depth << indexBits) | i);
});

export const resolve = tgpu.computeFn({ workgroupSize: [1] })(() => {
'use gpu';
const key = std.atomicLoad(grabAccess.$.candidate);
std.atomicStore(grabAccess.$.candidate, noCandidate);
if (key === noCandidate) return;

const index = d.i32(key & indexMask);
const position = verticesAccess.$[index].position;
const clip = toClip(position);
const depth = clip.z / clip.w;

grabAccess.$.index = index;
grabAccess.$.depth = depth;
grabAccess.$.offset = position - toWorld(pointerAccess.$.position, depth);
});

export const forces = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const i = gid.x;
if (i >= sheet.vertexCount) return;

if (isPinned(i) || d.i32(i) === grabAccess.$.index) {
forceAccess.$[i] = d.vec3f();
return;
}

const cell = d.vec2i(i % stride, std.intdiv(i, stride));
const position = verticesAccess.$[i].position;
const velocity = velocityAccess.$[i];

const params = paramsAccess.$;
const gust = params.wind * (6 + 4 * std.sin(params.time * 2 + position.x * 3 + position.y));
let force =
d.vec3f(params.wind * std.sin(params.time + position.y) * 2, -9.8, gust) *
(timeStep * timeStep);

for (const offset of neighbors.$) {
const neighbor = cell + offset;
if (std.any(std.lt(neighbor, d.vec2i())) || std.any(std.gt(neighbor, d.vec2i(segments)))) {
continue;
}

const neighborIndex = neighbor.y * stride + neighbor.x;
const delta = verticesAccess.$[neighborIndex].position - position;
const distance = std.max(std.length(delta), 0.000001);
const direction = delta / distance;
const restLength = std.length(d.vec2f(offset)) * spacing;

const bending = std.any(std.eq(std.abs(offset), d.vec2i(2)));
const stiffness = params.stiffness * (bending ? 0.1 : 0.5);
const damping = bending ? 0 : 0.06;
const relativeVelocity = velocityAccess.$[neighborIndex] - velocity;
const stretchSpeed = std.dot(relativeVelocity, direction);

force += direction * ((distance - restLength) * stiffness + stretchSpeed * damping);
}

forceAccess.$[i] = d.vec3f(force);
});

export const integrate = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const i = gid.x;
if (i >= sheet.vertexCount || isPinned(i)) return;

const vertex = verticesAccess.$[i];
if (d.i32(i) === grabAccess.$.index) {
velocityAccess.$[i] = d.vec3f();
const target = toWorld(pointerAccess.$.position, grabAccess.$.depth) + grabAccess.$.offset;
const delta = target - vertex.position;
vertex.position += delta * std.min(1, (8 * timeStep) / std.max(std.length(delta), 0.000001));
} else {
velocityAccess.$[i] = velocityAccess.$[i] * 0.995 + forceAccess.$[i];
vertex.position += velocityAccess.$[i];
}
});

export const normals = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const i = gid.x;
if (i >= sheet.vertexCount) return;

const x = d.i32(i % stride);
const y = d.i32(std.intdiv(i, stride));

const left = verticesAccess.$[y * stride + std.max(x - 1, 0)].position;
const right = verticesAccess.$[y * stride + std.min(x + 1, segments)].position;
const top = verticesAccess.$[std.max(y - 1, 0) * stride + x].position;
const bottom = verticesAccess.$[std.min(y + 1, segments) * stride + x].position;

const normal = std.cross(right - left, bottom - top);
verticesAccess.$[i].normal = normal / std.max(std.length(normal), 0.000001);
});
82 changes: 82 additions & 0 deletions apps/typegpu-docs/src/examples/simulation/cloth-mesh/drag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { d } from 'typegpu';
import type { Pointer } from './cloth.ts';

const grabRadius = 24;

function suppressMenu(event: Event) {
event.preventDefault();
}

export function setupClothDrag(
canvas: HTMLCanvasElement,
{
onGrab,
onMove,
onRelease,
}: {
onGrab: (pointer: d.Infer<typeof Pointer>) => void;
onMove: (position: d.v2f) => void;
onRelease: () => void;
},
) {
let pointer: number | undefined;

function pointerAt(event: PointerEvent) {
const rect = canvas.getBoundingClientRect();
return {
position: d.vec2f(
((event.clientX - rect.left) / rect.width) * 2 - 1,
1 - ((event.clientY - rect.top) / rect.height) * 2,
),
radius: d.vec2f((2 * grabRadius) / rect.width, (2 * grabRadius) / rect.height),
};
}

function grab(event: PointerEvent) {
if (event.pointerType === 'touch' && !event.isPrimary) {
release();
Comment thread
reczkok marked this conversation as resolved.
Comment thread
reczkok marked this conversation as resolved.
return;
}
if (pointer !== undefined || event.button !== 0 || event.shiftKey || !event.isPrimary) return;
event.preventDefault();
pointer = event.pointerId;
canvas.setPointerCapture(pointer);
canvas.style.cursor = 'grabbing';
onGrab(pointerAt(event));
}

function move(event: PointerEvent) {
if (event.pointerId === pointer) onMove(pointerAt(event).position);
}

function release() {
if (pointer === undefined) return;
pointer = undefined;
canvas.style.cursor = 'grab';
onRelease();
}

function end(event: PointerEvent) {
if (event.pointerId === pointer) release();
}

canvas.style.cursor = 'grab';
canvas.addEventListener('pointerdown', grab);
canvas.addEventListener('pointermove', move);
canvas.addEventListener('pointerup', end);
canvas.addEventListener('pointercancel', end);
canvas.addEventListener('lostpointercapture', end);
canvas.addEventListener('contextmenu', suppressMenu);
window.addEventListener('blur', release);

return {
get active() {
return pointer !== undefined;
},
release,
cleanup() {
release();
window.removeEventListener('blur', release);
Comment thread
reczkok marked this conversation as resolved.
},
Comment thread
reczkok marked this conversation as resolved.
};
}
30 changes: 30 additions & 0 deletions apps/typegpu-docs/src/examples/simulation/cloth-mesh/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<style>
.controls-header {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}

.controls {
font-size: 0.8rem;
}

#help {
position: absolute;
margin: 0 auto;
text-align: left;
z-index: 1;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 10px;
user-select: none;
pointer-events: none;
}
</style>
<div id="help">
<h3 class="controls-header">Controls</h3>
<p class="controls"><b>Drag:</b> Pull the cloth</p>
<p class="controls"><b>Shift + Drag:</b> Orbit camera</p>
<p class="controls"><b>Scroll / Pinch:</b> Zoom</p>
</div>
<canvas data-fit-to-container style="touch-action: none"></canvas>
Loading
Loading