@solidrt/3d 0.0.55 → 0.0.57
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/AGENTS.md +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Environment baking: the pure pipeline behind `srt tool 3d/environment`
|
|
2
|
+
// and its tests. A Radiance .hdr panorama is decoded to linear float, laid
|
|
3
|
+
// onto the six faces of a cube, convolved level by level with the GGX lobe
|
|
4
|
+
// `standard`'s split sum samples (Karis's importance sampling, the source
|
|
5
|
+
// lod picked from each sample's solid angle - the Filament and Unity
|
|
6
|
+
// method; Godot and Three convolve the same way at runtime), and packed
|
|
7
|
+
// into the .srte container loadEnvironment uploads as an explicit rgba16f
|
|
8
|
+
// chain. Runs under bun and on the runtime alike: no GPU, no flux imports.
|
|
9
|
+
//
|
|
10
|
+
// Cube faces hold what a GL lookup of a world direction returns (GL's own
|
|
11
|
+
// convention, no library flip): cubeDirection is the texel-to-direction
|
|
12
|
+
// table and cubeLookup its inverse.
|
|
13
|
+
|
|
14
|
+
/** Face edge of the mip level a roughness of 1 samples (ENVIRONMENT in
|
|
15
|
+
* glsl.ts maps roughness onto the chain by it): the chain is convolved
|
|
16
|
+
* down to this edge, and the levels below repeat roughness 1 - never
|
|
17
|
+
* read by a material, present because a chain must reach 1x1. */
|
|
18
|
+
export const ENV_ROUGH_FACE = 4
|
|
19
|
+
// GGX samples per texel of a convolved level.
|
|
20
|
+
const PREFILTER_SAMPLES = 512
|
|
21
|
+
// Bias on the source lod a sample reads (Karis: +1 smooths the estimate
|
|
22
|
+
// without visible extra blur).
|
|
23
|
+
const SOURCE_LOD_BIAS = 1
|
|
24
|
+
// A sample's normal-lobe cosine below this contributes nothing (the lobe
|
|
25
|
+
// mirrored below the horizon).
|
|
26
|
+
const MIN_NOL = 1e-6
|
|
27
|
+
// The mip level count of a chain from `size` down to 1x1 (GL's rule).
|
|
28
|
+
export function mipLevels(size: number): number {
|
|
29
|
+
return 32 - Math.clz32(size)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A decoded panorama: linear rgb floats, row-major from the top row. */
|
|
33
|
+
export type Panorama = { width: number; height: number; data: Float32Array }
|
|
34
|
+
/** Six rgba float faces in GL order (+X, -X, +Y, -Y, +Z, -Z). */
|
|
35
|
+
export type CubeFaces = Float32Array[]
|
|
36
|
+
|
|
37
|
+
// The Radiance format tag a .hdr carries (or omits, meaning the same).
|
|
38
|
+
const RGBE_FORMAT = "32-bit_rle_rgbe"
|
|
39
|
+
// New-style RLE scanlines exist only in this width range (the format's rule).
|
|
40
|
+
const RLE_MIN_WIDTH = 8
|
|
41
|
+
const RLE_MAX_WIDTH = 0x7fff
|
|
42
|
+
// A run byte above this is a repeat of the next byte (count - 128 times).
|
|
43
|
+
const RUN_FLAG = 128
|
|
44
|
+
// The RGBE exponent bias: a mantissa byte scales by 2^(e - 128) / 255.
|
|
45
|
+
const RGBE_EXPONENT_BIAS = 128
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decode a Radiance .hdr (RGBE) file into linear rgb floats: the header
|
|
49
|
+
* (`#?` signature, `FORMAT=32-bit_rle_rgbe`), the standard `-Y h +X w`
|
|
50
|
+
* orientation, flat or new-style run-length scanlines - what every HDRI
|
|
51
|
+
* source ships (Three's RGBELoader reads the same subset). Throws on a
|
|
52
|
+
* truncated file, another orientation or the pre-1990 old-style RLE.
|
|
53
|
+
*/
|
|
54
|
+
export function decodeHdr(bytes: Uint8Array): Panorama {
|
|
55
|
+
let pos = 0
|
|
56
|
+
let readLine = (): string => {
|
|
57
|
+
let s = ""
|
|
58
|
+
while (pos < bytes.length) {
|
|
59
|
+
let c = bytes[pos++]!
|
|
60
|
+
if (c === 0x0a) break
|
|
61
|
+
s += String.fromCharCode(c)
|
|
62
|
+
}
|
|
63
|
+
return s
|
|
64
|
+
}
|
|
65
|
+
if (!readLine().startsWith("#?")) throw new Error("decodeHdr: not a Radiance .hdr file (no #? signature)")
|
|
66
|
+
let format = RGBE_FORMAT
|
|
67
|
+
for (;;) {
|
|
68
|
+
if (pos >= bytes.length) throw new Error("decodeHdr: truncated header")
|
|
69
|
+
let line = readLine()
|
|
70
|
+
if (line === "") break
|
|
71
|
+
if (line.startsWith("FORMAT=")) format = line.slice("FORMAT=".length)
|
|
72
|
+
}
|
|
73
|
+
if (format !== RGBE_FORMAT) throw new Error("decodeHdr: unsupported FORMAT " + format + " (only " + RGBE_FORMAT + ")")
|
|
74
|
+
let res = readLine().trim().split(/\s+/)
|
|
75
|
+
if (res.length !== 4 || res[0] !== "-Y" || res[2] !== "+X") {
|
|
76
|
+
throw new Error("decodeHdr: unsupported orientation " + res.join(" ") + " (only -Y <height> +X <width>)")
|
|
77
|
+
}
|
|
78
|
+
let height = Number(res[1])
|
|
79
|
+
let width = Number(res[3])
|
|
80
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
81
|
+
throw new Error("decodeHdr: bad resolution " + res.join(" "))
|
|
82
|
+
}
|
|
83
|
+
let data = new Float32Array(width * height * 3)
|
|
84
|
+
let rgbe = new Uint8Array(width * 4)
|
|
85
|
+
let rle = width >= RLE_MIN_WIDTH && width <= RLE_MAX_WIDTH
|
|
86
|
+
let byte = (): number => {
|
|
87
|
+
if (pos >= bytes.length) throw new Error("decodeHdr: truncated pixel data")
|
|
88
|
+
return bytes[pos++]!
|
|
89
|
+
}
|
|
90
|
+
for (let y = 0; y < height; y++) {
|
|
91
|
+
if (rle && bytes[pos] === 2 && bytes[pos + 1] === 2 && ((bytes[pos + 2]! << 8) | bytes[pos + 3]!) === width) {
|
|
92
|
+
pos += 4
|
|
93
|
+
for (let ch = 0; ch < 4; ch++) {
|
|
94
|
+
let x = 0
|
|
95
|
+
while (x < width) {
|
|
96
|
+
let count = byte()
|
|
97
|
+
if (count > RUN_FLAG) {
|
|
98
|
+
count -= RUN_FLAG
|
|
99
|
+
if (x + count > width) throw new Error("decodeHdr: run overflows scanline " + y)
|
|
100
|
+
let v = byte()
|
|
101
|
+
while (count-- > 0) rgbe[x++ * 4 + ch] = v
|
|
102
|
+
} else {
|
|
103
|
+
if (count === 0 || x + count > width) throw new Error("decodeHdr: run overflows scanline " + y)
|
|
104
|
+
while (count-- > 0) rgbe[x++ * 4 + ch] = byte()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
for (let x = 0; x < width; x++) {
|
|
110
|
+
let r = byte()
|
|
111
|
+
let g = byte()
|
|
112
|
+
let b = byte()
|
|
113
|
+
let e = byte()
|
|
114
|
+
if (r === 1 && g === 1 && b === 1) throw new Error("decodeHdr: old-style RLE is not supported")
|
|
115
|
+
rgbe[x * 4] = r
|
|
116
|
+
rgbe[x * 4 + 1] = g
|
|
117
|
+
rgbe[x * 4 + 2] = b
|
|
118
|
+
rgbe[x * 4 + 3] = e
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let row = y * width * 3
|
|
122
|
+
for (let x = 0; x < width; x++) {
|
|
123
|
+
let e = rgbe[x * 4 + 3]!
|
|
124
|
+
let scale = e === 0 ? 0 : Math.pow(2, e - RGBE_EXPONENT_BIAS) / 255
|
|
125
|
+
data[row + x * 3] = rgbe[x * 4]! * scale
|
|
126
|
+
data[row + x * 3 + 1] = rgbe[x * 4 + 1]! * scale
|
|
127
|
+
data[row + x * 3 + 2] = rgbe[x * 4 + 2]! * scale
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { width, height, data }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The world direction a lookup must have to land on face `face` at
|
|
134
|
+
* (s, t) in 0..1 (t = 0 the first row): the GL cube-map table, written
|
|
135
|
+
* into `out` (unnormalized). */
|
|
136
|
+
export function cubeDirection(face: number, s: number, t: number, out: Float64Array): void {
|
|
137
|
+
let a = 2 * s - 1
|
|
138
|
+
let b = 2 * t - 1
|
|
139
|
+
let x: number
|
|
140
|
+
let y: number
|
|
141
|
+
let z: number
|
|
142
|
+
if (face === 0) (x = 1), (y = -b), (z = -a)
|
|
143
|
+
else if (face === 1) (x = -1), (y = -b), (z = a)
|
|
144
|
+
else if (face === 2) (x = a), (y = 1), (z = b)
|
|
145
|
+
else if (face === 3) (x = a), (y = -1), (z = -b)
|
|
146
|
+
else if (face === 4) (x = a), (y = -b), (z = 1)
|
|
147
|
+
else (x = -a), (y = -b), (z = -1)
|
|
148
|
+
out[0] = x
|
|
149
|
+
out[1] = y
|
|
150
|
+
out[2] = z
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The inverse of cubeDirection: the face and (s, t) a world direction
|
|
154
|
+
* lands on, written into `out` as [face, s, t]. */
|
|
155
|
+
export function cubeLookup(wx: number, wy: number, wz: number, out: Float64Array): void {
|
|
156
|
+
let dx = wx
|
|
157
|
+
let dy = wy
|
|
158
|
+
let dz = wz
|
|
159
|
+
let ax = Math.abs(dx)
|
|
160
|
+
let ay = Math.abs(dy)
|
|
161
|
+
let az = Math.abs(dz)
|
|
162
|
+
let face: number
|
|
163
|
+
let a: number
|
|
164
|
+
let b: number
|
|
165
|
+
if (ax >= ay && ax >= az) {
|
|
166
|
+
if (dx > 0) (face = 0), (a = -dz / dx), (b = -dy / dx)
|
|
167
|
+
else (face = 1), (a = -dz / dx), (b = dy / dx)
|
|
168
|
+
} else if (ay >= az) {
|
|
169
|
+
if (dy > 0) (face = 2), (a = dx / dy), (b = dz / dy)
|
|
170
|
+
else (face = 3), (a = -dx / dy), (b = dz / dy)
|
|
171
|
+
} else {
|
|
172
|
+
if (dz > 0) (face = 4), (a = dx / dz), (b = -dy / dz)
|
|
173
|
+
else (face = 5), (a = dx / dz), (b = dy / dz)
|
|
174
|
+
}
|
|
175
|
+
out[0] = face
|
|
176
|
+
out[1] = (a + 1) / 2
|
|
177
|
+
out[2] = (b + 1) / 2
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Bilinear panorama sample along a direction (the GLSL equirect mapping:
|
|
181
|
+
// the center column faces -Z, the top row +Y), wrapping horizontally and
|
|
182
|
+
// clamping at the poles; accumulated into `out` at weight `w`.
|
|
183
|
+
function samplePanorama(p: Panorama, dx: number, dy: number, dz: number, w: number, out: Float64Array): void {
|
|
184
|
+
let u = Math.atan2(dx, -dz) / (2 * Math.PI) + 0.5
|
|
185
|
+
let v = Math.acos(Math.max(-1, Math.min(1, dy))) / Math.PI
|
|
186
|
+
let fx = u * p.width - 0.5
|
|
187
|
+
let fy = v * p.height - 0.5
|
|
188
|
+
let x0 = Math.floor(fx)
|
|
189
|
+
let y0 = Math.floor(fy)
|
|
190
|
+
let tx = fx - x0
|
|
191
|
+
let ty = fy - y0
|
|
192
|
+
let x1 = (((x0 + 1) % p.width) + p.width) % p.width
|
|
193
|
+
x0 = ((x0 % p.width) + p.width) % p.width
|
|
194
|
+
let y1 = Math.min(p.height - 1, Math.max(0, y0 + 1))
|
|
195
|
+
y0 = Math.min(p.height - 1, Math.max(0, y0))
|
|
196
|
+
let d = p.data
|
|
197
|
+
let i00 = (y0 * p.width + x0) * 3
|
|
198
|
+
let i10 = (y0 * p.width + x1) * 3
|
|
199
|
+
let i01 = (y1 * p.width + x0) * 3
|
|
200
|
+
let i11 = (y1 * p.width + x1) * 3
|
|
201
|
+
let w00 = (1 - tx) * (1 - ty) * w
|
|
202
|
+
let w10 = tx * (1 - ty) * w
|
|
203
|
+
let w01 = (1 - tx) * ty * w
|
|
204
|
+
let w11 = tx * ty * w
|
|
205
|
+
for (let c = 0; c < 3; c++) {
|
|
206
|
+
out[c] = out[c]! + d[i00 + c]! * w00 + d[i10 + c]! * w10 + d[i01 + c]! * w01 + d[i11 + c]! * w11
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Lay a panorama onto the six faces of a `size` cube (rgba floats, alpha
|
|
212
|
+
* 1), supersampling each texel enough to average the panorama's finer
|
|
213
|
+
* texels rather than skip them. The CPU form of equirectToCube.
|
|
214
|
+
*/
|
|
215
|
+
export function panoramaToCube(p: Panorama, size: number): CubeFaces {
|
|
216
|
+
if (!Number.isInteger(size) || size < 1) throw new Error("panoramaToCube: size must be a positive integer, got " + size)
|
|
217
|
+
// Sub-samples per axis: the panorama's texels along the equator per cube
|
|
218
|
+
// texel (a face spans a quarter turn).
|
|
219
|
+
let ss = Math.max(1, Math.ceil(p.width / (4 * size)))
|
|
220
|
+
let weight = 1 / (ss * ss)
|
|
221
|
+
let dir = new Float64Array(3)
|
|
222
|
+
let acc = new Float64Array(3)
|
|
223
|
+
let faces: CubeFaces = []
|
|
224
|
+
for (let face = 0; face < 6; face++) {
|
|
225
|
+
let px = new Float32Array(size * size * 4)
|
|
226
|
+
for (let y = 0; y < size; y++) {
|
|
227
|
+
for (let x = 0; x < size; x++) {
|
|
228
|
+
acc[0] = acc[1] = acc[2] = 0
|
|
229
|
+
for (let j = 0; j < ss; j++) {
|
|
230
|
+
for (let i = 0; i < ss; i++) {
|
|
231
|
+
cubeDirection(face, (x + (i + 0.5) / ss) / size, (y + (j + 0.5) / ss) / size, dir)
|
|
232
|
+
let n = 1 / Math.hypot(dir[0]!, dir[1]!, dir[2]!)
|
|
233
|
+
samplePanorama(p, dir[0]! * n, dir[1]! * n, dir[2]! * n, weight, acc)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let o = (y * size + x) * 4
|
|
237
|
+
px[o] = acc[0]!
|
|
238
|
+
px[o + 1] = acc[1]!
|
|
239
|
+
px[o + 2] = acc[2]!
|
|
240
|
+
px[o + 3] = 1
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
faces.push(px)
|
|
244
|
+
}
|
|
245
|
+
return faces
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The 2x2 box chain of a cube down to 1x1 (within each face; the source
|
|
249
|
+
// the convolution reads at a per-sample lod).
|
|
250
|
+
function boxChain(base: CubeFaces, size: number): CubeFaces[] {
|
|
251
|
+
let chain = [base]
|
|
252
|
+
let edge = size
|
|
253
|
+
while (edge > 1) {
|
|
254
|
+
let prev = chain[chain.length - 1]!
|
|
255
|
+
let next = Math.max(1, edge >> 1)
|
|
256
|
+
let level: CubeFaces = []
|
|
257
|
+
for (let face = 0; face < 6; face++) {
|
|
258
|
+
let src = prev[face]!
|
|
259
|
+
let dst = new Float32Array(next * next * 4)
|
|
260
|
+
for (let y = 0; y < next; y++) {
|
|
261
|
+
for (let x = 0; x < next; x++) {
|
|
262
|
+
let x0 = Math.min(edge - 1, x * 2)
|
|
263
|
+
let x1 = Math.min(edge - 1, x * 2 + 1)
|
|
264
|
+
let y0 = Math.min(edge - 1, y * 2)
|
|
265
|
+
let y1 = Math.min(edge - 1, y * 2 + 1)
|
|
266
|
+
let o = (y * next + x) * 4
|
|
267
|
+
for (let c = 0; c < 4; c++) {
|
|
268
|
+
dst[o + c] =
|
|
269
|
+
(src[(y0 * edge + x0) * 4 + c]! + src[(y0 * edge + x1) * 4 + c]! + src[(y1 * edge + x0) * 4 + c]! + src[(y1 * edge + x1) * 4 + c]!) / 4
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
level.push(dst)
|
|
274
|
+
}
|
|
275
|
+
chain.push(level)
|
|
276
|
+
edge = next
|
|
277
|
+
}
|
|
278
|
+
return chain
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Bilinear cube fetch within the face a direction lands on (no cross-face
|
|
282
|
+
// blend; the hardware's seamless filtering is not needed for a bake),
|
|
283
|
+
// accumulated into `out` at weight `w`.
|
|
284
|
+
function fetchCube(level: CubeFaces, edge: number, dx: number, dy: number, dz: number, w: number, lk: Float64Array, out: Float64Array): void {
|
|
285
|
+
cubeLookup(dx, dy, dz, lk)
|
|
286
|
+
let px = level[lk[0]!]!
|
|
287
|
+
let fx = Math.min(edge - 1, Math.max(0, lk[1]! * edge - 0.5))
|
|
288
|
+
let fy = Math.min(edge - 1, Math.max(0, lk[2]! * edge - 0.5))
|
|
289
|
+
let x0 = Math.floor(fx)
|
|
290
|
+
let y0 = Math.floor(fy)
|
|
291
|
+
let tx = fx - x0
|
|
292
|
+
let ty = fy - y0
|
|
293
|
+
let x1 = Math.min(edge - 1, x0 + 1)
|
|
294
|
+
let y1 = Math.min(edge - 1, y0 + 1)
|
|
295
|
+
let i00 = (y0 * edge + x0) * 4
|
|
296
|
+
let i10 = (y0 * edge + x1) * 4
|
|
297
|
+
let i01 = (y1 * edge + x0) * 4
|
|
298
|
+
let i11 = (y1 * edge + x1) * 4
|
|
299
|
+
let w00 = (1 - tx) * (1 - ty) * w
|
|
300
|
+
let w10 = tx * (1 - ty) * w
|
|
301
|
+
let w01 = (1 - tx) * ty * w
|
|
302
|
+
let w11 = tx * ty * w
|
|
303
|
+
for (let c = 0; c < 3; c++) {
|
|
304
|
+
out[c] = out[c]! + px[i00 + c]! * w00 + px[i10 + c]! * w10 + px[i01 + c]! * w01 + px[i11 + c]! * w11
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Van der Corput radical inverse: the second Hammersley coordinate.
|
|
309
|
+
function radicalInverse(i: number): number {
|
|
310
|
+
let bits = i >>> 0
|
|
311
|
+
bits = ((bits << 16) | (bits >>> 16)) >>> 0
|
|
312
|
+
bits = (((bits & 0x55555555) << 1) | ((bits & 0xaaaaaaaa) >>> 1)) >>> 0
|
|
313
|
+
bits = (((bits & 0x33333333) << 2) | ((bits & 0xcccccccc) >>> 2)) >>> 0
|
|
314
|
+
bits = (((bits & 0x0f0f0f0f) << 4) | ((bits & 0xf0f0f0f0) >>> 4)) >>> 0
|
|
315
|
+
bits = (((bits & 0x00ff00ff) << 8) | ((bits & 0xff00ff00) >>> 8)) >>> 0
|
|
316
|
+
return bits / 4294967296
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The perceptual roughness mip level `level` of a `size` chain holds:
|
|
320
|
+
* linear from 0 at the base to 1 at the ENV_ROUGH_FACE level, 1 below it. */
|
|
321
|
+
export function levelRoughness(size: number, level: number): number {
|
|
322
|
+
let roughLevels = Math.log2(size) - Math.log2(ENV_ROUGH_FACE)
|
|
323
|
+
return roughLevels <= 0 ? 1 : Math.min(1, level / roughLevels)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Convolve a cube into its full mip chain: level 0 the faces as given, each
|
|
328
|
+
* level below prefiltered with the GGX lobe at levelRoughness (the normal,
|
|
329
|
+
* view and reflection directions taken equal, as every engine's bake
|
|
330
|
+
* does), by importance sampling with the source read at the lod that
|
|
331
|
+
* matches each sample's solid angle. Returns the levels, base first, each
|
|
332
|
+
* six rgba float faces.
|
|
333
|
+
*/
|
|
334
|
+
export function prefilterCube(base: CubeFaces, size: number): CubeFaces[] {
|
|
335
|
+
if (base.length !== 6) throw new Error("prefilterCube: six faces expected, got " + base.length)
|
|
336
|
+
let source = boxChain(base, size)
|
|
337
|
+
let levels: CubeFaces[] = [base.map(f => Float32Array.from(f))]
|
|
338
|
+
let count = mipLevels(size)
|
|
339
|
+
// The solid angle of one base-level source texel.
|
|
340
|
+
let texelAngle = (4 * Math.PI) / (6 * size * size)
|
|
341
|
+
let dir = new Float64Array(3)
|
|
342
|
+
let lk = new Float64Array(3)
|
|
343
|
+
let acc = new Float64Array(3)
|
|
344
|
+
// Per level, the sample set in the normal's tangent frame: it depends on
|
|
345
|
+
// roughness only, so it is built once and reused for every texel.
|
|
346
|
+
let sx = new Float64Array(PREFILTER_SAMPLES)
|
|
347
|
+
let sy = new Float64Array(PREFILTER_SAMPLES)
|
|
348
|
+
let sz = new Float64Array(PREFILTER_SAMPLES)
|
|
349
|
+
let sLod = new Int32Array(PREFILTER_SAMPLES)
|
|
350
|
+
let sNol = new Float64Array(PREFILTER_SAMPLES)
|
|
351
|
+
for (let level = 1; level < count; level++) {
|
|
352
|
+
let edge = Math.max(1, size >> level)
|
|
353
|
+
let rough = levelRoughness(size, level)
|
|
354
|
+
let alpha = rough * rough
|
|
355
|
+
let a2 = alpha * alpha
|
|
356
|
+
let samples = 0
|
|
357
|
+
for (let i = 0; i < PREFILTER_SAMPLES; i++) {
|
|
358
|
+
let u1 = (i + 0.5) / PREFILTER_SAMPLES
|
|
359
|
+
let u2 = radicalInverse(i)
|
|
360
|
+
let phi = 2 * Math.PI * u1
|
|
361
|
+
let cosT = Math.sqrt((1 - u2) / (1 + (a2 - 1) * u2))
|
|
362
|
+
let sinT = Math.sqrt(Math.max(0, 1 - cosT * cosT))
|
|
363
|
+
// N = V, so the reflected sample's cosine is 2 cos^2 - 1.
|
|
364
|
+
let nol = 2 * cosT * cosT - 1
|
|
365
|
+
if (nol <= MIN_NOL) continue
|
|
366
|
+
let d = cosT * cosT * (a2 - 1) + 1
|
|
367
|
+
let ggx = a2 / (Math.PI * d * d)
|
|
368
|
+
let pdf = ggx / 4
|
|
369
|
+
let sampleAngle = 1 / (PREFILTER_SAMPLES * pdf)
|
|
370
|
+
let lod = Math.round(Math.max(0, 0.5 * Math.log2(sampleAngle / texelAngle) + SOURCE_LOD_BIAS))
|
|
371
|
+
sx[samples] = sinT * Math.cos(phi)
|
|
372
|
+
sy[samples] = sinT * Math.sin(phi)
|
|
373
|
+
sz[samples] = cosT
|
|
374
|
+
sLod[samples] = Math.min(source.length - 1, lod)
|
|
375
|
+
sNol[samples] = nol
|
|
376
|
+
samples++
|
|
377
|
+
}
|
|
378
|
+
let faces: CubeFaces = []
|
|
379
|
+
for (let face = 0; face < 6; face++) {
|
|
380
|
+
let px = new Float32Array(edge * edge * 4)
|
|
381
|
+
for (let y = 0; y < edge; y++) {
|
|
382
|
+
for (let x = 0; x < edge; x++) {
|
|
383
|
+
cubeDirection(face, (x + 0.5) / edge, (y + 0.5) / edge, dir)
|
|
384
|
+
let inv = 1 / Math.hypot(dir[0]!, dir[1]!, dir[2]!)
|
|
385
|
+
let nx = dir[0]! * inv
|
|
386
|
+
let ny = dir[1]! * inv
|
|
387
|
+
let nz = dir[2]! * inv
|
|
388
|
+
// A tangent frame around the normal.
|
|
389
|
+
let ux = 0
|
|
390
|
+
let uy = 0
|
|
391
|
+
let uz = 1
|
|
392
|
+
if (Math.abs(nz) > 0.999) (ux = 1), (uz = 0)
|
|
393
|
+
let tx = uy * nz - uz * ny
|
|
394
|
+
let ty = uz * nx - ux * nz
|
|
395
|
+
let tz = ux * ny - uy * nx
|
|
396
|
+
let tn = 1 / Math.hypot(tx, ty, tz)
|
|
397
|
+
tx *= tn
|
|
398
|
+
ty *= tn
|
|
399
|
+
tz *= tn
|
|
400
|
+
let bx = ny * tz - nz * ty
|
|
401
|
+
let by = nz * tx - nx * tz
|
|
402
|
+
let bz = nx * ty - ny * tx
|
|
403
|
+
acc[0] = acc[1] = acc[2] = 0
|
|
404
|
+
let weight = 0
|
|
405
|
+
for (let i = 0; i < samples; i++) {
|
|
406
|
+
let hx = tx * sx[i]! + bx * sy[i]! + nx * sz[i]!
|
|
407
|
+
let hy = ty * sx[i]! + by * sy[i]! + ny * sz[i]!
|
|
408
|
+
let hz = tz * sx[i]! + bz * sy[i]! + nz * sz[i]!
|
|
409
|
+
let k = 2 * sz[i]!
|
|
410
|
+
let lx = k * hx - nx
|
|
411
|
+
let ly = k * hy - ny
|
|
412
|
+
let lz = k * hz - nz
|
|
413
|
+
let lod = sLod[i]!
|
|
414
|
+
fetchCube(source[lod]!, Math.max(1, size >> lod), lx, ly, lz, sNol[i]!, lk, acc)
|
|
415
|
+
weight += sNol[i]!
|
|
416
|
+
}
|
|
417
|
+
let o = (y * edge + x) * 4
|
|
418
|
+
px[o] = acc[0]! / weight
|
|
419
|
+
px[o + 1] = acc[1]! / weight
|
|
420
|
+
px[o + 2] = acc[2]! / weight
|
|
421
|
+
px[o + 3] = 1
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
faces.push(px)
|
|
425
|
+
}
|
|
426
|
+
levels.push(faces)
|
|
427
|
+
}
|
|
428
|
+
return levels
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// The .srte container: "SRTE" u32 | version u32 | size u32 | levels u32,
|
|
432
|
+
// then the faces as float32 rgba, level-major, face-major - the exact
|
|
433
|
+
// buffers createCubeTexture's explicit chain takes, viewed in place.
|
|
434
|
+
/** "SRTE" read as a little-endian u32. */
|
|
435
|
+
const MAGIC = 0x45545253
|
|
436
|
+
const VERSION = 1
|
|
437
|
+
const HEADER_BYTES = 16
|
|
438
|
+
|
|
439
|
+
/** Serialize a prefiltered chain (prefilterCube's result) into the .srte container. */
|
|
440
|
+
export function encodeEnvironment(levels: CubeFaces[], size: number): Uint8Array {
|
|
441
|
+
if (levels.length !== mipLevels(size)) {
|
|
442
|
+
throw new Error("encodeEnvironment: a " + size + " chain has " + mipLevels(size) + " levels, got " + levels.length)
|
|
443
|
+
}
|
|
444
|
+
let total = HEADER_BYTES
|
|
445
|
+
for (let faces of levels) for (let f of faces) total += f.byteLength
|
|
446
|
+
let out = new Uint8Array(total)
|
|
447
|
+
let view = new DataView(out.buffer)
|
|
448
|
+
view.setUint32(0, MAGIC, true)
|
|
449
|
+
view.setUint32(4, VERSION, true)
|
|
450
|
+
view.setUint32(8, size, true)
|
|
451
|
+
view.setUint32(12, levels.length, true)
|
|
452
|
+
let pos = HEADER_BYTES
|
|
453
|
+
for (let faces of levels) {
|
|
454
|
+
for (let f of faces) {
|
|
455
|
+
out.set(new Uint8Array(f.buffer, f.byteOffset, f.byteLength), pos)
|
|
456
|
+
pos += f.byteLength
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return out
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Read a .srte container back into its size and chain, the faces viewing
|
|
463
|
+
* the bytes in place (copied only if the buffer is not float-aligned). */
|
|
464
|
+
export function decodeEnvironment(bytes: Uint8Array): { size: number; levels: CubeFaces[] } {
|
|
465
|
+
if (bytes.byteLength < HEADER_BYTES) throw new Error("decodeEnvironment: not an environment file (too short)")
|
|
466
|
+
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
467
|
+
if (view.getUint32(0, true) !== MAGIC) throw new Error("decodeEnvironment: not an environment file (bad magic)")
|
|
468
|
+
let version = view.getUint32(4, true)
|
|
469
|
+
if (version !== VERSION) throw new Error("decodeEnvironment: version " + version + ", expected " + VERSION + " - re-bake with srt tool 3d/environment")
|
|
470
|
+
let size = view.getUint32(8, true)
|
|
471
|
+
let count = view.getUint32(12, true)
|
|
472
|
+
if (size < 1 || count !== mipLevels(size)) throw new Error("decodeEnvironment: a " + size + " chain has " + mipLevels(size) + " levels, file says " + count)
|
|
473
|
+
let aligned = (bytes.byteOffset + HEADER_BYTES) % 4 === 0
|
|
474
|
+
let pos = HEADER_BYTES
|
|
475
|
+
let levels: CubeFaces[] = []
|
|
476
|
+
for (let level = 0; level < count; level++) {
|
|
477
|
+
let edge = Math.max(1, size >> level)
|
|
478
|
+
let floats = edge * edge * 4
|
|
479
|
+
let faces: CubeFaces = []
|
|
480
|
+
for (let face = 0; face < 6; face++) {
|
|
481
|
+
if (pos + floats * 4 > bytes.byteLength) throw new Error("decodeEnvironment: truncated at level " + level + " face " + face)
|
|
482
|
+
faces.push(
|
|
483
|
+
aligned
|
|
484
|
+
? new Float32Array(bytes.buffer, bytes.byteOffset + pos, floats)
|
|
485
|
+
: new Float32Array(bytes.slice(pos, pos + floats * 4).buffer),
|
|
486
|
+
)
|
|
487
|
+
pos += floats * 4
|
|
488
|
+
}
|
|
489
|
+
levels.push(faces)
|
|
490
|
+
}
|
|
491
|
+
return { size, levels }
|
|
492
|
+
}
|