@dylanebert/shallot-grid 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/index.ts +8 -3
- package/src/shader.ts +49 -29
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# shallot-grid
|
|
2
2
|
|
|
3
|
-
An infinite world grid with axis lines for [Shallot](https://github.com/dylanebert/shallot). One fullscreen pass draws the y=0 plane. Each pixel picks two decade levels from its own screen footprint and cross-fades between them, so the grid reads at any zoom without popping. The horizon fade scales with camera height, and red X, green Y and blue Z axis lines draw in the same pass.
|
|
3
|
+
An infinite world grid with axis lines for [Shallot](https://github.com/dylanebert/shallot). One fullscreen pass draws the y=0 plane. Each pixel picks two decade levels per line direction from its own screen footprint and cross-fades between them, so the grid reads at any zoom without popping. The horizon fade scales with camera height, and red X, green Y and blue Z axis lines draw in the same pass.
|
|
4
4
|
|
|
5
5
|
## Enabling it
|
|
6
6
|
|
|
@@ -28,12 +28,13 @@ Add a `<a grid />` singleton to your scene to show it:
|
|
|
28
28
|
Every field is optional. Colors are sRGB hex with alpha (`0xRRGGBBAA`), and an axis with alpha `00` is hidden:
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
<a grid="neutral: 0x504945ff; axis-x: 0xcc241dff; axis-y: 0x6b9d65ff; axis-z: 0x458588ff; opacity: 1; fade: 20; cells:
|
|
31
|
+
<a grid="neutral: 0x504945ff; axis-x: 0xcc241dff; axis-y: 0x6b9d65ff; axis-z: 0x458588ff; opacity: 1; fade: 20; cells: 40; floor: 1" />
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
- `opacity`: the whole grid's opacity.
|
|
35
35
|
- `fade`: horizon fade reach in camera heights; `0` turns it off.
|
|
36
|
-
- `cells`: pixels per cell at a decade boundary; the finest level
|
|
36
|
+
- `cells`: pixels per cell at a decade boundary; the finest level fades in as its cells grow from `cells / 10` to `cells` pixels, so at `40` it is gone by 4 px and full by 40 px.
|
|
37
|
+
- `floor`: the smallest cell in metres; no finer decade draws, and closer in that level grows on screen.
|
|
37
38
|
|
|
38
39
|
## Layout
|
|
39
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dylanebert/shallot-grid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Infinite world grid with axis lines for Shallot: a fullscreen ray-plane pass that blends decade levels so it reads at any zoom.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dylan Ebert <dylan.ebert@gmail.com>",
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { GRID_AT, GRID_BYTES, GRID_FLOATS, GRID_SHADER } from "./shader";
|
|
|
30
30
|
*
|
|
31
31
|
* @example
|
|
32
32
|
* ```
|
|
33
|
-
* <a grid="axis-y: 0x6b9d6500; fade: 40; cells:
|
|
33
|
+
* <a grid="axis-y: 0x6b9d6500; fade: 40; cells: 48; floor: 0.1" />
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
export const Grid = {
|
|
@@ -46,8 +46,10 @@ export const Grid = {
|
|
|
46
46
|
opacity: sparse(f32),
|
|
47
47
|
/** horizon fade reach in camera heights: the plane fades out by `fade × height` (0 = off) */
|
|
48
48
|
fade: sparse(f32),
|
|
49
|
-
/** pixels per cell at a decade boundary: the
|
|
49
|
+
/** pixels per cell at a decade boundary: the finer level fades in as its cells grow from k/10 to k pixels */
|
|
50
50
|
cells: sparse(f32),
|
|
51
|
+
/** smallest cell in metres: no finer decade draws, and below it this level grows on screen */
|
|
52
|
+
floor: sparse(f32),
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
/** the look a `Grid` singleton carries, one number per field. */
|
|
@@ -60,7 +62,8 @@ export const GRID_DEFAULTS: GridStyle = {
|
|
|
60
62
|
axisZ: 0x458588ff,
|
|
61
63
|
opacity: 1,
|
|
62
64
|
fade: 20,
|
|
63
|
-
cells:
|
|
65
|
+
cells: 40,
|
|
66
|
+
floor: 1,
|
|
64
67
|
};
|
|
65
68
|
|
|
66
69
|
function packColorAt(rgba: number, out: Float32Array, at: number): void {
|
|
@@ -81,6 +84,7 @@ export function packGrid(style: GridStyle, out: Float32Array): void {
|
|
|
81
84
|
out[GRID_AT.params + 1] = style.fade;
|
|
82
85
|
out[GRID_AT.params + 2] = style.cells;
|
|
83
86
|
out[GRID_AT.params + 3] = 0;
|
|
87
|
+
out[GRID_AT.floor] = style.floor;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
function readGrid(eid: number): GridStyle {
|
|
@@ -92,6 +96,7 @@ function readGrid(eid: number): GridStyle {
|
|
|
92
96
|
opacity: Grid.opacity.get(eid),
|
|
93
97
|
fade: Grid.fade.get(eid),
|
|
94
98
|
cells: Grid.cells.get(eid),
|
|
99
|
+
floor: Grid.floor.get(eid),
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
|
package/src/shader.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// The world grid shader: one fullscreen ray-plane pass over the y=0 plane plus the vertical Y axis.
|
|
2
2
|
//
|
|
3
|
-
// Decade blend.
|
|
4
|
-
// pixel
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// Decade blend, per direction. Lines across x (constant x) read their footprint `f = fwidth(p.x)`, metres
|
|
4
|
+
// per pixel along x, and lines across z read `fwidth(p.z)`, so a grazing view that stretches one direction
|
|
5
|
+
// never fuses the other. With the cells constant `k`, `l = log10(f * k)` names each direction's decade:
|
|
6
|
+
// the finer level's cell is `max(10^floor(l), floor)` metres and the coarser level's is ten times that.
|
|
7
|
+
// An unfloored finer cell spans between k/10 and k pixels and its coarser cell between k and 10k. Both
|
|
8
|
+
// draw with Ben Golus's pristine line primitive (PlayCanvas `scripts/esm/grid.mjs`, `pristineGrid`) at a
|
|
9
|
+
// constant screen width. The finer level is weighted by its cell size in pixels,
|
|
10
|
+
// `smoothstep(k/10, k, cellPx)`, Blender's `overlay_grid_frag.glsl` shape: at the default `k` of 40 it is
|
|
11
|
+
// gone by 4 px and full by 40 px, so no level draws with cells near line width. The coarser level draws
|
|
12
|
+
// at full weight, combined with `max`. Every coarse line is also a fine line, so as the fine level's
|
|
13
|
+
// cells shrink to k/10 pixels it has faded to nothing, and at the next decade the coarse level, at k
|
|
14
|
+
// pixels, becomes the fine level at full weight. Nothing pops, and no zoom runs out of levels.
|
|
15
|
+
//
|
|
16
|
+
// Floor. The finest decade never drops under `floor` metres: below the zoom where it would, the floored
|
|
17
|
+
// level grows on screen at full weight and no finer level appears.
|
|
13
18
|
//
|
|
14
19
|
// View-relative fade. The footprint already fades the fine level. The plane also fades toward the
|
|
15
20
|
// horizon by `1 - smoothstep(0, fade * h, dist)`, where `h` is the camera's height above the plane and
|
|
@@ -22,7 +27,9 @@
|
|
|
22
27
|
// plane.
|
|
23
28
|
//
|
|
24
29
|
// Depth is reverse-Z (near 1, far 0): the pass tests `greater-equal` with depth writes off and writes
|
|
25
|
-
// the winning element's depth through `frag_depth`.
|
|
30
|
+
// the winning element's depth through `frag_depth`. A grid has no edge of range: a plane or axis point
|
|
31
|
+
// beyond the camera's far plane still draws, its depth clamped to the far plane's 0, so the horizon fade
|
|
32
|
+
// alone ends the grid. Only a point behind the camera or nearer than the near plane is rejected.
|
|
26
33
|
|
|
27
34
|
/** f32 lane offsets of the `Grid` uniform, as the WGSL struct below lays it out. */
|
|
28
35
|
export const GRID_AT = {
|
|
@@ -34,10 +41,11 @@ export const GRID_AT = {
|
|
|
34
41
|
axisY: 44,
|
|
35
42
|
axisZ: 48,
|
|
36
43
|
params: 52,
|
|
44
|
+
floor: 56,
|
|
37
45
|
} as const;
|
|
38
46
|
|
|
39
47
|
/** byte size of the `Grid` uniform. */
|
|
40
|
-
export const GRID_BYTES =
|
|
48
|
+
export const GRID_BYTES = 240;
|
|
41
49
|
/** `Grid` uniform size in f32 lanes. */
|
|
42
50
|
export const GRID_FLOATS = GRID_BYTES / 4;
|
|
43
51
|
|
|
@@ -51,6 +59,7 @@ struct Grid {
|
|
|
51
59
|
axisY: vec4<f32>,
|
|
52
60
|
axisZ: vec4<f32>,
|
|
53
61
|
params: vec4<f32>,
|
|
62
|
+
floor: f32,
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
@group(0) @binding(0) var<uniform> grid: Grid;
|
|
@@ -90,17 +99,29 @@ struct FragOut {
|
|
|
90
99
|
@location(0) color: vec4<f32>,
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
// Ben Golus's pristine grid for thin lines:
|
|
94
|
-
// width the line width in cells. Coverage fades to the line's mean coverage as cells shrink under a pixel.
|
|
95
|
-
fn pristine(
|
|
96
|
-
let goal = min(width,
|
|
97
|
-
let drawWidth = clamp(goal, deriv,
|
|
102
|
+
// Ben Golus's pristine grid for thin lines along one direction: u in cells, deriv the cell footprint of one
|
|
103
|
+
// pixel, width the line width in cells. Coverage fades to the line's mean coverage as cells shrink under a pixel.
|
|
104
|
+
fn pristine(u: f32, deriv: f32, width: f32) -> f32 {
|
|
105
|
+
let goal = min(width, 0.5);
|
|
106
|
+
let drawWidth = clamp(goal, deriv, 0.5);
|
|
98
107
|
let aa = deriv * 1.5;
|
|
99
|
-
let g = 1.0 - abs(fract(
|
|
108
|
+
let g = 1.0 - abs(fract(u) * 2.0 - 1.0);
|
|
100
109
|
var g2 = 1.0 - smoothstep(drawWidth - aa, drawWidth + aa, g);
|
|
101
110
|
g2 = g2 * saturate(goal / drawWidth);
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
return mix(g2, goal, saturate(deriv * 2.0 - 1.0));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// one direction's two decade levels: coord the plane coordinate, f its footprint in metres per pixel,
|
|
115
|
+
// deriv its anti-aliasing footprint
|
|
116
|
+
fn decades(coord: f32, f: f32, deriv: f32, cells: f32, minCell: f32) -> f32 {
|
|
117
|
+
let foot = max(f, 1e-12);
|
|
118
|
+
let l = log(foot * cells) * INV_LN10;
|
|
119
|
+
let fine = max(pow(10.0, floor(l)), minCell);
|
|
120
|
+
let coarse = fine * 10.0;
|
|
121
|
+
let fineWeight = smoothstep(cells * 0.1, cells, fine / foot);
|
|
122
|
+
let gFine = pristine(coord / fine, deriv / fine, deriv * LINE_PX / fine);
|
|
123
|
+
let gCoarse = pristine(coord / coarse, deriv / coarse, deriv * LINE_PX / coarse);
|
|
124
|
+
return max(gCoarse, gFine * fineWeight);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
fn axisCoverage(px: f32) -> f32 {
|
|
@@ -110,7 +131,8 @@ fn axisCoverage(px: f32) -> f32 {
|
|
|
110
131
|
fn depthOf(p: vec3<f32>) -> f32 {
|
|
111
132
|
let clip = grid.viewProj * vec4(p, 1.0);
|
|
112
133
|
if (clip.w <= 0.0) { return -1.0; }
|
|
113
|
-
|
|
134
|
+
// past the far plane clamps to the far plane's depth
|
|
135
|
+
return max(clip.z / clip.w, 0.0);
|
|
114
136
|
}
|
|
115
137
|
|
|
116
138
|
fn over(front: vec4<f32>, back: vec4<f32>) -> vec4<f32> {
|
|
@@ -125,6 +147,7 @@ fn fs(input: VSOut) -> FragOut {
|
|
|
125
147
|
let opacity = grid.params.x;
|
|
126
148
|
let fade = grid.params.y;
|
|
127
149
|
let cells = max(grid.params.z, 1e-3);
|
|
150
|
+
let minCell = max(grid.floor, 0.0);
|
|
128
151
|
|
|
129
152
|
let origin = input.nearPoint;
|
|
130
153
|
let ray = input.farPoint - input.nearPoint;
|
|
@@ -135,7 +158,8 @@ fn fs(input: VSOut) -> FragOut {
|
|
|
135
158
|
let dx = dpdx(p.xz);
|
|
136
159
|
let dy = dpdy(p.xz);
|
|
137
160
|
let deriv = vec2(length(vec2(dx.x, dy.x)), length(vec2(dx.y, dy.y)));
|
|
138
|
-
let
|
|
161
|
+
let fx = fwidth(p.x);
|
|
162
|
+
let fz = fwidth(p.z);
|
|
139
163
|
|
|
140
164
|
// Y axis: closest approach of the ray to x=z=0, measured in the xz plane
|
|
141
165
|
let rayXZ = ray.xz;
|
|
@@ -153,13 +177,9 @@ fn fs(input: VSOut) -> FragOut {
|
|
|
153
177
|
planeDepth = depthOf(p);
|
|
154
178
|
}
|
|
155
179
|
if (hit && planeDepth >= 0.0 && planeDepth <= 1.0) {
|
|
156
|
-
let
|
|
157
|
-
let
|
|
158
|
-
|
|
159
|
-
let fineWeight = 1.0 - fract(l);
|
|
160
|
-
let gFine = pristine(p.xz / fine, deriv / fine, deriv * LINE_PX / fine);
|
|
161
|
-
let gCoarse = pristine(p.xz / coarse, deriv / coarse, deriv * LINE_PX / coarse);
|
|
162
|
-
var alpha = max(gCoarse, gFine * fineWeight) * grid.neutral.a;
|
|
180
|
+
let gx = decades(p.x, fx, deriv.x, cells, minCell);
|
|
181
|
+
let gz = decades(p.z, fz, deriv.y, cells, minCell);
|
|
182
|
+
var alpha = mix(gx, 1.0, gz) * grid.neutral.a;
|
|
163
183
|
var color = grid.neutral.rgb;
|
|
164
184
|
|
|
165
185
|
let xCov = axisCoverage(abs(p.z) / max(deriv.y, 1e-12)) * grid.axisX.a;
|