Skip to content

Commit 82150b2

Browse files
authored
feat(@typegpu/geometry): lines & circles (#1911)
1 parent c29d569 commit 82150b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2604
-9
lines changed

apps/typegpu-docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@stackblitz/sdk": "^1.11.0",
2727
"@tailwindcss/vite": "^4.1.13",
2828
"@typegpu/color": "workspace:*",
29+
"@typegpu/geometry": "workspace:*",
2930
"@typegpu/noise": "workspace:*",
3031
"@typegpu/sdf": "workspace:*",
3132
"@types/react": "^19.1.8",

apps/typegpu-docs/src/components/ExampleView.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,7 @@ export function ExampleView({ example }: Props) {
171171
</div>
172172

173173
<div className='absolute right-0 z-5 md:top-15 md:right-8'>
174-
<Button
175-
onClick={() => openInStackBlitz(example)}
176-
>
174+
<Button onClick={() => openInStackBlitz(example)}>
177175
<span className='font-bold'>Edit on</span>
178176
<img
179177
src='https://developer.stackblitz.com/img/logo/stackblitz-logo-black_blue.svg'
@@ -256,15 +254,16 @@ function useResizableCanvas(exampleHtmlRef: RefObject<HTMLDivElement | null>) {
256254

257255
canvas.parentElement?.replaceChild(container, canvas);
258256

259-
const onResize = () => {
260-
newCanvas.width = frame.clientWidth * window.devicePixelRatio;
261-
newCanvas.height = frame.clientHeight * window.devicePixelRatio;
257+
const onResize: ResizeObserverCallback = (entries) => {
258+
const size = entries[0]?.devicePixelContentBoxSize[0];
259+
if (size) {
260+
newCanvas.width = size.inlineSize;
261+
newCanvas.height = size.blockSize;
262+
}
262263
};
263264

264-
onResize();
265-
266265
const observer = new ResizeObserver(onResize);
267-
observer.observe(container);
266+
observer.observe(newCanvas);
268267
observers.push(observer);
269268
}
270269

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<canvas></canvas>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
import * as s from 'typegpu/std';
4+
5+
import { circle, circleVertexCount } from '@typegpu/geometry';
6+
7+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
8+
const canvas = document.querySelector('canvas');
9+
const context = canvas?.getContext('webgpu');
10+
const multisample = true;
11+
12+
if (!canvas) {
13+
throw new Error('Could not find canvas');
14+
}
15+
if (!context) {
16+
throw new Error('Could not create WebGPU context');
17+
}
18+
19+
const adapter = await navigator.gpu.requestAdapter();
20+
console.log(`Using ${adapter?.info.vendor} adapter`);
21+
const device = await adapter?.requestDevice({
22+
requiredFeatures: ['timestamp-query'],
23+
});
24+
if (!device) {
25+
throw new Error('Could not get WebGPU device');
26+
}
27+
const root = tgpu.initFromDevice({ device });
28+
29+
context.configure({
30+
device: root.device,
31+
format: presentationFormat,
32+
alphaMode: 'premultiplied',
33+
});
34+
35+
// Textures
36+
let msaaTexture: GPUTexture;
37+
let msaaTextureView: GPUTextureView;
38+
39+
const createDepthAndMsaaTextures = () => {
40+
if (msaaTexture) {
41+
msaaTexture.destroy();
42+
}
43+
msaaTexture = device.createTexture({
44+
size: [canvas.width, canvas.height, 1],
45+
format: presentationFormat,
46+
sampleCount: 4,
47+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
48+
});
49+
msaaTextureView = msaaTexture.createView();
50+
};
51+
52+
createDepthAndMsaaTextures();
53+
const resizeObserver = new ResizeObserver(createDepthAndMsaaTextures);
54+
resizeObserver.observe(canvas);
55+
56+
// const Uniforms = d.struct({});
57+
58+
const Circle = d.struct({
59+
position: d.vec2f,
60+
radius: d.f32,
61+
});
62+
63+
const bindGroupLayout = tgpu.bindGroupLayout({
64+
// uniforms: {
65+
// uniform: Uniforms,
66+
// },
67+
circles: {
68+
storage: (n: number) => d.arrayOf(Circle, n),
69+
},
70+
});
71+
72+
// const uniforms = root.createBuffer(Uniforms, {}).$usage(
73+
// 'uniform',
74+
// );
75+
76+
const circleCount = 1000;
77+
const circles = root.createBuffer(
78+
d.arrayOf(Circle, circleCount),
79+
Array.from({ length: circleCount }).map(() =>
80+
Circle({
81+
position: d.vec2f(Math.random() * 2 - 1, Math.random() * 2 - 1),
82+
radius: 0.05 * Math.random() + 0.01,
83+
})
84+
),
85+
).$usage('storage');
86+
87+
const uniformsBindGroup = root.createBindGroup(bindGroupLayout, {
88+
// uniforms,
89+
circles,
90+
});
91+
92+
const mainVertexMaxArea = tgpu['~unstable'].vertexFn({
93+
in: {
94+
instanceIndex: d.builtin.instanceIndex,
95+
vertexIndex: d.builtin.vertexIndex,
96+
},
97+
out: {
98+
outPos: d.builtin.position,
99+
uv: d.vec2f,
100+
instanceIndex: d.interpolate('flat', d.u32),
101+
},
102+
})(({ vertexIndex, instanceIndex }) => {
103+
const C = bindGroupLayout.$.circles[instanceIndex];
104+
const unit = circle(vertexIndex);
105+
const pos = s.add(C.position, s.mul(unit, C.radius));
106+
return {
107+
outPos: d.vec4f(pos, 0.0, 1.0),
108+
uv: unit,
109+
instanceIndex,
110+
};
111+
});
112+
113+
const mainFragment = tgpu['~unstable'].fragmentFn({
114+
in: {
115+
uv: d.vec2f,
116+
instanceIndex: d.interpolate('flat', d.u32),
117+
},
118+
out: d.vec4f,
119+
})(({ uv, instanceIndex }) => {
120+
const color = d.vec3f(
121+
1,
122+
s.cos(d.f32(instanceIndex)),
123+
s.sin(5 * d.f32(instanceIndex)),
124+
);
125+
const r = s.length(uv);
126+
return d.vec4f(
127+
s.mix(color, d.vec3f(), s.clamp((r - 0.9) * 20, 0, 0.5)),
128+
1,
129+
);
130+
});
131+
132+
const pipeline = root['~unstable']
133+
.withVertex(mainVertexMaxArea, {})
134+
.withFragment(mainFragment, { format: presentationFormat })
135+
.withMultisample({ count: multisample ? 4 : 1 })
136+
.createPipeline();
137+
138+
setTimeout(() => {
139+
pipeline
140+
.with(bindGroupLayout, uniformsBindGroup)
141+
.withColorAttachment({
142+
...(multisample
143+
? {
144+
view: msaaTextureView,
145+
resolveTarget: context.getCurrentTexture().createView(),
146+
}
147+
: {
148+
view: context.getCurrentTexture().createView(),
149+
}),
150+
clearValue: [0, 0, 0, 0],
151+
loadOp: 'clear',
152+
storeOp: 'store',
153+
})
154+
.withPerformanceCallback((a, b) => {
155+
console.log((Number(b - a) * 1e-6).toFixed(3), 'ms');
156+
})
157+
.draw(circleVertexCount(4), circleCount);
158+
}, 100);
159+
160+
export function onCleanup() {
161+
root.destroy();
162+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"title": "Circles",
3+
"category": "geometry",
4+
"tags": ["experimental"],
5+
"dev": true
6+
}
750 KB
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const TEST_SEGMENT_COUNT = 2000;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<canvas></canvas>

0 commit comments

Comments
 (0)