Skip to content

Commit 1b5ba58

Browse files
committed
@typegpu/geometry Improve line rendering APIs
1 parent f935fbb commit 1b5ba58

17 files changed

Lines changed: 186 additions & 75 deletions

File tree

apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
joinSlot,
66
lineSegmentIndices,
77
lineSegmentLeftIndices,
8-
lineSegmentVariableWidth,
8+
polylineVariableWidth,
99
lineSegmentWireframeIndices,
1010
startCapSlot,
11+
arrowCapParamsSlot,
1112
} from '@typegpu/geometry';
1213
import tgpu, { type ColorAttachment } from 'typegpu';
1314
import {
@@ -140,7 +141,7 @@ const mainVertex = tgpu.vertexFn({
140141
return Out();
141142
}
142143

143-
const result = lineSegmentVariableWidth(vertexIndex, A, B, C, D, MAX_JOIN_COUNT);
144+
const result = polylineVariableWidth(A, B, C, D, vertexIndex, MAX_JOIN_COUNT);
144145

145146
return {
146147
outPos: vec4f(result.vertexPosition * result.w, 0, result.w),
@@ -287,6 +288,7 @@ function createPipelines() {
287288
.with(startCapSlot, startCap)
288289
.with(endCapSlot, endCap)
289290
.with(testCaseSlot, testCase)
291+
.with(arrowCapParamsSlot, { length: 7.5, width: 2 })
290292
.createRenderPipeline({
291293
vertex: mainVertex,
292294
fragment: mainFragment,
@@ -303,6 +305,7 @@ function createPipelines() {
303305
.with(startCapSlot, startCap)
304306
.with(endCapSlot, endCap)
305307
.with(testCaseSlot, testCase)
308+
.with(arrowCapParamsSlot, { length: 7.5, width: 2 })
306309
.createRenderPipeline({
307310
vertex: mainVertex,
308311
fragment: outlineFragment,

apps/typegpu-docs/src/examples/simulation/wind-map/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
endCapSlot,
44
LineControlPoint,
55
lineSegmentIndices,
6-
lineSegmentVariableWidth,
6+
polylineVariableWidth,
77
startCapSlot,
88
} from '@typegpu/geometry';
99
import tgpu from 'typegpu';
@@ -177,7 +177,7 @@ const mainVertex = tgpu.vertexFn({
177177
radius: lineWidth(f32(trailIndexOriginal + 3) / (TRAIL_LENGTH - 1)),
178178
});
179179

180-
const result = lineSegmentVariableWidth(vertexIndex, A, B, C, D, MAX_JOIN_COUNT);
180+
const result = polylineVariableWidth(A, B, C, D, vertexIndex, MAX_JOIN_COUNT);
181181

182182
return {
183183
outPos: vec4f(result.vertexPosition, 0, 1),

apps/typegpu-docs/tests/individual-example-tests/global-wind-map.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('global wind map example', () => {
228228
return (join.C.position + (dir * join.C.radius));
229229
}
230230
231-
fn lineSegmentVariableWidth(vertexIndex: u32, A: LineControlPoint, B: LineControlPoint, C: LineControlPoint, D: LineControlPoint, maxJoinCount: u32) -> LineSegmentOutput {
231+
fn polylineVariableWidth(A: LineControlPoint, B: LineControlPoint, C: LineControlPoint, D: LineControlPoint, vertexIndex: u32, maxJoinCount: u32) -> LineSegmentOutput {
232232
var AB = (B.position - A.position);
233233
var BC = (C.position - B.position);
234234
var DC = (C.position - D.position);
@@ -323,7 +323,7 @@ describe('global wind map example', () => {
323323
var B = LineControlPoint((*particle).positions[iB], lineWidth((f32((trailIndexOriginal + 1u)) / 19f)));
324324
var C = LineControlPoint((*particle).positions[iC], lineWidth((f32((trailIndexOriginal + 2u)) / 19f)));
325325
var D = LineControlPoint((*particle).positions[iD], lineWidth((f32((trailIndexOriginal + 3u)) / 19f)));
326-
var result = lineSegmentVariableWidth(vertexIndex, A, B, C, D, 3u);
326+
var result = polylineVariableWidth(A, B, C, D, vertexIndex, 3u);
327327
return mainVertex_Output(vec4f(result.vertexPosition, 0f, 1f), result.vertexPosition, (f32(trailIndexOriginal) / 19f));
328328
}
329329

apps/typegpu-docs/tests/individual-example-tests/lines-combinations.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe('lines combinations example', () => {
177177
return (join.C.position + (dir * join.C.radius));
178178
}
179179
180-
fn lineSegmentVariableWidth(vertexIndex: u32, A: LineControlPoint, B: LineControlPoint, C: LineControlPoint, D: LineControlPoint, maxJoinCount: u32) -> LineSegmentOutput {
180+
fn polylineVariableWidth(A: LineControlPoint, B: LineControlPoint, C: LineControlPoint, D: LineControlPoint, vertexIndex: u32, maxJoinCount: u32) -> LineSegmentOutput {
181181
var AB = (B.position - A.position);
182182
var BC = (C.position - B.position);
183183
var DC = (C.position - D.position);
@@ -263,7 +263,7 @@ describe('lines combinations example', () => {
263263
if (((((A.radius < 0f) || (B.radius < 0f)) || (C.radius < 0f)) || (D.radius < 0f))) {
264264
return mainVertex_Output();
265265
}
266-
var result = lineSegmentVariableWidth(vertexIndex, A, B, C, D, 6u);
266+
var result = polylineVariableWidth(A, B, C, D, vertexIndex, 6u);
267267
return mainVertex_Output(vec4f((result.vertexPosition * result.w), 0f, result.w), result.vertexPosition, vec2f(0f, select(0f, 1f, (vertexIndex > 1u))), instanceIndex, vertexIndex, 0u);
268268
}
269269

packages/typegpu-geometry/LinesExplanation.md

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ limited. It allows only single pixel width lines! Good for debugging, but
1111
terrible for anything user-facing. With TypeGPU, it is easier than ever to
1212
create reusable, composable libraries. And now, high quality line rendering is
1313
just an `npm install` away! This article serves as documentation for the library
14-
source code. It should also give you an appreciation for how complex line
15-
rendering can actually get.
14+
source code. It should also give you an appreciation for just how complex line
15+
rendering can get!
1616

1717
## Goals
1818

1919
As already described in many online articles, drawing lines using GPU can be
20-
notoriously difficult to do well. There are many different, sometimes
20+
quite involved. There are many different, sometimes
2121
conflicting, goals you might have when it comes to line rendering. Following
2222
goals are considered by this article and implementation:
2323

@@ -34,74 +34,74 @@ goals are considered by this article and implementation:
3434
### Non-goals
3535

3636
- maximum performance
37-
- triangle counts
38-
- minimizing quad-overdraw (having max-area triangles)
37+
- minimizing triangle counts
38+
- minimizing quad-overdraw (producing max-area triangles)
3939

40-
## Single Line Segment
40+
## Single Line
4141

42-
We start with a single segment. Two vertices, `C1` and `C2` (C for center), and
43-
radii `r1` and `r2`.
42+
We start with a single line. Two control points, $A$ and $B$ with
43+
radii $r_A$ and $r_B$.
4444

45-
![-](./assets/basic.svg)
45+
<img src="./assets/basic.svg" style="display: block; margin: 0 auto; width: 100%; max-width: 400px;" />
4646

47-
Two most important directions to compute are `nL` and `nR`, left (CCW) and right
48-
(CW) external tangent **normals**.
47+
Two most important directions to compute are $\hat{n_L}$ and $\hat{n_R}$, left (CCW) and right
48+
(CW) external tangent **normals** (with respect to $\vec{AB}$).
4949

50-
```ts
51-
x = (r1 - r2) / distance(C1, C2);
52-
y = sqrt(1 - x ^ 2);
53-
nL = vec2(x, y);
54-
nR = vec2(x, -y);
50+
```math
51+
x = \frac{r1 - r2}{\|{AB}\|} \\
52+
y = \sqrt{1 - x ^ 2} \\
53+
\hat{n_L} = (x, y) \\
54+
\hat{n_R} = (x, -y)
5555
```
5656

57-
NOTE: in `externalNormals.ts`, additional care is taken to return `nL` and `nR`
58-
rotated relative to the `distance` vector between the circles.
57+
NOTE: in `externalNormals.ts`, additional care is taken to return $\hat{n_L}$ and $\hat{n_R}$
58+
rotated relative to $\vec{AB}$.
5959

6060
Using these two directions, it is trivial to compute all other points necessary
6161
for triangulation:
6262

63-
![-](./assets/triangulation.svg)
63+
<img src="./assets/triangulation.svg" style="display: block; margin: 0 auto; width: 100%; max-width: 400px;" />
6464

65-
**Core** (red) vertices 0-5 are:
65+
**Core** 🔴 vertices 0-5 are:
6666

67-
```ts
68-
v0 = C1;
69-
v1 = C2;
70-
v2 = C1 + r1 * nL;
71-
v3 = C1 + r1 * nR;
72-
v4 = C2 + r2 * nR;
73-
v5 = C2 + r2 * nL;
67+
```math
68+
{\color{red} v_0} = A \\
69+
{\color{red} v_1} = B \\
70+
{\color{red} v_2} = A + r_A n_L \\
71+
{\color{red} v_3} = A + r_A n_R \\
72+
{\color{red} v_4} = B + r_B n_R \\
73+
{\color{red} v_5} = B + r_B n_L
7474
```
7575

76-
Vertices 6+ are called `join` vertices in the code, however they are used for
76+
Vertices 6+ are called `join` vertices in the code, but they are used for
7777
both `joins` and `caps`. Their computation will depend on the type of `join` or
78-
`cap` used. Here, a `round` cap is shown. It is important to note their
78+
`cap` used. Here, an (incomplete) `round` cap is shown. It is important to note their
7979
distribution. Just like the 4 core vertices 2-5, they are distributed CCW
8080
progressively further away from their respective core vertex. This makes it
8181
possible to dynamically vary the number of segments in the joins, while using
8282
the same index buffer. Each join vertex is identified by:
8383

84-
- `coreVertexIndex` which core vertex it belongs to (`v2,v6,v10=0`,
85-
`v3,v7,v11=1` etc.)
86-
- `joinVertexIndex` number of vertices away from the core vertex (`v2=0`,
87-
`v6=1`, `v10=2`)
84+
- `coreVertexIndex` which core vertex it belongs to
85+
- `joinVertexIndex` number of vertices away from the core vertex
8886

8987
```ts
90-
coreVertexIndex = (vertexIndex - 2) % 4;
91-
joinVertexIndex = (vertexIndex - 2) / 4;
88+
const coreVertexIndex = (vertexIndex - 2) % 4;
89+
const joinVertexIndex = (vertexIndex - 2) / 4;
9290
```
9391

94-
NOTE: instead of the slow `% 4` and `/ 4`, real code uses `& 0b11` and `>> 2`.
95-
Probably can be optimized away by wgsl compilers, but you never know.
92+
NOTE: real code uses `& 0b11` and `>> 2` instead of div.
9693

97-
Cap functions can then use `joinVertexIndex` and `maxJoinCount` to compute the
94+
Cap functions can then use `joinVertexIndex / MAX_JOIN_COUNT` to compute the
9895
final position of each join vertex.
9996

10097
If all you want to do is render single line segments, you should use
101-
`singleLineSegmentVariableWidth(A, B)` function. It does exactly what we just
98+
`lineVariableWidth(A, B, vertexIndex, MAX_JOIN_COUNT)` function. It does exactly what we just
10299
discussed and nothing more.
103100

104-
## Joining Line Segments
101+
## Polylines
102+
103+
Easy part is done. Joining segments into polylines is what makes line rendering interesting! First, lets consider how in theory the joining should work. A polyline consists of many joined segments. Each segment's geometry depends on its two neighboring segments. This is why the function accepts 4 consecutive control points $A, B, C, D$ with radii $r_A, r_B, r_C, r_D$.
104+
105+
<img src="./assets/polyline-basic.svg" style="display: block; margin: 0 auto; width: 100%; max-width: 400px;" />
105106

106-
Easy part is done. Joining segments is where the difficulties and edge cases
107-
start. First, lets consider how in theory the joining should work:
107+
The segment that actually gets drawn is $BC$, shaded in $\color{blue} blue$. The function needs $A$ and $D$ in order to compute the join geometry, but it does not create the red shaded regions. Darker shaded regions highlight the join area. Notice that only half the join is handled by each segment.

0 commit comments

Comments
 (0)