@hello-terrain/three 0.0.0-alpha.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 ADDED
@@ -0,0 +1,42 @@
1
+ # @hello-terrain/three
2
+ Realtime web terrain engine, for vast virtual worlds. Built for [three.js](https://threejs.org/).
3
+ ## Features
4
+
5
+ - Performant variable LOD system for huge (earth-scale!) open worlds
6
+ - Elevation manipulation, terrain holes, texture painting, overlays, colors, and wetness
7
+ - TSL-based elevation and texture assignment nodes
8
+ - Composable compute stage plugins
9
+
10
+ ## Getting Started
11
+
12
+ 1. Read the [Introduction](http://hello-terrain.kenny.wtf/docs) to understand the architecture and see how it's used.
13
+
14
+ 2. Read the [Installation](http://hello-terrain.kenny.wtf/docs/installation) instructions.
15
+
16
+ 3. Review the [Examples](http://hello-terrain.kenny.wtf/examples) to get an idea of how to do things.
17
+
18
+ 4. For support, join the [Discord server](https://discord.gg/HgTd2B828n).
19
+
20
+ ## Project Architecture
21
+ This library uses [unbuild](https://github.com/unjs/unbuild) for building.
22
+ - `src/index.ts` is the main entry point for your library exports
23
+ - Add your library code in the `src` folder
24
+ - `tests/` contains your test files
25
+
26
+
27
+ ## Libraries
28
+ The following libraries are used - checkout the linked docs to learn more
29
+ - [unbuild](https://github.com/unjs/unbuild) - Unified JavaScript build system
30
+
31
+
32
+ ## Tools
33
+ - [Vitest](https://vitest.dev/) - Fast unit test framework powered by Vite
34
+ - [Oxlint](https://oxc.rs/docs/guide/usage/linter) - A fast linter for JavaScript and TypeScript
35
+ - [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) - Fast Prettier-compatible code formatter
36
+
37
+
38
+ ## Development Commands
39
+ - `pnpm install` to install the dependencies
40
+ - `pnpm run build` to build the library into the `dist` folder
41
+ - `pnpm run test` to run the tests
42
+ - `pnpm run release` to build and publish to npm
package/dist/index.cjs ADDED
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ const three = require('three');
4
+ const tsl = require('three/tsl');
5
+
6
+ class TerrainGeometry extends three.BufferGeometry {
7
+ constructor(innerSegments = 14, extendUV = false) {
8
+ super();
9
+ if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
10
+ throw new Error(
11
+ `Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
12
+ );
13
+ }
14
+ try {
15
+ this.setIndex(this.generateIndices(innerSegments));
16
+ this.setAttribute(
17
+ "position",
18
+ new three.BufferAttribute(
19
+ new Float32Array(this.generatePositions(innerSegments)),
20
+ 3
21
+ )
22
+ );
23
+ this.setAttribute(
24
+ "normal",
25
+ new three.BufferAttribute(
26
+ new Float32Array(this.generateNormals(innerSegments)),
27
+ 3
28
+ )
29
+ );
30
+ this.setAttribute(
31
+ "uv",
32
+ new three.BufferAttribute(
33
+ new Float32Array(
34
+ extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
35
+ ),
36
+ 2
37
+ )
38
+ );
39
+ } catch (error) {
40
+ console.error("Error creating TerrainGeometry:", error);
41
+ throw error;
42
+ }
43
+ }
44
+ /**
45
+ * Generate indices for terrain geometry with proper skirt corner handling.
46
+ * The key improvement is in how corner triangles are subdivided.
47
+ */
48
+ /**
49
+ * Generate indices for terrain geometry with proper skirt corner handling.
50
+ *
51
+ * The mesh layout is a regular grid (with duplicated outermost ring for skirt):
52
+ *
53
+ * SKIRT RING (rotational symmetry for proper corners):
54
+ * o---o---o---o---o
55
+ * | \ | / | \ | / |
56
+ * o---o---o---o---o
57
+ * | / | | \ |
58
+ * o---o o---o
59
+ * | \ | | / |
60
+ * o---o---o---o---o
61
+ * | / | \ | / | \ |
62
+ * o---o---o---o---o
63
+ *
64
+ * INNER GRID (consistent diagonal, no rotational symmetry):
65
+ * o---o---o
66
+ * | \ | \ |
67
+ * o---o---o
68
+ * | \ | \ |
69
+ * o---o---o
70
+ *
71
+ * Where o = vertex
72
+ * Each square cell is split into 2 triangles.
73
+ * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
74
+ * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
75
+ *
76
+ * Vertex layout (for innerSegments = 2):
77
+ *
78
+ * 0----1----2----3----4
79
+ * | | | | |
80
+ * 5----6----7----8----9
81
+ * | | | | |
82
+ * 10---11---12---13---14
83
+ * | | | | |
84
+ * 15---16---17---18---19
85
+ * | | | | |
86
+ * 20---21---22---23---24
87
+ *
88
+ * For each cell:
89
+ * a = top-left,
90
+ * b = top-right,
91
+ * c = bottom-left,
92
+ * d = bottom-right (all as flat array indices)
93
+ *
94
+ * Diagonal a-d:
95
+ * triangle 1: a, d, b
96
+ * triangle 2: a, c, d
97
+ * Diagonal b-c:
98
+ * triangle 1: a, c, b
99
+ * triangle 2: b, c, d
100
+ */
101
+ generateIndices(innerSegments) {
102
+ const innerEdgeVertexCount = innerSegments + 1;
103
+ const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
104
+ const indices = [];
105
+ const cellsPerEdge = edgeVertexCountWithSkirt - 1;
106
+ const mid = Math.floor(cellsPerEdge / 2);
107
+ for (let y = 0; y < cellsPerEdge; y++) {
108
+ for (let x = 0; x < cellsPerEdge; x++) {
109
+ const a = y * edgeVertexCountWithSkirt + x;
110
+ const b = a + 1;
111
+ const c = a + edgeVertexCountWithSkirt;
112
+ const d = c + 1;
113
+ const isSkirtCell = x === 0 || x === cellsPerEdge - 1 || y === 0 || y === cellsPerEdge - 1;
114
+ let useDefaultDiagonal;
115
+ if (isSkirtCell) {
116
+ const leftHalf = x < mid;
117
+ const topHalf = y < mid;
118
+ useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
119
+ } else {
120
+ useDefaultDiagonal = true;
121
+ }
122
+ if (useDefaultDiagonal) {
123
+ indices.push(a, d, b);
124
+ indices.push(a, c, d);
125
+ } else {
126
+ indices.push(a, c, b);
127
+ indices.push(b, c, d);
128
+ }
129
+ }
130
+ }
131
+ return indices;
132
+ }
133
+ /**
134
+ * Generate vertex positions for the terrain with skirts.
135
+ * Positions are normalized to [-0.5, 0.5] range.
136
+ */
137
+ generatePositions(innerSegments) {
138
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
139
+ const positions = [];
140
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
141
+ const v = Math.min(Math.max((iy - 1) / innerSegments, 0), 1);
142
+ const z = v - 0.5;
143
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
144
+ const u = Math.min(Math.max((ix - 1) / innerSegments, 0), 1);
145
+ const x = u - 0.5;
146
+ positions.push(x, 0, z);
147
+ }
148
+ }
149
+ return positions;
150
+ }
151
+ /**
152
+ * Generate UV coordinates for the inner grid only (skirt duplicates clamped to border).
153
+ * UVs are normalized to [0, 1] range with flipped V.
154
+ */
155
+ generateUvsOnlyInner(innerSegments) {
156
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
157
+ const uvs = [];
158
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
159
+ const v = Math.min(Math.max((iy - 1) / innerSegments, 0), 1);
160
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
161
+ const u = Math.min(Math.max((ix - 1) / innerSegments, 0), 1);
162
+ uvs.push(u, 1 - v);
163
+ }
164
+ }
165
+ return uvs;
166
+ }
167
+ /**
168
+ * Generate UVs that extend 1 extra unit outward to the skirt ring.
169
+ * Map the entire geometry (including skirts) into [0,1] so side faces
170
+ * receive proper UVs without relying on texture wrapping. V is flipped.
171
+ */
172
+ generateUvsExtended(innerSegments) {
173
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
174
+ const uvs = [];
175
+ const denom = edgeVertexCountWithSkirt - 1;
176
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
177
+ const v = iy / denom;
178
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
179
+ const u = ix / denom;
180
+ uvs.push(u, 1 - v);
181
+ }
182
+ }
183
+ return uvs;
184
+ }
185
+ /**
186
+ * Generate vertex normals.
187
+ */
188
+ generateNormals(innerSegments) {
189
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
190
+ const last = edgeVertexCountWithSkirt - 1;
191
+ const normals = [];
192
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
193
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
194
+ const onEdgeX = ix === 0 || ix === last;
195
+ const onEdgeY = iy === 0 || iy === last;
196
+ if (onEdgeX || onEdgeY) {
197
+ let nx = 0;
198
+ let nz = 0;
199
+ if (ix === 0) nx -= 1;
200
+ if (ix === last) nx += 1;
201
+ if (iy === 0) nz -= 1;
202
+ if (iy === last) nz += 1;
203
+ const len = Math.hypot(nx, nz);
204
+ if (len > 0) {
205
+ normals.push(nx / len, 0, nz / len);
206
+ } else {
207
+ normals.push(0, 1, 0);
208
+ }
209
+ } else {
210
+ normals.push(0, 1, 0);
211
+ }
212
+ }
213
+ }
214
+ return normals;
215
+ }
216
+ }
217
+
218
+ const isSkirtVertex = tsl.Fn(([segments]) => {
219
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
220
+ const vIndex = tsl.int(tsl.vertexIndex);
221
+ const segmentEdges = tsl.int(segmentsNode.add(3));
222
+ const vx = vIndex.mod(segmentEdges);
223
+ const vy = vIndex.div(segmentEdges);
224
+ const last = segmentEdges.sub(tsl.int(1));
225
+ return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
226
+ });
227
+ const isSkirtUV = tsl.Fn(([segments]) => {
228
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
229
+ const ux = tsl.uv().x;
230
+ const uy = tsl.uv().y;
231
+ const segmentCount = segmentsNode.add(2);
232
+ const segmentStep = tsl.float(1).div(segmentCount);
233
+ const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
234
+ const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
235
+ return innerX.and(innerY).not();
236
+ });
237
+
238
+ exports.TerrainGeometry = TerrainGeometry;
239
+ exports.isSkirtUV = isSkirtUV;
240
+ exports.isSkirtVertex = isSkirtVertex;
@@ -0,0 +1,116 @@
1
+ import { BufferGeometry } from 'three';
2
+ import * as three_src_nodes_TSL_js from 'three/src/nodes/TSL.js';
3
+ import { Node } from 'three/webgpu';
4
+
5
+ /**
6
+ * Custom geometry for terrain tiles with properly handled skirts.
7
+ * This geometry ensures that corner triangles are subdivided correctly.
8
+ */
9
+ declare class TerrainGeometry extends BufferGeometry {
10
+ constructor(innerSegments?: number, extendUV?: boolean);
11
+ /**
12
+ * Generate indices for terrain geometry with proper skirt corner handling.
13
+ * The key improvement is in how corner triangles are subdivided.
14
+ */
15
+ /**
16
+ * Generate indices for terrain geometry with proper skirt corner handling.
17
+ *
18
+ * The mesh layout is a regular grid (with duplicated outermost ring for skirt):
19
+ *
20
+ * SKIRT RING (rotational symmetry for proper corners):
21
+ * o---o---o---o---o
22
+ * | \ | / | \ | / |
23
+ * o---o---o---o---o
24
+ * | / | | \ |
25
+ * o---o o---o
26
+ * | \ | | / |
27
+ * o---o---o---o---o
28
+ * | / | \ | / | \ |
29
+ * o---o---o---o---o
30
+ *
31
+ * INNER GRID (consistent diagonal, no rotational symmetry):
32
+ * o---o---o
33
+ * | \ | \ |
34
+ * o---o---o
35
+ * | \ | \ |
36
+ * o---o---o
37
+ *
38
+ * Where o = vertex
39
+ * Each square cell is split into 2 triangles.
40
+ * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
41
+ * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
42
+ *
43
+ * Vertex layout (for innerSegments = 2):
44
+ *
45
+ * 0----1----2----3----4
46
+ * | | | | |
47
+ * 5----6----7----8----9
48
+ * | | | | |
49
+ * 10---11---12---13---14
50
+ * | | | | |
51
+ * 15---16---17---18---19
52
+ * | | | | |
53
+ * 20---21---22---23---24
54
+ *
55
+ * For each cell:
56
+ * a = top-left,
57
+ * b = top-right,
58
+ * c = bottom-left,
59
+ * d = bottom-right (all as flat array indices)
60
+ *
61
+ * Diagonal a-d:
62
+ * triangle 1: a, d, b
63
+ * triangle 2: a, c, d
64
+ * Diagonal b-c:
65
+ * triangle 1: a, c, b
66
+ * triangle 2: b, c, d
67
+ */
68
+ private generateIndices;
69
+ /**
70
+ * Generate vertex positions for the terrain with skirts.
71
+ * Positions are normalized to [-0.5, 0.5] range.
72
+ */
73
+ private generatePositions;
74
+ /**
75
+ * Generate UV coordinates for the inner grid only (skirt duplicates clamped to border).
76
+ * UVs are normalized to [0, 1] range with flipped V.
77
+ */
78
+ private generateUvsOnlyInner;
79
+ /**
80
+ * Generate UVs that extend 1 extra unit outward to the skirt ring.
81
+ * Map the entire geometry (including skirts) into [0,1] so side faces
82
+ * receive proper UVs without relying on texture wrapping. V is flipped.
83
+ */
84
+ private generateUvsExtended;
85
+ /**
86
+ * Generate vertex normals.
87
+ */
88
+ private generateNormals;
89
+ }
90
+
91
+ /**
92
+ * Returns a node that is true for skirt vertices in the vertex stage.
93
+ *
94
+ * @remarks
95
+ * Only valid in the vertex shader. A vertex belongs to the skirt if it is on
96
+ * the outermost ring of the tile grid (first/last column or row). The grid
97
+ * resolution is derived from `segments`.
98
+ *
99
+ * @param segments - The number of inner segments in the terrain grid.
100
+ * @returns A node resolving to a boolean indicating a skirt vertex.
101
+ */
102
+ declare const isSkirtVertex: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
103
+ /**
104
+ * Returns a node that is true for skirt UVs.
105
+ *
106
+ * @remarks
107
+ * Uses interpolated UVs and the grid size
108
+ * from `segments` to mark fragments outside the inner range
109
+ * `(step, 1 - step)` on either axis as skirt, where `step = 1 / (segments + 2)`.
110
+ *
111
+ * @param segments - The number of inner segments in the terrain grid.
112
+ * @returns A node resolving to a boolean indicating a skirt fragment.
113
+ */
114
+ declare const isSkirtUV: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
115
+
116
+ export { TerrainGeometry, isSkirtUV, isSkirtVertex };
@@ -0,0 +1,116 @@
1
+ import { BufferGeometry } from 'three';
2
+ import * as three_src_nodes_TSL_js from 'three/src/nodes/TSL.js';
3
+ import { Node } from 'three/webgpu';
4
+
5
+ /**
6
+ * Custom geometry for terrain tiles with properly handled skirts.
7
+ * This geometry ensures that corner triangles are subdivided correctly.
8
+ */
9
+ declare class TerrainGeometry extends BufferGeometry {
10
+ constructor(innerSegments?: number, extendUV?: boolean);
11
+ /**
12
+ * Generate indices for terrain geometry with proper skirt corner handling.
13
+ * The key improvement is in how corner triangles are subdivided.
14
+ */
15
+ /**
16
+ * Generate indices for terrain geometry with proper skirt corner handling.
17
+ *
18
+ * The mesh layout is a regular grid (with duplicated outermost ring for skirt):
19
+ *
20
+ * SKIRT RING (rotational symmetry for proper corners):
21
+ * o---o---o---o---o
22
+ * | \ | / | \ | / |
23
+ * o---o---o---o---o
24
+ * | / | | \ |
25
+ * o---o o---o
26
+ * | \ | | / |
27
+ * o---o---o---o---o
28
+ * | / | \ | / | \ |
29
+ * o---o---o---o---o
30
+ *
31
+ * INNER GRID (consistent diagonal, no rotational symmetry):
32
+ * o---o---o
33
+ * | \ | \ |
34
+ * o---o---o
35
+ * | \ | \ |
36
+ * o---o---o
37
+ *
38
+ * Where o = vertex
39
+ * Each square cell is split into 2 triangles.
40
+ * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
41
+ * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
42
+ *
43
+ * Vertex layout (for innerSegments = 2):
44
+ *
45
+ * 0----1----2----3----4
46
+ * | | | | |
47
+ * 5----6----7----8----9
48
+ * | | | | |
49
+ * 10---11---12---13---14
50
+ * | | | | |
51
+ * 15---16---17---18---19
52
+ * | | | | |
53
+ * 20---21---22---23---24
54
+ *
55
+ * For each cell:
56
+ * a = top-left,
57
+ * b = top-right,
58
+ * c = bottom-left,
59
+ * d = bottom-right (all as flat array indices)
60
+ *
61
+ * Diagonal a-d:
62
+ * triangle 1: a, d, b
63
+ * triangle 2: a, c, d
64
+ * Diagonal b-c:
65
+ * triangle 1: a, c, b
66
+ * triangle 2: b, c, d
67
+ */
68
+ private generateIndices;
69
+ /**
70
+ * Generate vertex positions for the terrain with skirts.
71
+ * Positions are normalized to [-0.5, 0.5] range.
72
+ */
73
+ private generatePositions;
74
+ /**
75
+ * Generate UV coordinates for the inner grid only (skirt duplicates clamped to border).
76
+ * UVs are normalized to [0, 1] range with flipped V.
77
+ */
78
+ private generateUvsOnlyInner;
79
+ /**
80
+ * Generate UVs that extend 1 extra unit outward to the skirt ring.
81
+ * Map the entire geometry (including skirts) into [0,1] so side faces
82
+ * receive proper UVs without relying on texture wrapping. V is flipped.
83
+ */
84
+ private generateUvsExtended;
85
+ /**
86
+ * Generate vertex normals.
87
+ */
88
+ private generateNormals;
89
+ }
90
+
91
+ /**
92
+ * Returns a node that is true for skirt vertices in the vertex stage.
93
+ *
94
+ * @remarks
95
+ * Only valid in the vertex shader. A vertex belongs to the skirt if it is on
96
+ * the outermost ring of the tile grid (first/last column or row). The grid
97
+ * resolution is derived from `segments`.
98
+ *
99
+ * @param segments - The number of inner segments in the terrain grid.
100
+ * @returns A node resolving to a boolean indicating a skirt vertex.
101
+ */
102
+ declare const isSkirtVertex: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
103
+ /**
104
+ * Returns a node that is true for skirt UVs.
105
+ *
106
+ * @remarks
107
+ * Uses interpolated UVs and the grid size
108
+ * from `segments` to mark fragments outside the inner range
109
+ * `(step, 1 - step)` on either axis as skirt, where `step = 1 / (segments + 2)`.
110
+ *
111
+ * @param segments - The number of inner segments in the terrain grid.
112
+ * @returns A node resolving to a boolean indicating a skirt fragment.
113
+ */
114
+ declare const isSkirtUV: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
115
+
116
+ export { TerrainGeometry, isSkirtUV, isSkirtVertex };
@@ -0,0 +1,116 @@
1
+ import { BufferGeometry } from 'three';
2
+ import * as three_src_nodes_TSL_js from 'three/src/nodes/TSL.js';
3
+ import { Node } from 'three/webgpu';
4
+
5
+ /**
6
+ * Custom geometry for terrain tiles with properly handled skirts.
7
+ * This geometry ensures that corner triangles are subdivided correctly.
8
+ */
9
+ declare class TerrainGeometry extends BufferGeometry {
10
+ constructor(innerSegments?: number, extendUV?: boolean);
11
+ /**
12
+ * Generate indices for terrain geometry with proper skirt corner handling.
13
+ * The key improvement is in how corner triangles are subdivided.
14
+ */
15
+ /**
16
+ * Generate indices for terrain geometry with proper skirt corner handling.
17
+ *
18
+ * The mesh layout is a regular grid (with duplicated outermost ring for skirt):
19
+ *
20
+ * SKIRT RING (rotational symmetry for proper corners):
21
+ * o---o---o---o---o
22
+ * | \ | / | \ | / |
23
+ * o---o---o---o---o
24
+ * | / | | \ |
25
+ * o---o o---o
26
+ * | \ | | / |
27
+ * o---o---o---o---o
28
+ * | / | \ | / | \ |
29
+ * o---o---o---o---o
30
+ *
31
+ * INNER GRID (consistent diagonal, no rotational symmetry):
32
+ * o---o---o
33
+ * | \ | \ |
34
+ * o---o---o
35
+ * | \ | \ |
36
+ * o---o---o
37
+ *
38
+ * Where o = vertex
39
+ * Each square cell is split into 2 triangles.
40
+ * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
41
+ * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
42
+ *
43
+ * Vertex layout (for innerSegments = 2):
44
+ *
45
+ * 0----1----2----3----4
46
+ * | | | | |
47
+ * 5----6----7----8----9
48
+ * | | | | |
49
+ * 10---11---12---13---14
50
+ * | | | | |
51
+ * 15---16---17---18---19
52
+ * | | | | |
53
+ * 20---21---22---23---24
54
+ *
55
+ * For each cell:
56
+ * a = top-left,
57
+ * b = top-right,
58
+ * c = bottom-left,
59
+ * d = bottom-right (all as flat array indices)
60
+ *
61
+ * Diagonal a-d:
62
+ * triangle 1: a, d, b
63
+ * triangle 2: a, c, d
64
+ * Diagonal b-c:
65
+ * triangle 1: a, c, b
66
+ * triangle 2: b, c, d
67
+ */
68
+ private generateIndices;
69
+ /**
70
+ * Generate vertex positions for the terrain with skirts.
71
+ * Positions are normalized to [-0.5, 0.5] range.
72
+ */
73
+ private generatePositions;
74
+ /**
75
+ * Generate UV coordinates for the inner grid only (skirt duplicates clamped to border).
76
+ * UVs are normalized to [0, 1] range with flipped V.
77
+ */
78
+ private generateUvsOnlyInner;
79
+ /**
80
+ * Generate UVs that extend 1 extra unit outward to the skirt ring.
81
+ * Map the entire geometry (including skirts) into [0,1] so side faces
82
+ * receive proper UVs without relying on texture wrapping. V is flipped.
83
+ */
84
+ private generateUvsExtended;
85
+ /**
86
+ * Generate vertex normals.
87
+ */
88
+ private generateNormals;
89
+ }
90
+
91
+ /**
92
+ * Returns a node that is true for skirt vertices in the vertex stage.
93
+ *
94
+ * @remarks
95
+ * Only valid in the vertex shader. A vertex belongs to the skirt if it is on
96
+ * the outermost ring of the tile grid (first/last column or row). The grid
97
+ * resolution is derived from `segments`.
98
+ *
99
+ * @param segments - The number of inner segments in the terrain grid.
100
+ * @returns A node resolving to a boolean indicating a skirt vertex.
101
+ */
102
+ declare const isSkirtVertex: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
103
+ /**
104
+ * Returns a node that is true for skirt UVs.
105
+ *
106
+ * @remarks
107
+ * Uses interpolated UVs and the grid size
108
+ * from `segments` to mark fragments outside the inner range
109
+ * `(step, 1 - step)` on either axis as skirt, where `step = 1 / (segments + 2)`.
110
+ *
111
+ * @param segments - The number of inner segments in the terrain grid.
112
+ * @returns A node resolving to a boolean indicating a skirt fragment.
113
+ */
114
+ declare const isSkirtUV: three_src_nodes_TSL_js.ShaderNodeFn<[segments: number | Node]>;
115
+
116
+ export { TerrainGeometry, isSkirtUV, isSkirtVertex };
package/dist/index.mjs ADDED
@@ -0,0 +1,236 @@
1
+ import { BufferGeometry, BufferAttribute } from 'three';
2
+ import { Fn, int, vertexIndex, uv, float } from 'three/tsl';
3
+
4
+ class TerrainGeometry extends BufferGeometry {
5
+ constructor(innerSegments = 14, extendUV = false) {
6
+ super();
7
+ if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
8
+ throw new Error(
9
+ `Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
10
+ );
11
+ }
12
+ try {
13
+ this.setIndex(this.generateIndices(innerSegments));
14
+ this.setAttribute(
15
+ "position",
16
+ new BufferAttribute(
17
+ new Float32Array(this.generatePositions(innerSegments)),
18
+ 3
19
+ )
20
+ );
21
+ this.setAttribute(
22
+ "normal",
23
+ new BufferAttribute(
24
+ new Float32Array(this.generateNormals(innerSegments)),
25
+ 3
26
+ )
27
+ );
28
+ this.setAttribute(
29
+ "uv",
30
+ new BufferAttribute(
31
+ new Float32Array(
32
+ extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
33
+ ),
34
+ 2
35
+ )
36
+ );
37
+ } catch (error) {
38
+ console.error("Error creating TerrainGeometry:", error);
39
+ throw error;
40
+ }
41
+ }
42
+ /**
43
+ * Generate indices for terrain geometry with proper skirt corner handling.
44
+ * The key improvement is in how corner triangles are subdivided.
45
+ */
46
+ /**
47
+ * Generate indices for terrain geometry with proper skirt corner handling.
48
+ *
49
+ * The mesh layout is a regular grid (with duplicated outermost ring for skirt):
50
+ *
51
+ * SKIRT RING (rotational symmetry for proper corners):
52
+ * o---o---o---o---o
53
+ * | \ | / | \ | / |
54
+ * o---o---o---o---o
55
+ * | / | | \ |
56
+ * o---o o---o
57
+ * | \ | | / |
58
+ * o---o---o---o---o
59
+ * | / | \ | / | \ |
60
+ * o---o---o---o---o
61
+ *
62
+ * INNER GRID (consistent diagonal, no rotational symmetry):
63
+ * o---o---o
64
+ * | \ | \ |
65
+ * o---o---o
66
+ * | \ | \ |
67
+ * o---o---o
68
+ *
69
+ * Where o = vertex
70
+ * Each square cell is split into 2 triangles.
71
+ * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
72
+ * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
73
+ *
74
+ * Vertex layout (for innerSegments = 2):
75
+ *
76
+ * 0----1----2----3----4
77
+ * | | | | |
78
+ * 5----6----7----8----9
79
+ * | | | | |
80
+ * 10---11---12---13---14
81
+ * | | | | |
82
+ * 15---16---17---18---19
83
+ * | | | | |
84
+ * 20---21---22---23---24
85
+ *
86
+ * For each cell:
87
+ * a = top-left,
88
+ * b = top-right,
89
+ * c = bottom-left,
90
+ * d = bottom-right (all as flat array indices)
91
+ *
92
+ * Diagonal a-d:
93
+ * triangle 1: a, d, b
94
+ * triangle 2: a, c, d
95
+ * Diagonal b-c:
96
+ * triangle 1: a, c, b
97
+ * triangle 2: b, c, d
98
+ */
99
+ generateIndices(innerSegments) {
100
+ const innerEdgeVertexCount = innerSegments + 1;
101
+ const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
102
+ const indices = [];
103
+ const cellsPerEdge = edgeVertexCountWithSkirt - 1;
104
+ const mid = Math.floor(cellsPerEdge / 2);
105
+ for (let y = 0; y < cellsPerEdge; y++) {
106
+ for (let x = 0; x < cellsPerEdge; x++) {
107
+ const a = y * edgeVertexCountWithSkirt + x;
108
+ const b = a + 1;
109
+ const c = a + edgeVertexCountWithSkirt;
110
+ const d = c + 1;
111
+ const isSkirtCell = x === 0 || x === cellsPerEdge - 1 || y === 0 || y === cellsPerEdge - 1;
112
+ let useDefaultDiagonal;
113
+ if (isSkirtCell) {
114
+ const leftHalf = x < mid;
115
+ const topHalf = y < mid;
116
+ useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
117
+ } else {
118
+ useDefaultDiagonal = true;
119
+ }
120
+ if (useDefaultDiagonal) {
121
+ indices.push(a, d, b);
122
+ indices.push(a, c, d);
123
+ } else {
124
+ indices.push(a, c, b);
125
+ indices.push(b, c, d);
126
+ }
127
+ }
128
+ }
129
+ return indices;
130
+ }
131
+ /**
132
+ * Generate vertex positions for the terrain with skirts.
133
+ * Positions are normalized to [-0.5, 0.5] range.
134
+ */
135
+ generatePositions(innerSegments) {
136
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
137
+ const positions = [];
138
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
139
+ const v = Math.min(Math.max((iy - 1) / innerSegments, 0), 1);
140
+ const z = v - 0.5;
141
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
142
+ const u = Math.min(Math.max((ix - 1) / innerSegments, 0), 1);
143
+ const x = u - 0.5;
144
+ positions.push(x, 0, z);
145
+ }
146
+ }
147
+ return positions;
148
+ }
149
+ /**
150
+ * Generate UV coordinates for the inner grid only (skirt duplicates clamped to border).
151
+ * UVs are normalized to [0, 1] range with flipped V.
152
+ */
153
+ generateUvsOnlyInner(innerSegments) {
154
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
155
+ const uvs = [];
156
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
157
+ const v = Math.min(Math.max((iy - 1) / innerSegments, 0), 1);
158
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
159
+ const u = Math.min(Math.max((ix - 1) / innerSegments, 0), 1);
160
+ uvs.push(u, 1 - v);
161
+ }
162
+ }
163
+ return uvs;
164
+ }
165
+ /**
166
+ * Generate UVs that extend 1 extra unit outward to the skirt ring.
167
+ * Map the entire geometry (including skirts) into [0,1] so side faces
168
+ * receive proper UVs without relying on texture wrapping. V is flipped.
169
+ */
170
+ generateUvsExtended(innerSegments) {
171
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
172
+ const uvs = [];
173
+ const denom = edgeVertexCountWithSkirt - 1;
174
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
175
+ const v = iy / denom;
176
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
177
+ const u = ix / denom;
178
+ uvs.push(u, 1 - v);
179
+ }
180
+ }
181
+ return uvs;
182
+ }
183
+ /**
184
+ * Generate vertex normals.
185
+ */
186
+ generateNormals(innerSegments) {
187
+ const edgeVertexCountWithSkirt = innerSegments + 1 + 2;
188
+ const last = edgeVertexCountWithSkirt - 1;
189
+ const normals = [];
190
+ for (let iy = 0; iy < edgeVertexCountWithSkirt; iy++) {
191
+ for (let ix = 0; ix < edgeVertexCountWithSkirt; ix++) {
192
+ const onEdgeX = ix === 0 || ix === last;
193
+ const onEdgeY = iy === 0 || iy === last;
194
+ if (onEdgeX || onEdgeY) {
195
+ let nx = 0;
196
+ let nz = 0;
197
+ if (ix === 0) nx -= 1;
198
+ if (ix === last) nx += 1;
199
+ if (iy === 0) nz -= 1;
200
+ if (iy === last) nz += 1;
201
+ const len = Math.hypot(nx, nz);
202
+ if (len > 0) {
203
+ normals.push(nx / len, 0, nz / len);
204
+ } else {
205
+ normals.push(0, 1, 0);
206
+ }
207
+ } else {
208
+ normals.push(0, 1, 0);
209
+ }
210
+ }
211
+ }
212
+ return normals;
213
+ }
214
+ }
215
+
216
+ const isSkirtVertex = Fn(([segments]) => {
217
+ const segmentsNode = typeof segments === "number" ? int(segments) : segments;
218
+ const vIndex = int(vertexIndex);
219
+ const segmentEdges = int(segmentsNode.add(3));
220
+ const vx = vIndex.mod(segmentEdges);
221
+ const vy = vIndex.div(segmentEdges);
222
+ const last = segmentEdges.sub(int(1));
223
+ return vx.equal(int(0)).or(vx.equal(last)).or(vy.equal(int(0))).or(vy.equal(last));
224
+ });
225
+ const isSkirtUV = Fn(([segments]) => {
226
+ const segmentsNode = typeof segments === "number" ? int(segments) : segments;
227
+ const ux = uv().x;
228
+ const uy = uv().y;
229
+ const segmentCount = segmentsNode.add(2);
230
+ const segmentStep = float(1).div(segmentCount);
231
+ const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
232
+ const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
233
+ return innerX.and(innerY).not();
234
+ });
235
+
236
+ export { TerrainGeometry, isSkirtUV, isSkirtVertex };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@hello-terrain/three",
3
+ "description": "High performance terrain system for three.js",
4
+ "version": "0.0.0-alpha.1",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "peerDependencies": {
20
+ "three": ">=0.182.0"
21
+ },
22
+ "devDependencies": {
23
+ "unbuild": "^3.5.0",
24
+ "vitest": "^4.0.16",
25
+ "@types/three": ">=0.182.0",
26
+ "@config/oxfmt": "0.1.0",
27
+ "@config/oxlint": "0.1.0",
28
+ "@config/typescript": "0.1.0"
29
+ },
30
+ "scripts": {
31
+ "build": "unbuild",
32
+ "release": "pnpm run build && pnpm publish --access=public",
33
+ "test": "vitest",
34
+ "lint": "oxlint -c node_modules/@config/oxlint/base.json",
35
+ "format": "oxfmt -c node_modules/@config/oxfmt/base.json --write ."
36
+ }
37
+ }