topazcube 0.1.31 → 0.1.35
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/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { vec3 } from "../math.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate a bounding sphere from position data
|
|
5
|
+
* Uses Ritter's bounding sphere algorithm for a good approximation
|
|
6
|
+
*
|
|
7
|
+
* @param {Float32Array|Array} positions - Position data (x, y, z, x, y, z, ...)
|
|
8
|
+
* @returns {{ center: [number, number, number], radius: number }}
|
|
9
|
+
*/
|
|
10
|
+
function calculateBoundingSphere(positions) {
|
|
11
|
+
if (!positions || positions.length < 3) {
|
|
12
|
+
return { center: [0, 0, 0], radius: 0 }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const vertexCount = Math.floor(positions.length / 3)
|
|
16
|
+
|
|
17
|
+
// Step 1: Find the centroid (average of all points)
|
|
18
|
+
let cx = 0, cy = 0, cz = 0
|
|
19
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
20
|
+
cx += positions[i * 3]
|
|
21
|
+
cy += positions[i * 3 + 1]
|
|
22
|
+
cz += positions[i * 3 + 2]
|
|
23
|
+
}
|
|
24
|
+
cx /= vertexCount
|
|
25
|
+
cy /= vertexCount
|
|
26
|
+
cz /= vertexCount
|
|
27
|
+
|
|
28
|
+
// Step 2: Find the point farthest from the centroid
|
|
29
|
+
let maxDistSq = 0
|
|
30
|
+
let farthestIdx = 0
|
|
31
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
32
|
+
const dx = positions[i * 3] - cx
|
|
33
|
+
const dy = positions[i * 3 + 1] - cy
|
|
34
|
+
const dz = positions[i * 3 + 2] - cz
|
|
35
|
+
const distSq = dx * dx + dy * dy + dz * dz
|
|
36
|
+
if (distSq > maxDistSq) {
|
|
37
|
+
maxDistSq = distSq
|
|
38
|
+
farthestIdx = i
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Step 3: Find the point farthest from that point
|
|
43
|
+
let p1x = positions[farthestIdx * 3]
|
|
44
|
+
let p1y = positions[farthestIdx * 3 + 1]
|
|
45
|
+
let p1z = positions[farthestIdx * 3 + 2]
|
|
46
|
+
|
|
47
|
+
maxDistSq = 0
|
|
48
|
+
let oppositeIdx = 0
|
|
49
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
50
|
+
const dx = positions[i * 3] - p1x
|
|
51
|
+
const dy = positions[i * 3 + 1] - p1y
|
|
52
|
+
const dz = positions[i * 3 + 2] - p1z
|
|
53
|
+
const distSq = dx * dx + dy * dy + dz * dz
|
|
54
|
+
if (distSq > maxDistSq) {
|
|
55
|
+
maxDistSq = distSq
|
|
56
|
+
oppositeIdx = i
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let p2x = positions[oppositeIdx * 3]
|
|
61
|
+
let p2y = positions[oppositeIdx * 3 + 1]
|
|
62
|
+
let p2z = positions[oppositeIdx * 3 + 2]
|
|
63
|
+
|
|
64
|
+
// Step 4: Initial sphere from these two extreme points
|
|
65
|
+
let sphereCx = (p1x + p2x) * 0.5
|
|
66
|
+
let sphereCy = (p1y + p2y) * 0.5
|
|
67
|
+
let sphereCz = (p1z + p2z) * 0.5
|
|
68
|
+
let radius = Math.sqrt(maxDistSq) * 0.5
|
|
69
|
+
|
|
70
|
+
// Step 5: Ritter's expansion - grow sphere to include all points
|
|
71
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
72
|
+
const px = positions[i * 3]
|
|
73
|
+
const py = positions[i * 3 + 1]
|
|
74
|
+
const pz = positions[i * 3 + 2]
|
|
75
|
+
|
|
76
|
+
const dx = px - sphereCx
|
|
77
|
+
const dy = py - sphereCy
|
|
78
|
+
const dz = pz - sphereCz
|
|
79
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
80
|
+
|
|
81
|
+
if (dist > radius) {
|
|
82
|
+
// Point is outside sphere, expand to include it
|
|
83
|
+
const newRadius = (radius + dist) * 0.5
|
|
84
|
+
const ratio = (newRadius - radius) / dist
|
|
85
|
+
|
|
86
|
+
sphereCx += dx * ratio
|
|
87
|
+
sphereCy += dy * ratio
|
|
88
|
+
sphereCz += dz * ratio
|
|
89
|
+
radius = newRadius
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add a small epsilon to avoid floating-point issues
|
|
94
|
+
radius *= 1.001
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
center: [sphereCx, sphereCy, sphereCz],
|
|
98
|
+
radius: radius
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate axis-aligned bounding box from positions
|
|
104
|
+
* @param {Float32Array|Array} positions - Position data
|
|
105
|
+
* @returns {{ min: [number, number, number], max: [number, number, number] }}
|
|
106
|
+
*/
|
|
107
|
+
function calculateAABB(positions) {
|
|
108
|
+
if (!positions || positions.length < 3) {
|
|
109
|
+
return { min: [0, 0, 0], max: [0, 0, 0] }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const vertexCount = Math.floor(positions.length / 3)
|
|
113
|
+
|
|
114
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity
|
|
115
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
118
|
+
const x = positions[i * 3]
|
|
119
|
+
const y = positions[i * 3 + 1]
|
|
120
|
+
const z = positions[i * 3 + 2]
|
|
121
|
+
|
|
122
|
+
if (x < minX) minX = x
|
|
123
|
+
if (y < minY) minY = y
|
|
124
|
+
if (z < minZ) minZ = z
|
|
125
|
+
if (x > maxX) maxX = x
|
|
126
|
+
if (y > maxY) maxY = y
|
|
127
|
+
if (z > maxZ) maxZ = z
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
min: [minX, minY, minZ],
|
|
132
|
+
max: [maxX, maxY, maxZ]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create bounding sphere from AABB
|
|
138
|
+
*/
|
|
139
|
+
function boundingSphereFromAABB(aabb) {
|
|
140
|
+
const center = [
|
|
141
|
+
(aabb.min[0] + aabb.max[0]) * 0.5,
|
|
142
|
+
(aabb.min[1] + aabb.max[1]) * 0.5,
|
|
143
|
+
(aabb.min[2] + aabb.max[2]) * 0.5
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
const dx = aabb.max[0] - aabb.min[0]
|
|
147
|
+
const dy = aabb.max[1] - aabb.min[1]
|
|
148
|
+
const dz = aabb.max[2] - aabb.min[2]
|
|
149
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.5
|
|
150
|
+
|
|
151
|
+
return { center, radius }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Merge two bounding spheres
|
|
156
|
+
*/
|
|
157
|
+
function mergeBoundingSpheres(a, b) {
|
|
158
|
+
const dx = b.center[0] - a.center[0]
|
|
159
|
+
const dy = b.center[1] - a.center[1]
|
|
160
|
+
const dz = b.center[2] - a.center[2]
|
|
161
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
162
|
+
|
|
163
|
+
// Check if one sphere contains the other
|
|
164
|
+
if (dist + b.radius <= a.radius) {
|
|
165
|
+
return { center: [...a.center], radius: a.radius }
|
|
166
|
+
}
|
|
167
|
+
if (dist + a.radius <= b.radius) {
|
|
168
|
+
return { center: [...b.center], radius: b.radius }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Calculate new sphere that contains both
|
|
172
|
+
const newRadius = (dist + a.radius + b.radius) * 0.5
|
|
173
|
+
const ratio = (newRadius - a.radius) / dist
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
center: [
|
|
177
|
+
a.center[0] + dx * ratio,
|
|
178
|
+
a.center[1] + dy * ratio,
|
|
179
|
+
a.center[2] + dz * ratio
|
|
180
|
+
],
|
|
181
|
+
radius: newRadius
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Transform a bounding sphere by a matrix
|
|
187
|
+
* @param {Object} bsphere - Bounding sphere
|
|
188
|
+
* @param {mat4} matrix - Transform matrix
|
|
189
|
+
* @returns {Object} Transformed bounding sphere
|
|
190
|
+
*/
|
|
191
|
+
function transformBoundingSphere(bsphere, matrix) {
|
|
192
|
+
// Transform center
|
|
193
|
+
const center = vec3.create()
|
|
194
|
+
vec3.transformMat4(center, bsphere.center, matrix)
|
|
195
|
+
|
|
196
|
+
// Get scale factor from matrix (approximate as max axis scale)
|
|
197
|
+
const sx = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2])
|
|
198
|
+
const sy = Math.sqrt(matrix[4] * matrix[4] + matrix[5] * matrix[5] + matrix[6] * matrix[6])
|
|
199
|
+
const sz = Math.sqrt(matrix[8] * matrix[8] + matrix[9] * matrix[9] + matrix[10] * matrix[10])
|
|
200
|
+
const maxScale = Math.max(sx, sy, sz)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
center: [center[0], center[1], center[2]],
|
|
204
|
+
radius: bsphere.radius * maxScale
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Test if a bounding sphere is visible in a frustum
|
|
210
|
+
* @param {Object} bsphere - Bounding sphere { center, radius }
|
|
211
|
+
* @param {Float32Array} planes - Frustum planes (6 vec4s)
|
|
212
|
+
* @returns {boolean} True if visible
|
|
213
|
+
*/
|
|
214
|
+
function sphereInFrustum(bsphere, planes) {
|
|
215
|
+
for (let i = 0; i < 6; i++) {
|
|
216
|
+
const offset = i * 4
|
|
217
|
+
const nx = planes[offset]
|
|
218
|
+
const ny = planes[offset + 1]
|
|
219
|
+
const nz = planes[offset + 2]
|
|
220
|
+
const d = planes[offset + 3]
|
|
221
|
+
|
|
222
|
+
const dist = nx * bsphere.center[0] + ny * bsphere.center[1] + nz * bsphere.center[2] + d
|
|
223
|
+
|
|
224
|
+
if (dist < -bsphere.radius) {
|
|
225
|
+
return false // Completely outside this plane
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Test if a bounding sphere is within a distance from a point
|
|
233
|
+
* @param {Object} bsphere - Bounding sphere
|
|
234
|
+
* @param {Array} point - Test point [x, y, z]
|
|
235
|
+
* @param {number} maxDistance - Maximum distance
|
|
236
|
+
* @returns {boolean} True if within distance
|
|
237
|
+
*/
|
|
238
|
+
function sphereWithinDistance(bsphere, point, maxDistance) {
|
|
239
|
+
const dx = bsphere.center[0] - point[0]
|
|
240
|
+
const dy = bsphere.center[1] - point[1]
|
|
241
|
+
const dz = bsphere.center[2] - point[2]
|
|
242
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) - bsphere.radius
|
|
243
|
+
return dist <= maxDistance
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculate a "shadow bounding sphere" that encompasses both the object
|
|
248
|
+
* and its shadow projection on the ground plane.
|
|
249
|
+
*
|
|
250
|
+
* This is used for shadow map culling optimization - we can cull objects
|
|
251
|
+
* whose shadow spheres are not visible to the camera (frustum/occlusion).
|
|
252
|
+
*
|
|
253
|
+
* The algorithm:
|
|
254
|
+
* 1. Start with the object's world-space bounding sphere
|
|
255
|
+
* 2. Project the sphere onto the ground plane along the light direction
|
|
256
|
+
* - This creates an ellipse on the ground
|
|
257
|
+
* 3. Calculate a bounding sphere that contains both the object sphere
|
|
258
|
+
* and the shadow ellipse
|
|
259
|
+
*
|
|
260
|
+
* @param {Object} bsphere - Object's bounding sphere { center: [x,y,z], radius: r }
|
|
261
|
+
* @param {Array} lightDir - Normalized light direction vector [x,y,z] (pointing TO light)
|
|
262
|
+
* @param {number} groundLevel - Y coordinate of the ground/shadow receiver plane
|
|
263
|
+
* @returns {Object} Shadow bounding sphere { center: [x,y,z], radius: r }
|
|
264
|
+
*/
|
|
265
|
+
function calculateShadowBoundingSphere(bsphere, lightDir, groundLevel = 0) {
|
|
266
|
+
// Light direction should point FROM object TO light (i.e., towards the sky for sun)
|
|
267
|
+
// If Y component is positive, light comes from above
|
|
268
|
+
const lx = lightDir[0]
|
|
269
|
+
const ly = lightDir[1]
|
|
270
|
+
const lz = lightDir[2]
|
|
271
|
+
|
|
272
|
+
// Object sphere properties
|
|
273
|
+
const cx = bsphere.center[0]
|
|
274
|
+
const cy = bsphere.center[1]
|
|
275
|
+
const cz = bsphere.center[2]
|
|
276
|
+
const r = bsphere.radius
|
|
277
|
+
|
|
278
|
+
// If object is below ground level, its shadow is above it (not on ground)
|
|
279
|
+
// In that case, just return the original sphere with some margin
|
|
280
|
+
if (cy + r < groundLevel) {
|
|
281
|
+
return {
|
|
282
|
+
center: [...bsphere.center],
|
|
283
|
+
radius: r * 1.1
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Calculate the height of sphere center above ground
|
|
288
|
+
const heightAboveGround = cy - groundLevel
|
|
289
|
+
|
|
290
|
+
// If light is nearly horizontal (ly close to 0), shadow extends very far
|
|
291
|
+
// Clamp to a reasonable maximum shadow distance
|
|
292
|
+
const maxShadowDistance = 100
|
|
293
|
+
|
|
294
|
+
// Calculate shadow center on ground:
|
|
295
|
+
// The center of the sphere projects along light direction to ground
|
|
296
|
+
// shadowPos = center + t * (-lightDir) where t = heightAboveGround / ly
|
|
297
|
+
let shadowCenterX, shadowCenterZ
|
|
298
|
+
|
|
299
|
+
if (Math.abs(ly) < 0.01) {
|
|
300
|
+
// Nearly horizontal light - shadow extends very far
|
|
301
|
+
// Use a reasonable estimate: shadow at maxShadowDistance in light direction
|
|
302
|
+
const horizLen = Math.sqrt(lx * lx + lz * lz)
|
|
303
|
+
if (horizLen > 0.001) {
|
|
304
|
+
shadowCenterX = cx - (lx / horizLen) * maxShadowDistance
|
|
305
|
+
shadowCenterZ = cz - (lz / horizLen) * maxShadowDistance
|
|
306
|
+
} else {
|
|
307
|
+
shadowCenterX = cx
|
|
308
|
+
shadowCenterZ = cz
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Normal case: calculate projection along light ray
|
|
312
|
+
const t = heightAboveGround / ly
|
|
313
|
+
// Clamp shadow distance
|
|
314
|
+
const clampedT = Math.min(Math.abs(t), maxShadowDistance) * Math.sign(t)
|
|
315
|
+
shadowCenterX = cx - lx * clampedT
|
|
316
|
+
shadowCenterZ = cz - lz * clampedT
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The shadow of a sphere is an ellipse when light is not vertical
|
|
320
|
+
// The ellipse's major axis extends along the light direction
|
|
321
|
+
// For simplicity, we approximate the shadow as a circle with radius
|
|
322
|
+
// that encompasses the ellipse
|
|
323
|
+
|
|
324
|
+
// Shadow radius depends on the angle of the light
|
|
325
|
+
// When light is vertical (ly=1), shadow radius = object radius
|
|
326
|
+
// When light is at an angle, shadow stretches along the light direction
|
|
327
|
+
const cosAngle = Math.abs(ly)
|
|
328
|
+
const sinAngle = Math.sqrt(1 - cosAngle * cosAngle)
|
|
329
|
+
|
|
330
|
+
// Shadow elongation factor (how much the shadow stretches)
|
|
331
|
+
// When light angle is 45°, shadow is ~1.4x longer
|
|
332
|
+
// When light angle is 30° (from horizon), shadow is ~2x longer
|
|
333
|
+
const elongation = cosAngle > 0.01 ? 1 / cosAngle : maxShadowDistance / r
|
|
334
|
+
const clampedElongation = Math.min(elongation, maxShadowDistance / Math.max(r, 0.1))
|
|
335
|
+
|
|
336
|
+
// Shadow "radius" (bounding circle of the ellipse)
|
|
337
|
+
const shadowRadius = r * Math.max(1, clampedElongation)
|
|
338
|
+
|
|
339
|
+
// Now we have two spheres to encompass:
|
|
340
|
+
// 1. Object sphere: center=(cx, cy, cz), radius=r
|
|
341
|
+
// 2. Shadow "sphere" on ground: center=(shadowCenterX, groundLevel, shadowCenterZ), radius=shadowRadius
|
|
342
|
+
|
|
343
|
+
// Create a sphere that contains both
|
|
344
|
+
// Method: find the enclosing sphere of two spheres
|
|
345
|
+
const s1 = {
|
|
346
|
+
center: [cx, cy, cz],
|
|
347
|
+
radius: r
|
|
348
|
+
}
|
|
349
|
+
const s2 = {
|
|
350
|
+
center: [shadowCenterX, groundLevel, shadowCenterZ],
|
|
351
|
+
radius: shadowRadius
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Distance between sphere centers
|
|
355
|
+
const dx = s2.center[0] - s1.center[0]
|
|
356
|
+
const dy = s2.center[1] - s1.center[1]
|
|
357
|
+
const dz = s2.center[2] - s1.center[2]
|
|
358
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
359
|
+
|
|
360
|
+
// Check containment
|
|
361
|
+
if (dist + s2.radius <= s1.radius) {
|
|
362
|
+
// Shadow is inside object sphere (unlikely but handle it)
|
|
363
|
+
return { center: [...s1.center], radius: s1.radius * 1.01 }
|
|
364
|
+
}
|
|
365
|
+
if (dist + s1.radius <= s2.radius) {
|
|
366
|
+
// Object is inside shadow sphere
|
|
367
|
+
return { center: [...s2.center], radius: s2.radius * 1.01 }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Calculate enclosing sphere
|
|
371
|
+
const newRadius = (dist + s1.radius + s2.radius) * 0.5
|
|
372
|
+
|
|
373
|
+
// Center is along the line between sphere centers
|
|
374
|
+
// Position it so both spheres fit
|
|
375
|
+
const ratio = dist > 0.001 ? (newRadius - s1.radius) / dist : 0.5
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
center: [
|
|
379
|
+
s1.center[0] + dx * ratio,
|
|
380
|
+
s1.center[1] + dy * ratio,
|
|
381
|
+
s1.center[2] + dz * ratio
|
|
382
|
+
],
|
|
383
|
+
radius: newRadius * 1.01 // Small margin
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Test if a bounding sphere intersects a cascade's orthographic box
|
|
389
|
+
*
|
|
390
|
+
* @param {Object} bsphere - Bounding sphere { center: [x,y,z], radius: r }
|
|
391
|
+
* @param {mat4} cascadeMatrix - Cascade's light view-projection matrix
|
|
392
|
+
* @returns {boolean} True if sphere intersects the cascade box
|
|
393
|
+
*/
|
|
394
|
+
function sphereInCascade(bsphere, cascadeMatrix) {
|
|
395
|
+
// Transform sphere center to cascade clip space
|
|
396
|
+
const cx = bsphere.center[0]
|
|
397
|
+
const cy = bsphere.center[1]
|
|
398
|
+
const cz = bsphere.center[2]
|
|
399
|
+
|
|
400
|
+
// Apply cascade view-projection matrix to center
|
|
401
|
+
// clipPos = cascadeMatrix * vec4(center, 1)
|
|
402
|
+
const m = cascadeMatrix
|
|
403
|
+
const w = m[3] * cx + m[7] * cy + m[11] * cz + m[15]
|
|
404
|
+
|
|
405
|
+
if (Math.abs(w) < 0.0001) return true // Degenerate case, include it
|
|
406
|
+
|
|
407
|
+
const invW = 1.0 / w
|
|
408
|
+
const clipX = (m[0] * cx + m[4] * cy + m[8] * cz + m[12]) * invW
|
|
409
|
+
const clipY = (m[1] * cx + m[5] * cy + m[9] * cz + m[13]) * invW
|
|
410
|
+
const clipZ = (m[2] * cx + m[6] * cy + m[10] * cz + m[14]) * invW
|
|
411
|
+
|
|
412
|
+
// Get scale factor for radius (approximate as max axis scale)
|
|
413
|
+
// For orthographic projections, this is simpler
|
|
414
|
+
const sx = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2])
|
|
415
|
+
const sy = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6])
|
|
416
|
+
const sz = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10])
|
|
417
|
+
const maxScale = Math.max(sx, sy, sz)
|
|
418
|
+
const clipRadius = bsphere.radius * maxScale * invW
|
|
419
|
+
|
|
420
|
+
// NDC box is [-1, 1] for X/Y, [0, 1] for Z in WebGPU
|
|
421
|
+
// Check if sphere (with radius) intersects this box
|
|
422
|
+
if (clipX + clipRadius < -1 || clipX - clipRadius > 1) return false
|
|
423
|
+
if (clipY + clipRadius < -1 || clipY - clipRadius > 1) return false
|
|
424
|
+
if (clipZ + clipRadius < 0 || clipZ - clipRadius > 1) return false
|
|
425
|
+
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export {
|
|
430
|
+
calculateBoundingSphere,
|
|
431
|
+
calculateAABB,
|
|
432
|
+
boundingSphereFromAABB,
|
|
433
|
+
mergeBoundingSpheres,
|
|
434
|
+
transformBoundingSphere,
|
|
435
|
+
sphereInFrustum,
|
|
436
|
+
sphereWithinDistance,
|
|
437
|
+
calculateShadowBoundingSphere,
|
|
438
|
+
sphereInCascade
|
|
439
|
+
}
|