@driftengine/core 4.5.0 → 4.6.0
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/CHANGELOG.json +51 -0
- package/README.md +1 -1
- package/dist/geometry/boxCsg.d.ts +28 -0
- package/dist/geometry/boxCsg.js +139 -0
- package/dist/geometry/csg.d.ts +36 -0
- package/dist/geometry/csg.js +329 -0
- package/dist/geometry/meshBuilder.js +13 -0
- package/dist/geometry/meshConcat.js +1 -0
- package/dist/geometry/pixelFont.js +2 -0
- package/dist/geometry/solid.d.ts +61 -0
- package/dist/geometry/solid.js +152 -0
- package/dist/geometry/solidFlat.d.ts +35 -0
- package/dist/geometry/solidFlat.js +428 -0
- package/dist/geometry/solidHarness.d.ts +19 -0
- package/dist/geometry/solidHarness.js +66 -0
- package/dist/geometry/solidNormals.d.ts +17 -0
- package/dist/geometry/solidNormals.js +86 -0
- package/dist/geometry/solidRound.d.ts +41 -0
- package/dist/geometry/solidRound.js +323 -0
- package/dist/geometry/solidWriter.d.ts +16 -0
- package/dist/geometry/solidWriter.js +42 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +13 -1
- package/dist/render/backend/api.d.ts +17 -3
- package/dist/render/backend/drawFog.d.ts +26 -0
- package/dist/render/backend/drawFog.js +30 -0
- package/dist/render/backend/materialChanges.d.ts +2 -1
- package/dist/render/backend/materialChanges.js +6 -1
- package/dist/render/backend/webgl2/instanced.d.ts +24 -5
- package/dist/render/backend/webgl2/instanced.js +72 -15
- package/dist/render/backend/webgl2/renderer.d.ts +122 -8
- package/dist/render/backend/webgl2/renderer.js +238 -121
- package/dist/render/backend/webgpu/buffers.d.ts +10 -0
- package/dist/render/backend/webgpu/buffers.js +34 -3
- package/dist/render/backend/webgpu/depthPass.js +5 -1
- package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
- package/dist/render/backend/webgpu/drawCommand.js +1 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
- package/dist/render/backend/webgpu/flatPass.js +38 -9
- package/dist/render/backend/webgpu/glassTintPass.js +2 -1
- package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
- package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
- package/dist/render/backend/webgpu/instanced.d.ts +11 -1
- package/dist/render/backend/webgpu/instanced.js +21 -3
- package/dist/render/backend/webgpu/latePass.d.ts +6 -2
- package/dist/render/backend/webgpu/latePass.js +11 -4
- package/dist/render/backend/webgpu/postPass.d.ts +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +100 -10
- package/dist/render/backend/webgpu/renderer.js +462 -201
- package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
- package/dist/render/driftLight/bake.d.ts +7 -0
- package/dist/render/driftLight/bake.js +61 -56
- package/dist/render/driftLight/denseField.d.ts +46 -0
- package/dist/render/driftLight/denseField.js +109 -0
- package/dist/render/driftLight/lightField.d.ts +10 -5
- package/dist/render/driftLight/lightField.js +25 -19
- package/dist/render/driftLight/presence.d.ts +50 -0
- package/dist/render/driftLight/presence.js +43 -0
- package/dist/render/driftLight/uniforms.d.ts +3 -3
- package/dist/render/driftLight/uniforms.js +5 -6
- package/dist/render/driftLight/worldLightField.d.ts +41 -0
- package/dist/render/driftLight/worldLightField.js +66 -0
- package/dist/render/fixtureAtlas.d.ts +20 -0
- package/dist/render/fixtureAtlas.js +49 -0
- package/dist/render/instanceCull.d.ts +63 -0
- package/dist/render/instanceCull.js +160 -0
- package/dist/render/instances.d.ts +15 -0
- package/dist/render/lightBudget.d.ts +22 -33
- package/dist/render/lightBudget.js +24 -35
- package/dist/render/lightGrid.d.ts +42 -0
- package/dist/render/lightGrid.js +87 -0
- package/dist/render/mesh.d.ts +18 -24
- package/dist/render/mesh.js +51 -68
- package/dist/render/occlusion.d.ts +10 -0
- package/dist/render/occlusion.js +36 -7
- package/dist/render/pointLightSelection.d.ts +7 -0
- package/dist/render/pointLightSelection.js +29 -4
- package/dist/render/rainField.d.ts +13 -0
- package/dist/render/rainField.js +27 -5
- package/dist/render/recon/lateDepth.d.ts +11 -0
- package/dist/render/recon/lateDepth.js +70 -0
- package/dist/render/recon/resolve.d.ts +34 -4
- package/dist/render/recon/resolve.js +161 -20
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +2 -0
- package/dist/render/shaders/depth.js +12 -9
- package/dist/render/shaders/flat/driftLight.d.ts +1 -1
- package/dist/render/shaders/flat/driftLight.js +32 -7
- package/dist/render/shaders/flat/index.js +5 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +75 -23
- package/dist/render/shaders/flat/preamble.js +75 -20
- package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
- package/dist/render/shaders/flat/surfaceEffects.js +152 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
- package/dist/render/shaders/generated/depth.wgsl.js +8 -8
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
- package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
- package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
- package/dist/render/shaders/generated/rush.wgsl.js +21 -16
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
- package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
- package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/text.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
- package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
- package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
- package/dist/render/shaders/rush.js +11 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/sdfText.js +3 -2
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +18 -9
- package/dist/render/surfaceEffects.d.ts +83 -0
- package/dist/render/surfaceEffects.js +75 -0
- package/dist/render/surfaceScene.d.ts +8 -0
- package/dist/render/surfaceScene.js +8 -0
- package/dist/render/surfaceTexture.d.ts +18 -2
- package/dist/render/surfaceTexture.js +68 -15
- package/dist/render/temporalAa.d.ts +25 -6
- package/dist/render/temporalAa.js +45 -15
- package/dist/render/textureSource.d.ts +30 -0
- package/dist/render/textureSource.js +53 -0
- package/dist/render/vertexDefaults.js +2 -0
- package/dist/world/hlod.d.ts +104 -0
- package/dist/world/hlod.js +214 -0
- package/package.json +6 -6
- package/src/geometry/boxCsg.ts +186 -0
- package/src/geometry/csg.ts +384 -0
- package/src/geometry/meshBuilder.ts +15 -0
- package/src/geometry/meshConcat.ts +1 -0
- package/src/geometry/pixelFont.ts +2 -0
- package/src/geometry/solid.ts +189 -0
- package/src/geometry/solidFlat.ts +522 -0
- package/src/geometry/solidHarness.ts +83 -0
- package/src/geometry/solidNormals.ts +103 -0
- package/src/geometry/solidRound.ts +443 -0
- package/src/geometry/solidWriter.ts +58 -0
- package/src/index.ts +51 -1
- package/src/render/backend/api.ts +21 -2
- package/src/render/backend/drawFog.ts +31 -0
- package/src/render/backend/materialChanges.ts +9 -2
- package/src/render/backend/webgl2/instanced.ts +80 -14
- package/src/render/backend/webgl2/renderer.ts +300 -143
- package/src/render/backend/webgpu/buffers.ts +42 -3
- package/src/render/backend/webgpu/depthPass.ts +5 -1
- package/src/render/backend/webgpu/drawCommand.ts +6 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -11
- package/src/render/backend/webgpu/glassTintPass.ts +2 -1
- package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
- package/src/render/backend/webgpu/instanced.ts +21 -3
- package/src/render/backend/webgpu/latePass.ts +12 -3
- package/src/render/backend/webgpu/renderer.ts +540 -226
- package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
- package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
- package/src/render/driftLight/bake.ts +85 -62
- package/src/render/driftLight/denseField.ts +149 -0
- package/src/render/driftLight/lightField.ts +33 -18
- package/src/render/driftLight/presence.ts +69 -0
- package/src/render/driftLight/uniforms.ts +8 -9
- package/src/render/driftLight/worldLightField.ts +83 -0
- package/src/render/fixtureAtlas.ts +63 -0
- package/src/render/instanceCull.ts +205 -0
- package/src/render/instances.ts +16 -0
- package/src/render/lightBudget.ts +25 -36
- package/src/render/lightGrid.ts +130 -0
- package/src/render/mesh.ts +59 -72
- package/src/render/occlusion.ts +51 -7
- package/src/render/pointLightSelection.ts +72 -4
- package/src/render/rainField.ts +39 -5
- package/src/render/recon/lateDepth.ts +83 -0
- package/src/render/recon/resolve.ts +206 -20
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneTarget.ts +4 -0
- package/src/render/shaders/depth.ts +12 -9
- package/src/render/shaders/flat/driftLight.ts +32 -7
- package/src/render/shaders/flat/index.ts +5 -3
- package/src/render/shaders/flat/main.ts +75 -23
- package/src/render/shaders/flat/preamble.ts +75 -20
- package/src/render/shaders/flat/surfaceEffects.ts +152 -0
- package/src/render/shaders/generated/depth.wgsl.ts +8 -8
- package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
- package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
- package/src/render/shaders/generated/rush.wgsl.ts +21 -16
- package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
- package/src/render/shaders/generated/text.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
- package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
- package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
- package/src/render/shaders/rush.ts +11 -1
- package/src/render/shaders/sdfText.ts +3 -2
- package/src/render/shaders/text.ts +18 -9
- package/src/render/surfaceEffects.ts +132 -0
- package/src/render/surfaceScene.ts +16 -0
- package/src/render/surfaceTexture.ts +110 -19
- package/src/render/temporalAa.ts +78 -14
- package/src/render/textureSource.ts +72 -0
- package/src/render/vertexDefaults.ts +2 -0
- package/src/world/hlod.ts +290 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The straight-sided solids: box, rounded box, quad, triangular prism, a polygon extruded, and a
|
|
3
|
+
* profile swept along a path.
|
|
4
|
+
*
|
|
5
|
+
* Conventions, shared with `solidRound.ts`: centred on the origin unless stated, full extents rather
|
|
6
|
+
* than half, parametric texture coordinates (0..1 across each face), counter-clockwise outward
|
|
7
|
+
* faces. A polygon handed in may be wound either way — it is oriented here — and may be concave, so
|
|
8
|
+
* caps are ear-clipped rather than fanned.
|
|
9
|
+
*/
|
|
10
|
+
import { mergeSolids, transformSolid } from './solid.ts';
|
|
11
|
+
import type { Solid } from './solid.ts';
|
|
12
|
+
import { SolidWriter } from './solidWriter.ts';
|
|
13
|
+
import type { Vec3 } from '../math/color.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The six faces of an axis-aligned box as (normal, u axis, v axis), with u × v = normal so the
|
|
17
|
+
* corners (−,−) (+,−) (+,+) (−,+) run counter-clockwise seen from outside. The u and v axes also
|
|
18
|
+
* orient each face's texture: v is up on the four sides.
|
|
19
|
+
*/
|
|
20
|
+
const FACES: readonly (readonly [Vec3, Vec3, Vec3])[] = [
|
|
21
|
+
[
|
|
22
|
+
[1, 0, 0],
|
|
23
|
+
[0, 0, -1],
|
|
24
|
+
[0, 1, 0],
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
[-1, 0, 0],
|
|
28
|
+
[0, 0, 1],
|
|
29
|
+
[0, 1, 0],
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
[0, 1, 0],
|
|
33
|
+
[1, 0, 0],
|
|
34
|
+
[0, 0, -1],
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
[0, -1, 0],
|
|
38
|
+
[1, 0, 0],
|
|
39
|
+
[0, 0, 1],
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
[0, 0, 1],
|
|
43
|
+
[1, 0, 0],
|
|
44
|
+
[0, 1, 0],
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
[0, 0, -1],
|
|
48
|
+
[-1, 0, 0],
|
|
49
|
+
[0, 1, 0],
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const dot = (a: Vec3, b: Vec3): number => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
54
|
+
const abs3 = (a: Vec3): Vec3 => [Math.abs(a[0]), Math.abs(a[1]), Math.abs(a[2])];
|
|
55
|
+
|
|
56
|
+
/** A box of full extents `x, y, z`, faceted: four vertices a face, texture 0..1 across each. */
|
|
57
|
+
export function solidBox(x: number, y: number, z: number): Solid {
|
|
58
|
+
const half: Vec3 = [x / 2, y / 2, z / 2];
|
|
59
|
+
const out = new SolidWriter();
|
|
60
|
+
for (const [n, u, v] of FACES) {
|
|
61
|
+
const hn = dot(abs3(n), half);
|
|
62
|
+
const hu = dot(abs3(u), half);
|
|
63
|
+
const hv = dot(abs3(v), half);
|
|
64
|
+
const base = out.vertexCount;
|
|
65
|
+
for (const [su, sv] of CORNERS) {
|
|
66
|
+
out.vertex(
|
|
67
|
+
n[0] * hn + u[0] * hu * su + v[0] * hv * sv,
|
|
68
|
+
n[1] * hn + u[1] * hu * su + v[1] * hv * sv,
|
|
69
|
+
n[2] * hn + u[2] * hu * su + v[2] * hv * sv,
|
|
70
|
+
n[0],
|
|
71
|
+
n[1],
|
|
72
|
+
n[2],
|
|
73
|
+
(su + 1) / 2,
|
|
74
|
+
(sv + 1) / 2,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
out.quad(base, base + 1, base + 2, base + 3);
|
|
78
|
+
}
|
|
79
|
+
return out.finish();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CORNERS: readonly (readonly [number, number])[] = [
|
|
83
|
+
[-1, -1],
|
|
84
|
+
[1, -1],
|
|
85
|
+
[1, 1],
|
|
86
|
+
[-1, 1],
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** A single-sided plane of `x` by `y` in XY, facing +Z. The one open solid. */
|
|
90
|
+
export function solidQuad(x: number, y: number): Solid {
|
|
91
|
+
const out = new SolidWriter();
|
|
92
|
+
for (const [su, sv] of CORNERS) {
|
|
93
|
+
out.vertex((su * x) / 2, (sv * y) / 2, 0, 0, 0, 1, (su + 1) / 2, (sv + 1) / 2);
|
|
94
|
+
}
|
|
95
|
+
out.quad(0, 1, 2, 3);
|
|
96
|
+
return out.finish();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A box of full extents with every edge rounded to `radius`, `segments` steps per rounded band.
|
|
101
|
+
*
|
|
102
|
+
* Built as a Minkowski sum: each face is a grid whose points are pushed out from the inner box —
|
|
103
|
+
* the box shrunk by the radius — along the direction from their clamped inner point. Grid lines
|
|
104
|
+
* fall on the inner box's edges exactly, so the flat middle of each face is one quad and adjacent
|
|
105
|
+
* faces meet on identical seams.
|
|
106
|
+
*/
|
|
107
|
+
export function solidRoundedBox(
|
|
108
|
+
x: number,
|
|
109
|
+
y: number,
|
|
110
|
+
z: number,
|
|
111
|
+
radius: number,
|
|
112
|
+
segments: number,
|
|
113
|
+
): Solid {
|
|
114
|
+
const half: Vec3 = [x / 2, y / 2, z / 2];
|
|
115
|
+
const r = Math.max(0, Math.min(radius, half[0], half[1], half[2]));
|
|
116
|
+
if (r === 0) return solidBox(x, y, z);
|
|
117
|
+
const inner: Vec3 = [half[0] - r, half[1] - r, half[2] - r];
|
|
118
|
+
const steps = Math.max(1, Math.floor(segments));
|
|
119
|
+
const out = new SolidWriter();
|
|
120
|
+
for (const [n, u, v] of FACES) {
|
|
121
|
+
const hn = dot(abs3(n), half);
|
|
122
|
+
const us = bandCoordinates(dot(abs3(u), half), dot(abs3(u), inner), steps);
|
|
123
|
+
const vs = bandCoordinates(dot(abs3(v), half), dot(abs3(v), inner), steps);
|
|
124
|
+
const base = out.vertexCount;
|
|
125
|
+
for (const b of vs) {
|
|
126
|
+
for (const a of us) {
|
|
127
|
+
const p: Vec3 = [
|
|
128
|
+
n[0] * hn + u[0] * a + v[0] * b,
|
|
129
|
+
n[1] * hn + u[1] * a + v[1] * b,
|
|
130
|
+
n[2] * hn + u[2] * a + v[2] * b,
|
|
131
|
+
];
|
|
132
|
+
const q: Vec3 = [clamp(p[0], inner[0]), clamp(p[1], inner[1]), clamp(p[2], inner[2])];
|
|
133
|
+
let dx = p[0] - q[0];
|
|
134
|
+
let dy = p[1] - q[1];
|
|
135
|
+
let dz = p[2] - q[2];
|
|
136
|
+
const len = Math.hypot(dx, dy, dz);
|
|
137
|
+
if (len < 1e-12) {
|
|
138
|
+
[dx, dy, dz] = n;
|
|
139
|
+
} else {
|
|
140
|
+
dx /= len;
|
|
141
|
+
dy /= len;
|
|
142
|
+
dz /= len;
|
|
143
|
+
}
|
|
144
|
+
const uHalf = us[us.length - 1] ?? 1;
|
|
145
|
+
const vHalf = vs[vs.length - 1] ?? 1;
|
|
146
|
+
out.vertex(
|
|
147
|
+
q[0] + dx * r,
|
|
148
|
+
q[1] + dy * r,
|
|
149
|
+
q[2] + dz * r,
|
|
150
|
+
dx,
|
|
151
|
+
dy,
|
|
152
|
+
dz,
|
|
153
|
+
(a / uHalf + 1) / 2,
|
|
154
|
+
(b / vHalf + 1) / 2,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const row = us.length;
|
|
159
|
+
for (let j = 0; j < vs.length - 1; j++) {
|
|
160
|
+
for (let i = 0; i < row - 1; i++) {
|
|
161
|
+
const a = base + j * row + i;
|
|
162
|
+
out.quad(a, a + 1, a + 1 + row, a + row);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out.finish();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const clamp = (value: number, limit: number): number => Math.max(-limit, Math.min(limit, value));
|
|
170
|
+
|
|
171
|
+
/** Coordinates across one face axis: `steps` into each rounded band, one step across the flat. */
|
|
172
|
+
function bandCoordinates(outer: number, inner: number, steps: number): number[] {
|
|
173
|
+
const coords: number[] = [];
|
|
174
|
+
for (let s = 0; s <= steps; s++) coords.push(-outer + ((outer - inner) * s) / steps);
|
|
175
|
+
if (inner > 0)
|
|
176
|
+
for (let s = 0; s <= steps; s++) coords.push(inner + ((outer - inner) * s) / steps);
|
|
177
|
+
else for (let s = 1; s <= steps; s++) coords.push(inner + ((outer - inner) * s) / steps);
|
|
178
|
+
return coords;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A triangular prism: the section in XY — base `x` wide at −y/2, apex at +y/2 — extruded `z` along
|
|
183
|
+
* Z, centred. `right` puts the apex over the base's left end, a right angle there.
|
|
184
|
+
*/
|
|
185
|
+
export function solidPrism(x: number, y: number, z: number, right = false): Solid {
|
|
186
|
+
const apex = right ? -x / 2 : 0;
|
|
187
|
+
return extrudeCanonical([-x / 2, -y / 2, x / 2, -y / 2, apex, y / 2], -z / 2, z / 2);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A polygon in XZ (`x, z` pairs, either winding, convex or not) extruded along +Y from 0 to
|
|
192
|
+
* `height`, or centred on the origin when `centre` is set.
|
|
193
|
+
*/
|
|
194
|
+
export function solidExtrude(profile: ArrayLike<number>, height: number, centre = false): Solid {
|
|
195
|
+
const w0 = centre ? -height / 2 : 0;
|
|
196
|
+
/* Built with the polygon in (s, t) and depth along w, then mapped s→x, t→z, w→y: a swap of two
|
|
197
|
+
axes, whose negative determinant `transformSolid` answers by reversing the winding. */
|
|
198
|
+
const canonical = extrudeCanonical(Array.from(profile), w0, w0 + height);
|
|
199
|
+
return transformSolid(canonical, [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A profile (`x, y` pairs in the plane across the path) swept along a polyline path (`x, y, z`
|
|
204
|
+
* triples). At each point the frame is right = up × tangent, up' = tangent × right, so profile x
|
|
205
|
+
* runs right and profile y runs up; `up` defaults to +Y. Sides are faceted; `capEnds` closes both
|
|
206
|
+
* ends. Texture u runs around the profile and v along the path, both 0..1.
|
|
207
|
+
*
|
|
208
|
+
* Corners are not mitred: a sharp turn pinches the section. A path that turns sharply wants more
|
|
209
|
+
* points.
|
|
210
|
+
*/
|
|
211
|
+
export function solidSweep(
|
|
212
|
+
profile: ArrayLike<number>,
|
|
213
|
+
path: ArrayLike<number>,
|
|
214
|
+
capEnds: boolean,
|
|
215
|
+
up: Vec3 = [0, 1, 0],
|
|
216
|
+
): Solid {
|
|
217
|
+
const poly = counterClockwise(Array.from(profile));
|
|
218
|
+
const k = poly.length / 2;
|
|
219
|
+
const n = Math.floor(path.length / 3);
|
|
220
|
+
const rings: number[][] = [];
|
|
221
|
+
const along: number[] = [0];
|
|
222
|
+
for (let i = 0; i < n; i++) {
|
|
223
|
+
const prev = Math.max(0, i - 1);
|
|
224
|
+
const next = Math.min(n - 1, i + 1);
|
|
225
|
+
let tx = (path[next * 3] ?? 0) - (path[prev * 3] ?? 0);
|
|
226
|
+
let ty = (path[next * 3 + 1] ?? 0) - (path[prev * 3 + 1] ?? 0);
|
|
227
|
+
let tz = (path[next * 3 + 2] ?? 0) - (path[prev * 3 + 2] ?? 0);
|
|
228
|
+
const tl = Math.hypot(tx, ty, tz) || 1;
|
|
229
|
+
tx /= tl;
|
|
230
|
+
ty /= tl;
|
|
231
|
+
tz /= tl;
|
|
232
|
+
let rx = up[1] * tz - up[2] * ty;
|
|
233
|
+
let ry = up[2] * tx - up[0] * tz;
|
|
234
|
+
let rz = up[0] * ty - up[1] * tx;
|
|
235
|
+
const rl = Math.hypot(rx, ry, rz) || 1;
|
|
236
|
+
rx /= rl;
|
|
237
|
+
ry /= rl;
|
|
238
|
+
rz /= rl;
|
|
239
|
+
const ux = ty * rz - tz * ry;
|
|
240
|
+
const uy = tz * rx - tx * rz;
|
|
241
|
+
const uz = tx * ry - ty * rx;
|
|
242
|
+
const ring: number[] = [];
|
|
243
|
+
for (let p = 0; p < k; p++) {
|
|
244
|
+
const px = poly[p * 2] ?? 0;
|
|
245
|
+
const py = poly[p * 2 + 1] ?? 0;
|
|
246
|
+
ring.push(
|
|
247
|
+
(path[i * 3] ?? 0) + rx * px + ux * py,
|
|
248
|
+
(path[i * 3 + 1] ?? 0) + ry * px + uy * py,
|
|
249
|
+
(path[i * 3 + 2] ?? 0) + rz * px + uz * py,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
rings.push(ring);
|
|
253
|
+
if (i > 0) {
|
|
254
|
+
along.push(
|
|
255
|
+
(along[i - 1] ?? 0) +
|
|
256
|
+
Math.hypot(
|
|
257
|
+
(path[i * 3] ?? 0) - (path[(i - 1) * 3] ?? 0),
|
|
258
|
+
(path[i * 3 + 1] ?? 0) - (path[(i - 1) * 3 + 1] ?? 0),
|
|
259
|
+
(path[i * 3 + 2] ?? 0) - (path[(i - 1) * 3 + 2] ?? 0),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const total = along[n - 1] || 1;
|
|
265
|
+
const perimeter = perimeterFractions(poly);
|
|
266
|
+
const out = new SolidWriter();
|
|
267
|
+
for (let i = 0; i < n - 1; i++) {
|
|
268
|
+
const a = rings[i] ?? [];
|
|
269
|
+
const b = rings[i + 1] ?? [];
|
|
270
|
+
for (let p = 0; p < k; p++) {
|
|
271
|
+
const q = (p + 1) % k;
|
|
272
|
+
writeQuad(
|
|
273
|
+
out,
|
|
274
|
+
[a[p * 3] ?? 0, a[p * 3 + 1] ?? 0, a[p * 3 + 2] ?? 0],
|
|
275
|
+
[a[q * 3] ?? 0, a[q * 3 + 1] ?? 0, a[q * 3 + 2] ?? 0],
|
|
276
|
+
[b[q * 3] ?? 0, b[q * 3 + 1] ?? 0, b[q * 3 + 2] ?? 0],
|
|
277
|
+
[b[p * 3] ?? 0, b[p * 3 + 1] ?? 0, b[p * 3 + 2] ?? 0],
|
|
278
|
+
perimeter[p] ?? 0,
|
|
279
|
+
perimeter[p + 1] ?? 1,
|
|
280
|
+
(along[i] ?? 0) / total,
|
|
281
|
+
(along[i + 1] ?? 0) / total,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!capEnds || n < 2) return out.finish();
|
|
286
|
+
const sides = out.finish();
|
|
287
|
+
const caps = [
|
|
288
|
+
capFromRing(rings[0] ?? [], poly, true),
|
|
289
|
+
capFromRing(rings[n - 1] ?? [], poly, false),
|
|
290
|
+
];
|
|
291
|
+
return mergeSolids([sides, ...caps]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** One faceted quad with its own four vertices and the normal its corners imply. */
|
|
295
|
+
function writeQuad(
|
|
296
|
+
out: SolidWriter,
|
|
297
|
+
a: Vec3,
|
|
298
|
+
b: Vec3,
|
|
299
|
+
c: Vec3,
|
|
300
|
+
d: Vec3,
|
|
301
|
+
u0: number,
|
|
302
|
+
u1: number,
|
|
303
|
+
v0: number,
|
|
304
|
+
v1: number,
|
|
305
|
+
): void {
|
|
306
|
+
const ux = b[0] - a[0];
|
|
307
|
+
const uy = b[1] - a[1];
|
|
308
|
+
const uz = b[2] - a[2];
|
|
309
|
+
const vx = d[0] - a[0];
|
|
310
|
+
const vy = d[1] - a[1];
|
|
311
|
+
const vz = d[2] - a[2];
|
|
312
|
+
let nx = uy * vz - uz * vy;
|
|
313
|
+
let ny = uz * vx - ux * vz;
|
|
314
|
+
let nz = ux * vy - uy * vx;
|
|
315
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
316
|
+
nx /= len;
|
|
317
|
+
ny /= len;
|
|
318
|
+
nz /= len;
|
|
319
|
+
const base = out.vertexCount;
|
|
320
|
+
out.vertex(a[0], a[1], a[2], nx, ny, nz, u0, v0);
|
|
321
|
+
out.vertex(b[0], b[1], b[2], nx, ny, nz, u1, v0);
|
|
322
|
+
out.vertex(c[0], c[1], c[2], nx, ny, nz, u1, v1);
|
|
323
|
+
out.vertex(d[0], d[1], d[2], nx, ny, nz, u0, v1);
|
|
324
|
+
out.quad(base, base + 1, base + 2, base + 3);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A cap over one ring of a sweep: the profile's triangulation, reversed at the start. */
|
|
328
|
+
function capFromRing(ring: number[], poly: number[], start: boolean): Solid {
|
|
329
|
+
const k = poly.length / 2;
|
|
330
|
+
const tris = triangulate(poly);
|
|
331
|
+
const out = new SolidWriter();
|
|
332
|
+
const p0: Vec3 = [ring[0] ?? 0, ring[1] ?? 0, ring[2] ?? 0];
|
|
333
|
+
let nx = 0;
|
|
334
|
+
let ny = 0;
|
|
335
|
+
let nz = 0;
|
|
336
|
+
for (let t = 0; t < tris.length; t += 3) {
|
|
337
|
+
const [a, b, c] = [tris[t] ?? 0, tris[t + 1] ?? 0, tris[t + 2] ?? 0];
|
|
338
|
+
const ux = (ring[b * 3] ?? 0) - (ring[a * 3] ?? 0);
|
|
339
|
+
const uy = (ring[b * 3 + 1] ?? 0) - (ring[a * 3 + 1] ?? 0);
|
|
340
|
+
const uz = (ring[b * 3 + 2] ?? 0) - (ring[a * 3 + 2] ?? 0);
|
|
341
|
+
const vx = (ring[c * 3] ?? 0) - (ring[a * 3] ?? 0);
|
|
342
|
+
const vy = (ring[c * 3 + 1] ?? 0) - (ring[a * 3 + 1] ?? 0);
|
|
343
|
+
const vz = (ring[c * 3 + 2] ?? 0) - (ring[a * 3 + 2] ?? 0);
|
|
344
|
+
nx += uy * vz - uz * vy;
|
|
345
|
+
ny += uz * vx - ux * vz;
|
|
346
|
+
nz += ux * vy - uy * vx;
|
|
347
|
+
}
|
|
348
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
349
|
+
const s = start ? -1 : 1;
|
|
350
|
+
const [bx, by] = bounds(poly);
|
|
351
|
+
for (let p = 0; p < k; p++) {
|
|
352
|
+
out.vertex(
|
|
353
|
+
ring[p * 3] ?? p0[0],
|
|
354
|
+
ring[p * 3 + 1] ?? p0[1],
|
|
355
|
+
ring[p * 3 + 2] ?? p0[2],
|
|
356
|
+
(s * nx) / len,
|
|
357
|
+
(s * ny) / len,
|
|
358
|
+
(s * nz) / len,
|
|
359
|
+
((poly[p * 2] ?? 0) - bx[0]) / (bx[1] - bx[0] || 1),
|
|
360
|
+
((poly[p * 2 + 1] ?? 0) - by[0]) / (by[1] - by[0] || 1),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
for (let t = 0; t < tris.length; t += 3) {
|
|
364
|
+
if (start) out.triangle(tris[t] ?? 0, tris[t + 2] ?? 0, tris[t + 1] ?? 0);
|
|
365
|
+
else out.triangle(tris[t] ?? 0, tris[t + 1] ?? 0, tris[t + 2] ?? 0);
|
|
366
|
+
}
|
|
367
|
+
return out.finish();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A polygon in (s, t) extruded along w from `w0` to `w1`, in a frame where s × t = w: caps
|
|
372
|
+
* ear-clipped, sides faceted, texture u around the perimeter and v along w. The frame is XYZ
|
|
373
|
+
* itself, so a caller wanting another axis maps it with `transformSolid`.
|
|
374
|
+
*/
|
|
375
|
+
function extrudeCanonical(profile: number[], w0: number, w1: number): Solid {
|
|
376
|
+
const poly = counterClockwise(profile);
|
|
377
|
+
const k = poly.length / 2;
|
|
378
|
+
const tris = triangulate(poly);
|
|
379
|
+
const [bs, bt] = bounds(poly);
|
|
380
|
+
const perimeter = perimeterFractions(poly);
|
|
381
|
+
const out = new SolidWriter();
|
|
382
|
+
for (const [w, sign] of [
|
|
383
|
+
[w1, 1],
|
|
384
|
+
[w0, -1],
|
|
385
|
+
] as const) {
|
|
386
|
+
const base = out.vertexCount;
|
|
387
|
+
for (let p = 0; p < k; p++) {
|
|
388
|
+
const s = poly[p * 2] ?? 0;
|
|
389
|
+
const t = poly[p * 2 + 1] ?? 0;
|
|
390
|
+
out.vertex(
|
|
391
|
+
s,
|
|
392
|
+
t,
|
|
393
|
+
w,
|
|
394
|
+
0,
|
|
395
|
+
0,
|
|
396
|
+
sign,
|
|
397
|
+
(s - bs[0]) / (bs[1] - bs[0] || 1),
|
|
398
|
+
(t - bt[0]) / (bt[1] - bt[0] || 1),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
for (let i = 0; i < tris.length; i += 3) {
|
|
402
|
+
const a = base + (tris[i] ?? 0);
|
|
403
|
+
const b = base + (tris[i + 1] ?? 0);
|
|
404
|
+
const c = base + (tris[i + 2] ?? 0);
|
|
405
|
+
if (sign > 0) out.triangle(a, b, c);
|
|
406
|
+
else out.triangle(a, c, b);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (let p = 0; p < k; p++) {
|
|
410
|
+
const q = (p + 1) % k;
|
|
411
|
+
const a: Vec3 = [poly[p * 2] ?? 0, poly[p * 2 + 1] ?? 0, w0];
|
|
412
|
+
const b: Vec3 = [poly[q * 2] ?? 0, poly[q * 2 + 1] ?? 0, w0];
|
|
413
|
+
writeQuad(
|
|
414
|
+
out,
|
|
415
|
+
a,
|
|
416
|
+
b,
|
|
417
|
+
[b[0], b[1], w1],
|
|
418
|
+
[a[0], a[1], w1],
|
|
419
|
+
perimeter[p] ?? 0,
|
|
420
|
+
perimeter[p + 1] ?? 1,
|
|
421
|
+
0,
|
|
422
|
+
1,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return out.finish();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function signedArea(poly: number[]): number {
|
|
429
|
+
let sum = 0;
|
|
430
|
+
const k = poly.length / 2;
|
|
431
|
+
for (let p = 0; p < k; p++) {
|
|
432
|
+
const q = (p + 1) % k;
|
|
433
|
+
sum +=
|
|
434
|
+
(poly[p * 2] ?? 0) * (poly[q * 2 + 1] ?? 0) - (poly[q * 2] ?? 0) * (poly[p * 2 + 1] ?? 0);
|
|
435
|
+
}
|
|
436
|
+
return sum / 2;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The polygon wound counter-clockwise, reversed if it came the other way. */
|
|
440
|
+
function counterClockwise(poly: number[]): number[] {
|
|
441
|
+
if (signedArea(poly) >= 0) return poly;
|
|
442
|
+
const out: number[] = [];
|
|
443
|
+
for (let p = poly.length / 2 - 1; p >= 0; p--) out.push(poly[p * 2] ?? 0, poly[p * 2 + 1] ?? 0);
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function bounds(poly: number[]): [[number, number], [number, number]] {
|
|
448
|
+
let s0 = Infinity;
|
|
449
|
+
let s1 = -Infinity;
|
|
450
|
+
let t0 = Infinity;
|
|
451
|
+
let t1 = -Infinity;
|
|
452
|
+
for (let p = 0; p < poly.length; p += 2) {
|
|
453
|
+
s0 = Math.min(s0, poly[p] ?? 0);
|
|
454
|
+
s1 = Math.max(s1, poly[p] ?? 0);
|
|
455
|
+
t0 = Math.min(t0, poly[p + 1] ?? 0);
|
|
456
|
+
t1 = Math.max(t1, poly[p + 1] ?? 0);
|
|
457
|
+
}
|
|
458
|
+
return [
|
|
459
|
+
[s0, s1],
|
|
460
|
+
[t0, t1],
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** How far around the perimeter each corner sits, 0 at the first and 1 back at it. */
|
|
465
|
+
function perimeterFractions(poly: number[]): number[] {
|
|
466
|
+
const k = poly.length / 2;
|
|
467
|
+
const at: number[] = [0];
|
|
468
|
+
for (let p = 0; p < k; p++) {
|
|
469
|
+
const q = (p + 1) % k;
|
|
470
|
+
const edge = Math.hypot(
|
|
471
|
+
(poly[q * 2] ?? 0) - (poly[p * 2] ?? 0),
|
|
472
|
+
(poly[q * 2 + 1] ?? 0) - (poly[p * 2 + 1] ?? 0),
|
|
473
|
+
);
|
|
474
|
+
at.push((at[p] ?? 0) + edge);
|
|
475
|
+
}
|
|
476
|
+
const total = at[k] || 1;
|
|
477
|
+
return at.map((d) => d / total);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Ear clipping over a counter-clockwise simple polygon: corner indices, three a triangle.
|
|
482
|
+
* Quadratic, which is right for the dozens of corners a profile has.
|
|
483
|
+
*/
|
|
484
|
+
function triangulate(poly: number[]): number[] {
|
|
485
|
+
const k = poly.length / 2;
|
|
486
|
+
const remaining: number[] = [];
|
|
487
|
+
for (let p = 0; p < k; p++) remaining.push(p);
|
|
488
|
+
const tris: number[] = [];
|
|
489
|
+
const x = (i: number): number => poly[i * 2] ?? 0;
|
|
490
|
+
const y = (i: number): number => poly[i * 2 + 1] ?? 0;
|
|
491
|
+
const cross = (a: number, b: number, c: number): number =>
|
|
492
|
+
(x(b) - x(a)) * (y(c) - y(a)) - (y(b) - y(a)) * (x(c) - x(a));
|
|
493
|
+
let guard = k * k + 8;
|
|
494
|
+
while (remaining.length > 3 && guard-- > 0) {
|
|
495
|
+
let clipped = false;
|
|
496
|
+
for (let r = 0; r < remaining.length; r++) {
|
|
497
|
+
const a = remaining[(r + remaining.length - 1) % remaining.length] ?? 0;
|
|
498
|
+
const b = remaining[r] ?? 0;
|
|
499
|
+
const c = remaining[(r + 1) % remaining.length] ?? 0;
|
|
500
|
+
if (cross(a, b, c) <= 1e-12) continue;
|
|
501
|
+
let inside = false;
|
|
502
|
+
for (const o of remaining) {
|
|
503
|
+
if (o === a || o === b || o === c) continue;
|
|
504
|
+
if (cross(a, b, o) >= 0 && cross(b, c, o) >= 0 && cross(c, a, o) >= 0) {
|
|
505
|
+
inside = true;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (inside) continue;
|
|
510
|
+
tris.push(a, b, c);
|
|
511
|
+
remaining.splice(r, 1);
|
|
512
|
+
clipped = true;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
if (!clipped) {
|
|
516
|
+
/* Only collinear corners remain unclipped: drop the middle one, which spans no area. */
|
|
517
|
+
remaining.splice(1, 1);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (remaining.length === 3) tris.push(remaining[0] ?? 0, remaining[1] ?? 0, remaining[2] ?? 0);
|
|
521
|
+
return tris;
|
|
522
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a closed surface can be held to, for the solid and CSG tests.
|
|
3
|
+
*
|
|
4
|
+
* **Not a `.test.ts` file**, for the reason `rendererHarness.ts` gives: Vitest registers a test
|
|
5
|
+
* when the file declaring it is imported, so helpers living beside tests would re-run them in every
|
|
6
|
+
* file that imported them.
|
|
7
|
+
*
|
|
8
|
+
* `edgesPairOnce` welds by position and asks that every edge be shared by exactly two triangles
|
|
9
|
+
* running opposite ways, which is what closed and consistently wound means. `normalsFaceOutward`
|
|
10
|
+
* asks that every vertex normal lie on the outward side of its triangle. The matrices are
|
|
11
|
+
* column-major, as `transformSolid` takes them.
|
|
12
|
+
*/
|
|
13
|
+
import type { Solid } from './solid.ts';
|
|
14
|
+
|
|
15
|
+
export function edgesPairOnce(solid: Solid): boolean {
|
|
16
|
+
const p = solid.positions;
|
|
17
|
+
const key = (i: number): string =>
|
|
18
|
+
`${Math.round((p[i * 3] ?? 0) * 1e5)},${Math.round((p[i * 3 + 1] ?? 0) * 1e5)},${Math.round((p[i * 3 + 2] ?? 0) * 1e5)}`;
|
|
19
|
+
const directed = new Map<string, number>();
|
|
20
|
+
const idx = solid.indices;
|
|
21
|
+
for (let t = 0; t < idx.length; t += 3) {
|
|
22
|
+
for (let e = 0; e < 3; e++) {
|
|
23
|
+
const a = key(idx[t + e] ?? 0);
|
|
24
|
+
const b = key(idx[t + ((e + 1) % 3)] ?? 0);
|
|
25
|
+
if (a === b) continue;
|
|
26
|
+
const k = `${a}>${b}`;
|
|
27
|
+
directed.set(k, (directed.get(k) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const [k, n] of directed) {
|
|
31
|
+
const [a, b] = k.split('>');
|
|
32
|
+
if (n !== 1 || directed.get(`${b}>${a}`) !== 1) return false;
|
|
33
|
+
}
|
|
34
|
+
return directed.size > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalsFaceOutward(solid: Solid): boolean {
|
|
38
|
+
const p = solid.positions;
|
|
39
|
+
const n = solid.normals;
|
|
40
|
+
const idx = solid.indices;
|
|
41
|
+
const at = (i: number, c: number): number => p[i * 3 + c] ?? 0;
|
|
42
|
+
for (let t = 0; t < idx.length; t += 3) {
|
|
43
|
+
const a = idx[t] ?? 0;
|
|
44
|
+
const b = idx[t + 1] ?? 0;
|
|
45
|
+
const c = idx[t + 2] ?? 0;
|
|
46
|
+
const ux = at(b, 0) - at(a, 0);
|
|
47
|
+
const uy = at(b, 1) - at(a, 1);
|
|
48
|
+
const uz = at(b, 2) - at(a, 2);
|
|
49
|
+
const vx = at(c, 0) - at(a, 0);
|
|
50
|
+
const vy = at(c, 1) - at(a, 1);
|
|
51
|
+
const vz = at(c, 2) - at(a, 2);
|
|
52
|
+
const gx = uy * vz - uz * vy;
|
|
53
|
+
const gy = uz * vx - ux * vz;
|
|
54
|
+
const gz = ux * vy - uy * vx;
|
|
55
|
+
if (Math.hypot(gx, gy, gz) < 1e-12) continue;
|
|
56
|
+
for (const v of [a, b, c]) {
|
|
57
|
+
if (gx * (n[v * 3] ?? 0) + gy * (n[v * 3 + 1] ?? 0) + gz * (n[v * 3 + 2] ?? 0) <= 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function translation(x: number, y: number, z: number): number[] {
|
|
66
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function scaling(x: number, y: number, z: number): number[] {
|
|
70
|
+
return [x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function rotationX(a: number): number[] {
|
|
74
|
+
const c = Math.cos(a);
|
|
75
|
+
const s = Math.sin(a);
|
|
76
|
+
return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function rotationY(a: number): number[] {
|
|
80
|
+
const c = Math.cos(a);
|
|
81
|
+
const s = Math.sin(a);
|
|
82
|
+
return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
|
|
83
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crease-angle normal smoothing: every vertex takes the average of the faces meeting at its
|
|
3
|
+
* position whose normals lie within the crease angle of its own, so a cylinder's side goes round
|
|
4
|
+
* while its rim stays sharp.
|
|
5
|
+
*
|
|
6
|
+
* **Weighted by the corner angle each face makes at the vertex, not by area.** A quad split into
|
|
7
|
+
* two triangles meets a vertex with one triangle at one corner and two at the next, so area weights
|
|
8
|
+
* favour whichever side the split fell on and lean every normal of a smooth side a degree or two off
|
|
9
|
+
* true. The corner angle a quad makes is 90° however it was split, which is what makes the average
|
|
10
|
+
* belong to the shape rather than to its triangulation.
|
|
11
|
+
*
|
|
12
|
+
* Positions are welded at 1e-5 for the lookup only; the solid keeps its vertices, indices and
|
|
13
|
+
* texture coordinates, and each vertex's incoming normal chooses which faces it joins — so a
|
|
14
|
+
* faceted solid's own face normals decide the creases.
|
|
15
|
+
*/
|
|
16
|
+
import type { Solid } from './solid.ts';
|
|
17
|
+
|
|
18
|
+
export function smoothSolidNormals(solid: Solid, creaseDeg: number): Solid {
|
|
19
|
+
const p = solid.positions;
|
|
20
|
+
const idx = solid.indices;
|
|
21
|
+
const count = p.length / 3;
|
|
22
|
+
const cosCrease = Math.cos((creaseDeg * Math.PI) / 180);
|
|
23
|
+
|
|
24
|
+
const weld = new Map<string, number>();
|
|
25
|
+
const group = new Int32Array(count);
|
|
26
|
+
for (let v = 0; v < count; v++) {
|
|
27
|
+
const key = `${Math.round((p[v * 3] ?? 0) * 1e5)},${Math.round((p[v * 3 + 1] ?? 0) * 1e5)},${Math.round((p[v * 3 + 2] ?? 0) * 1e5)}`;
|
|
28
|
+
let id = weld.get(key);
|
|
29
|
+
if (id === undefined) {
|
|
30
|
+
id = weld.size;
|
|
31
|
+
weld.set(key, id);
|
|
32
|
+
}
|
|
33
|
+
group[v] = id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* For each welded position, the faces meeting there: unit normal and the corner angle. */
|
|
37
|
+
const corners: number[][] = Array.from({ length: weld.size }, () => []);
|
|
38
|
+
for (let t = 0; t < idx.length; t += 3) {
|
|
39
|
+
const ids = [idx[t] ?? 0, idx[t + 1] ?? 0, idx[t + 2] ?? 0];
|
|
40
|
+
const at = (i: number, c: number): number => p[(ids[i] ?? 0) * 3 + c] ?? 0;
|
|
41
|
+
const ux = at(1, 0) - at(0, 0);
|
|
42
|
+
const uy = at(1, 1) - at(0, 1);
|
|
43
|
+
const uz = at(1, 2) - at(0, 2);
|
|
44
|
+
const vx = at(2, 0) - at(0, 0);
|
|
45
|
+
const vy = at(2, 1) - at(0, 1);
|
|
46
|
+
const vz = at(2, 2) - at(0, 2);
|
|
47
|
+
let nx = uy * vz - uz * vy;
|
|
48
|
+
let ny = uz * vx - ux * vz;
|
|
49
|
+
let nz = ux * vy - uy * vx;
|
|
50
|
+
const len = Math.hypot(nx, ny, nz);
|
|
51
|
+
if (len < 1e-12) continue;
|
|
52
|
+
nx /= len;
|
|
53
|
+
ny /= len;
|
|
54
|
+
nz /= len;
|
|
55
|
+
for (let k = 0; k < 3; k++) {
|
|
56
|
+
const o = (k + 1) % 3;
|
|
57
|
+
const q = (k + 2) % 3;
|
|
58
|
+
const ax = at(o, 0) - at(k, 0);
|
|
59
|
+
const ay = at(o, 1) - at(k, 1);
|
|
60
|
+
const az = at(o, 2) - at(k, 2);
|
|
61
|
+
const bx = at(q, 0) - at(k, 0);
|
|
62
|
+
const by = at(q, 1) - at(k, 1);
|
|
63
|
+
const bz = at(q, 2) - at(k, 2);
|
|
64
|
+
const denom = Math.hypot(ax, ay, az) * Math.hypot(bx, by, bz) || 1;
|
|
65
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, (ax * bx + ay * by + az * bz) / denom)));
|
|
66
|
+
corners[group[ids[k] ?? 0] ?? 0]?.push(nx, ny, nz, angle);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normals = new Float32Array(count * 3);
|
|
71
|
+
for (let v = 0; v < count; v++) {
|
|
72
|
+
const own = [
|
|
73
|
+
solid.normals[v * 3] ?? 0,
|
|
74
|
+
solid.normals[v * 3 + 1] ?? 0,
|
|
75
|
+
solid.normals[v * 3 + 2] ?? 0,
|
|
76
|
+
];
|
|
77
|
+
const list = corners[group[v] ?? 0] ?? [];
|
|
78
|
+
let sx = 0;
|
|
79
|
+
let sy = 0;
|
|
80
|
+
let sz = 0;
|
|
81
|
+
for (let c = 0; c < list.length; c += 4) {
|
|
82
|
+
const fx = list[c] ?? 0;
|
|
83
|
+
const fy = list[c + 1] ?? 0;
|
|
84
|
+
const fz = list[c + 2] ?? 0;
|
|
85
|
+
if (fx * (own[0] ?? 0) + fy * (own[1] ?? 0) + fz * (own[2] ?? 0) < cosCrease) continue;
|
|
86
|
+
const w = list[c + 3] ?? 0;
|
|
87
|
+
sx += fx * w;
|
|
88
|
+
sy += fy * w;
|
|
89
|
+
sz += fz * w;
|
|
90
|
+
}
|
|
91
|
+
const len = Math.hypot(sx, sy, sz);
|
|
92
|
+
if (len < 1e-12) {
|
|
93
|
+
normals[v * 3] = own[0] ?? 0;
|
|
94
|
+
normals[v * 3 + 1] = own[1] ?? 0;
|
|
95
|
+
normals[v * 3 + 2] = own[2] ?? 0;
|
|
96
|
+
} else {
|
|
97
|
+
normals[v * 3] = sx / len;
|
|
98
|
+
normals[v * 3 + 1] = sy / len;
|
|
99
|
+
normals[v * 3 + 2] = sz / len;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { positions: solid.positions, normals, uvs: solid.uvs, indices: solid.indices };
|
|
103
|
+
}
|