Skip to content

Commit f9a4402

Browse files
docs: Sdfs (#1977)
1 parent a349f37 commit f9a4402

12 files changed

Lines changed: 241 additions & 115 deletions

File tree

apps/typegpu-docs/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ export default defineConfig({
211211
label: '@typegpu/noise',
212212
slug: 'ecosystem/typegpu-noise',
213213
},
214+
{
215+
label: '@typegpu/sdf',
216+
slug: 'ecosystem/typegpu-sdf',
217+
},
214218
DEV && {
215219
label: '@typegpu/color',
216220
slug: 'ecosystem/typegpu-color',
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
title: "@typegpu/sdf"
3+
---
4+
5+
The `@typegpu/sdf` package offers a set of [signed distance function](https://en.wikipedia.org/wiki/Signed_distance_function) utilities for use in TypeGPU and WebGPU projects.
6+
7+
## Example usage
8+
9+
By definition, signed distance functions return a distance from the given point to the boundary of a shape.
10+
For example, the `sdf.sdDisk(point, radius)` returns the signed distance from the `point`, to a circle located at point (0, 0) of radius equal to `radius`.
11+
12+
- `sdf.sdDisk(d.vec2f(2, 0), 1)` is equal to 1,
13+
- `sdf.sdDisk(d.vec2f(1, 0), 1)` is equal to 0,
14+
- `sdf.sdDisk(d.vec2f(0, 0), 1)` is equal to -1.
15+
16+
:::note
17+
Every shape is assumed to be located at position (0, 0).
18+
19+
Position, scale and translation of a shape can be adjusted by applying the inverse transformation to the position argument.
20+
:::
21+
22+
Here is an example of a fragment shader that draws a disk of radius 0.25 on the center of a (presumably square) canvas:
23+
24+
```ts twoslash
25+
import tgpu from 'typegpu';
26+
import * as d from 'typegpu/data';
27+
import * as sdf from '@typegpu/sdf';
28+
29+
const mainFragment = tgpu['~unstable'].fragmentFn({
30+
in: { uv: d.vec2f },
31+
out: d.vec4f,
32+
})(({ uv }) => {
33+
if (sdf.sdDisk(uv.sub(0.5), 0.25) < 0) {
34+
return d.vec4f(0, 0, 0, 1);
35+
}
36+
return d.vec4f(1);
37+
});
38+
```
39+
40+
## Implemented functions
41+
42+
For the list of all implemented functions see [this file](https://github.com/software-mansion/TypeGPU/blob/release/packages/typegpu-sdf/src/index.ts).
43+
All functions are documented using JSDoc.
44+
45+
For any missing SDFs or utilities, feel free to submit an issue at https://github.com/software-mansion/TypeGPU/issues.
46+
47+
## Further Reading
48+
49+
- [2D Distance Functions](https://iquilezles.org/articles/distfunctions2d/) by Íñigo Quílez
50+
- [3D Distance Functions](https://iquilezles.org/articles/distfunctions/) by Íñigo Quílez

packages/typegpu-sdf/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,27 @@
55
🚧 **Under Construction** 🚧
66

77
</div>
8+
9+
A set of signed distance functions and utilities for use in WebGPU/TypeGPU apps.
10+
11+
```ts
12+
import tgpu from 'typegpu';
13+
import * as sdf from '@typegpu/sdf';
14+
import * as d from 'typegpu/data';
15+
16+
const mainFragment = tgpu['~unstable'].fragmentFn({
17+
in: { uv: d.vec2f },
18+
out: d.vec4f,
19+
})(({ uv }) => {
20+
const distance = sdf.opSmoothUnion(
21+
sdf.sdDisk(uv.sub(0.5), 0.25),
22+
sdf.sdBox2d(uv.sub(0.5), d.vec2f(0.8, 0.05)),
23+
0.1,
24+
);
25+
26+
if (distance < 0) {
27+
return d.vec4f(0, 0, 0, 1);
28+
}
29+
return d.vec4f(1, 1, 1, 0);
30+
});
31+
```

packages/typegpu-sdf/src/2d.ts

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,44 @@ import {
2121

2222
/**
2323
* Signed distance function for a disk (filled circle)
24-
* @param p Point to evaluate
24+
* @param point Point to evaluate
2525
* @param radius Radius of the disk
2626
*/
27-
export const sdDisk = tgpu.fn([vec2f, f32], f32)((p, radius) => {
28-
return length(p) - radius;
27+
export const sdDisk = tgpu.fn([vec2f, f32], f32)((point, radius) => {
28+
return length(point) - radius;
2929
});
3030

3131
/**
32-
* Signed distance function for a 2d box
33-
* @param p Point to evaluate
32+
* Signed distance function for a 2d box (rectangle)
33+
* @param point Point to evaluate
3434
* @param size Half-dimensions of the box
3535
*/
36-
export const sdBox2d = tgpu.fn([vec2f, vec2f], f32)((p, size) => {
37-
const d = sub(abs(p), size);
36+
export const sdBox2d = tgpu.fn([vec2f, vec2f], f32)((point, size) => {
37+
const d = sub(abs(point), size);
3838
return length(max(d, vec2f(0))) + min(max(d.x, d.y), 0);
3939
});
4040

4141
/**
4242
* Signed distance function for a rounded 2d box
43-
* @param p Point to evaluate
43+
* @param point Point to evaluate
4444
* @param size Half-dimensions of the box
4545
* @param cornerRadius Box corner radius
4646
*/
4747
export const sdRoundedBox2d = tgpu
48-
.fn([vec2f, vec2f, f32], f32)((p, size, cornerRadius) => {
49-
const d = add(sub(abs(p), size), vec2f(cornerRadius));
48+
.fn([vec2f, vec2f, f32], f32)((point, size, cornerRadius) => {
49+
const d = add(sub(abs(point), size), vec2f(cornerRadius));
5050
return length(max(d, vec2f(0))) + min(max(d.x, d.y), 0) - cornerRadius;
5151
});
5252

5353
/**
5454
* Signed distance function for a line segment
55-
* @param p Point to evaluate
56-
* @param a First endpoint of the line
57-
* @param b Second endpoint of the line
55+
* @param point Point to evaluate
56+
* @param A First endpoint of the line
57+
* @param B Second endpoint of the line
5858
*/
59-
export const sdLine = tgpu.fn([vec2f, vec2f, vec2f], f32)((p, a, b) => {
60-
const pa = sub(p, a);
61-
const ba = sub(b, a);
59+
export const sdLine = tgpu.fn([vec2f, vec2f, vec2f], f32)((point, A, B) => {
60+
const pa = sub(point, A);
61+
const ba = sub(B, A);
6262
const h = max(0, min(1, dot(pa, ba) / dot(ba, ba)));
6363
return length(sub(pa, ba.mul(h)));
6464
});
@@ -70,17 +70,17 @@ const dot2 = (v: v2f) => {
7070

7171
/**
7272
* Signed distance function for a quadratic Bezier curve
73-
* @param pos Point to evaluate
73+
* @param point Point to evaluate
7474
* @param A First control point of the Bezier curve
7575
* @param B Second control point of the Bezier curve
7676
* @param C Third control point of the Bezier curve
7777
*/
7878
export const sdBezier = tgpu.fn([vec2f, vec2f, vec2f, vec2f], f32)(
79-
(pos, A, B, C) => {
79+
(point, A, B, C) => {
8080
const a = B.sub(A);
8181
const b = A.sub(B.mul(2)).add(C);
8282
const c = a.mul(f32(2));
83-
const d = A.sub(pos);
83+
const d = A.sub(point);
8484

8585
const dotB = max(dot(b, b), 0.0001);
8686
const kk = 1 / dotB;
@@ -121,27 +121,34 @@ export const sdBezier = tgpu.fn([vec2f, vec2f, vec2f, vec2f], f32)(
121121
},
122122
);
123123

124-
const cro = (a: v2f, b: v2f) => {
124+
const cross = (a: v2f, b: v2f) => {
125125
'use gpu';
126126
return a.x * b.y - a.y * b.x;
127127
};
128128

129+
/**
130+
* A fast approximation of the signed distance function for a quadratic Bezier curve
131+
* @param point Point to evaluate
132+
* @param A First control point of the Bezier curve
133+
* @param B Second control point of the Bezier curve
134+
* @param C Third control point of the Bezier curve
135+
*/
129136
export const sdBezierApprox = tgpu.fn(
130137
[vec2f, vec2f, vec2f, vec2f],
131138
f32,
132-
)((pos, A, B, C) => {
139+
)((point, A, B, C) => {
133140
const i = A.sub(C);
134141
const j = C.sub(B);
135142
const k = B.sub(A);
136143
const w = j.sub(k);
137144

138-
const v0 = A.sub(pos);
139-
const v1 = B.sub(pos);
140-
const v2 = C.sub(pos);
145+
const v0 = A.sub(point);
146+
const v1 = B.sub(point);
147+
const v2 = C.sub(point);
141148

142-
const x = cro(v0, v2);
143-
const y = cro(v1, v0);
144-
const z = cro(v2, v1);
149+
const x = cross(v0, v2);
150+
const y = cross(v1, v0);
151+
const z = cross(v2, v1);
145152

146153
const s = j.mul(y).add(k.mul(z)).mul(2).sub(i.mul(x));
147154

@@ -152,10 +159,17 @@ export const sdBezierApprox = tgpu.fn(
152159
return length(d);
153160
});
154161

155-
export const sdPie = tgpu.fn([vec2f, vec2f, f32], f32)((p, c, r) => {
156-
const p_w = vec2f(p);
157-
p_w.x = abs(p.x);
158-
const l = length(p_w) - r;
159-
const m = length(p_w.sub(c.mul(clamp(dot(p_w, c), 0, r))));
160-
return max(l, m * sign(c.y * p_w.x - c.x * p_w.y));
162+
/**
163+
* Computes the signed distance field for a pie shape (circular sector).
164+
*
165+
* @param point - The point to evaluate, in 2D space
166+
* @param sc - The sine/cosine of the pie's half-angle (`d.vec2f(std.sin(angle/2), std.cos(angle/2))`)
167+
* @param radius - The radius of the pie
168+
*/
169+
export const sdPie = tgpu.fn([vec2f, vec2f, f32], f32)((point, sc, radius) => {
170+
const p_w = vec2f(point);
171+
p_w.x = abs(point.x);
172+
const l = length(p_w) - radius;
173+
const m = length(p_w.sub(sc.mul(clamp(dot(p_w, sc), 0, radius))));
174+
return max(l, m * sign(sc.y * p_w.x - sc.x * p_w.y));
161175
});

packages/typegpu-sdf/src/3d.ts

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,45 @@ import { abs, add, dot, length, max, min, saturate, sub } from 'typegpu/std';
44

55
/**
66
* Signed distance function for a sphere
7-
* @param p Point to evaluate
7+
* @param point Point to evaluate
88
* @param radius Radius of the sphere
99
*/
10-
export const sdSphere = tgpu.fn([vec3f, f32], f32)((p, radius) => {
11-
return length(p) - radius;
10+
export const sdSphere = tgpu.fn([vec3f, f32], f32)((point, radius) => {
11+
return length(point) - radius;
1212
});
1313

1414
/**
1515
* Signed distance function for a 3d box
16-
* @param p Point to evaluate
16+
* @param point Point to evaluate
1717
* @param size Half-dimensions of the box
1818
*/
19-
export const sdBox3d = tgpu.fn([vec3f, vec3f], f32)((p, size) => {
20-
const d = sub(abs(p), size);
19+
export const sdBox3d = tgpu.fn([vec3f, vec3f], f32)((point, size) => {
20+
const d = sub(abs(point), size);
2121
return length(max(d, vec3f(0))) + min(max(max(d.x, d.y), d.z), 0);
2222
});
2323

2424
/**
2525
* Signed distance function for a rounded 3d box
26-
* @param p Point to evaluate
26+
* @param point Point to evaluate
2727
* @param size Half-dimensions of the box
2828
* @param cornerRadius Box corner radius
2929
*/
3030
export const sdRoundedBox3d = tgpu
31-
.fn([vec3f, vec3f, f32], f32)((p, size, cornerRadius) => {
32-
const d = add(sub(abs(p), size), vec3f(cornerRadius));
31+
.fn([vec3f, vec3f, f32], f32)((point, size, cornerRadius) => {
32+
const d = add(sub(abs(point), size), vec3f(cornerRadius));
3333
return length(max(d, vec3f(0))) + min(max(max(d.x, d.y), d.z), 0) -
3434
cornerRadius;
3535
});
3636

3737
/**
3838
* Signed distance function for a hollow box frame
39-
* @param p Point to evaluate
39+
* @param point Point to evaluate
4040
* @param size Half-dimensions of the box
4141
* @param thickness Frame thickness
4242
*/
4343
export const sdBoxFrame3d = tgpu
44-
.fn([vec3f, vec3f, f32], f32)((p, size, thickness) => {
45-
const p1 = sub(abs(p), size);
44+
.fn([vec3f, vec3f, f32], f32)((point, size, thickness) => {
45+
const p1 = sub(abs(point), size);
4646
const q = sub(abs(add(p1, thickness)), vec3f(thickness));
4747

4848
// Calculate three possible distances for each main axis being the outer one
@@ -61,38 +61,40 @@ export const sdBoxFrame3d = tgpu
6161

6262
/**
6363
* Signed distance function for a 3D line segment
64-
* @param p Point to evaluate
65-
* @param a First endpoint of the line
66-
* @param b Second endpoint of the line
64+
* @param point Point to evaluate
65+
* @param A First endpoint of the line
66+
* @param B Second endpoint of the line
6767
*/
68-
export const sdLine3d = tgpu.fn([vec3f, vec3f, vec3f], f32)((p, a, b) => {
69-
const pa = sub(p, a);
70-
const ba = sub(b, a);
68+
export const sdLine3d = tgpu.fn([vec3f, vec3f, vec3f], f32)((point, A, B) => {
69+
const pa = sub(point, A);
70+
const ba = sub(B, A);
7171
const h = max(0, min(1, dot(pa, ba) / dot(ba, ba)));
7272
return length(sub(pa, ba.mul(h)));
7373
});
7474

7575
/**
7676
* Signed distance function for an infinite plane
77-
* @param p Point to evaluate
78-
* @param n Normal vector of the plane (must be normalized)
79-
* @param h Height/offset of the plane along the normal
77+
* @param point Point to evaluate
78+
* @param normal Normal vector of the plane (must be normalized)
79+
* @param height Height/offset of the plane along the normal
8080
*/
81-
export const sdPlane = tgpu.fn([vec3f, vec3f, f32], f32)((p, n, h) => {
82-
return dot(p, n) + h;
83-
});
81+
export const sdPlane = tgpu.fn([vec3f, vec3f, f32], f32)(
82+
(point, normal, height) => {
83+
return dot(point, normal) + height;
84+
},
85+
);
8486

8587
/**
8688
* Signed distance function for a 3D capsule
87-
* @param p Point to evaluate
88-
* @param a First endpoint of the capsule segment
89-
* @param b Second endpoint of the capsule segment
89+
* @param point Point to evaluate
90+
* @param A First endpoint of the capsule segment
91+
* @param B Second endpoint of the capsule segment
9092
* @param radius Radius of the capsule
9193
*/
9294
export const sdCapsule = tgpu
93-
.fn([vec3f, vec3f, vec3f, f32], f32)((p, a, b, radius) => {
94-
const pa = sub(p, a);
95-
const ba = sub(b, a);
95+
.fn([vec3f, vec3f, vec3f, f32], f32)((point, A, B, radius) => {
96+
const pa = sub(point, A);
97+
const ba = sub(B, A);
9698
const h = saturate(dot(pa, ba) / dot(ba, ba));
9799
return length(sub(pa, ba.mul(h))) - radius;
98100
});

packages/typegpu-sdf/src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export * from './operators.ts';
2-
31
export {
42
sdBezier,
53
sdBezierApprox,
@@ -9,6 +7,7 @@ export {
97
sdPie,
108
sdRoundedBox2d,
119
} from './2d.ts';
10+
1211
export {
1312
sdBox3d,
1413
sdBoxFrame3d,
@@ -18,3 +17,12 @@ export {
1817
sdRoundedBox3d,
1918
sdSphere,
2019
} from './3d.ts';
20+
21+
export {
22+
opExtrudeX,
23+
opExtrudeY,
24+
opExtrudeZ,
25+
opSmoothDifference,
26+
opSmoothUnion,
27+
opUnion,
28+
} from './operators.ts';

0 commit comments

Comments
 (0)