reze-engine 0.41.1 → 0.41.3
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 +6 -3
- package/dist/dds-loader.d.ts +12 -0
- package/dist/dds-loader.d.ts.map +1 -0
- package/dist/dds-loader.js +246 -0
- package/dist/engine.d.ts +0 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +75 -11
- package/dist/psd-loader.d.ts +13 -0
- package/dist/psd-loader.d.ts.map +1 -0
- package/dist/psd-loader.js +192 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +19 -1
- package/package.json +1 -1
- package/src/dds-loader.ts +236 -0
- package/src/engine.ts +81 -19
- package/src/psd-loader.ts +193 -0
- package/src/shaders/passes/composite.ts +19 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// PSD → RGBA8, from the merged composite.
|
|
2
|
+
//
|
|
3
|
+
// MMD texture packs are often shipped as the artist's working files, so a
|
|
4
|
+
// material's texture path points at a .psd that no browser will decode. The
|
|
5
|
+
// layers are none of our business — Photoshop writes a flattened composite of
|
|
6
|
+
// the whole document at the end of the file, which is exactly the image the
|
|
7
|
+
// artist saw, so that is what this reads.
|
|
8
|
+
//
|
|
9
|
+
// (A file saved with "Maximize Compatibility" off has no useful composite. There
|
|
10
|
+
// is nothing to be done about that here beyond failing clearly: reconstructing it
|
|
11
|
+
// would mean compositing every layer, blend mode and clipping group — a Photoshop,
|
|
12
|
+
// not a texture loader.)
|
|
13
|
+
|
|
14
|
+
import type { DecodedImage } from "./tga-loader"
|
|
15
|
+
|
|
16
|
+
const MAGIC = 0x38425053 // "8BPS", big-endian — PSD is big-endian throughout
|
|
17
|
+
|
|
18
|
+
const enum ColorMode {
|
|
19
|
+
Bitmap = 0,
|
|
20
|
+
Grayscale = 1,
|
|
21
|
+
Indexed = 2,
|
|
22
|
+
RGB = 3,
|
|
23
|
+
CMYK = 4,
|
|
24
|
+
Multichannel = 7,
|
|
25
|
+
Duotone = 8,
|
|
26
|
+
Lab = 9,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MODE_NAMES: Record<number, string> = {
|
|
30
|
+
[ColorMode.Bitmap]: "bitmap",
|
|
31
|
+
[ColorMode.CMYK]: "CMYK",
|
|
32
|
+
[ColorMode.Multichannel]: "multichannel",
|
|
33
|
+
[ColorMode.Lab]: "Lab",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when these bytes are a PSD/PSB, by magic rather than by file extension. */
|
|
37
|
+
export function isPsd(buffer: ArrayBuffer): boolean {
|
|
38
|
+
return buffer.byteLength >= 26 && new DataView(buffer).getUint32(0, false) === MAGIC
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PackBits, one row at a time.
|
|
43
|
+
*
|
|
44
|
+
* `end` bounds the row rather than the buffer: the row-length table says how many
|
|
45
|
+
* compressed bytes this row occupies, and trusting the control bytes past that
|
|
46
|
+
* would let one malformed row eat the next one's data.
|
|
47
|
+
*/
|
|
48
|
+
function unpackBits(src: Uint8Array, start: number, end: number, dst: Uint8Array, at: number, limit: number): number {
|
|
49
|
+
let i = start
|
|
50
|
+
let o = at
|
|
51
|
+
while (i < end && o < limit) {
|
|
52
|
+
const n = (src[i++] << 24) >> 24 // to signed
|
|
53
|
+
if (n >= 0) {
|
|
54
|
+
const count = Math.min(n + 1, limit - o, end - i)
|
|
55
|
+
for (let k = 0; k < count; k++) dst[o++] = src[i++]
|
|
56
|
+
} else if (n !== -128) {
|
|
57
|
+
// -128 is a no-op by the spec, not a run of 129.
|
|
58
|
+
const count = Math.min(1 - n, limit - o)
|
|
59
|
+
const b = src[i++]
|
|
60
|
+
for (let k = 0; k < count; k++) dst[o++] = b
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return o
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decode a PSD's composite to RGBA8.
|
|
68
|
+
*
|
|
69
|
+
* RGB, grayscale, indexed and duotone at 8 or 16 bits per channel, raw or RLE —
|
|
70
|
+
* which is every texture that has actually turned up. CMYK and Lab throw: they
|
|
71
|
+
* need a colour conversion that would be guesswork without a profile, and a
|
|
72
|
+
* silently wrong-coloured texture is worse than a missing one.
|
|
73
|
+
*/
|
|
74
|
+
export function decodePsd(buffer: ArrayBuffer): DecodedImage {
|
|
75
|
+
const v = new DataView(buffer)
|
|
76
|
+
if (buffer.byteLength < 26 || v.getUint32(0, false) !== MAGIC) throw new Error("not a PSD")
|
|
77
|
+
|
|
78
|
+
const version = v.getUint16(4, false) // 1 = PSD, 2 = PSB
|
|
79
|
+
if (version !== 1 && version !== 2) throw new Error(`PSD unsupported version ${version}`)
|
|
80
|
+
const channels = v.getUint16(12, false)
|
|
81
|
+
const height = v.getUint32(14, false)
|
|
82
|
+
const width = v.getUint32(18, false)
|
|
83
|
+
const depth = v.getUint16(22, false)
|
|
84
|
+
const mode = v.getUint16(24, false)
|
|
85
|
+
|
|
86
|
+
if (width <= 0 || height <= 0) throw new Error(`PSD bad dimensions ${width}x${height}`)
|
|
87
|
+
if (depth !== 8 && depth !== 16) throw new Error(`PSD unsupported bit depth ${depth}`)
|
|
88
|
+
if (mode in MODE_NAMES) throw new Error(`PSD unsupported colour mode: ${MODE_NAMES[mode]}`)
|
|
89
|
+
|
|
90
|
+
// Three variable-length sections stand between the header and the pixels. Only
|
|
91
|
+
// the indexed palette is worth reading; the rest is skipped by its length.
|
|
92
|
+
let p = 26
|
|
93
|
+
const colorDataLen = v.getUint32(p, false)
|
|
94
|
+
const colorData = p + 4
|
|
95
|
+
p = colorData + colorDataLen
|
|
96
|
+
p += 4 + v.getUint32(p, false) // image resources
|
|
97
|
+
// PSB states this length in 8 bytes. Only the low word can matter — the high
|
|
98
|
+
// one would mean a layer section larger than 4GB.
|
|
99
|
+
if (version === 2) {
|
|
100
|
+
p += 8 + v.getUint32(p + 4, false)
|
|
101
|
+
} else {
|
|
102
|
+
p += 4 + v.getUint32(p, false)
|
|
103
|
+
}
|
|
104
|
+
if (p + 2 > buffer.byteLength) throw new Error("PSD truncated before the composite")
|
|
105
|
+
|
|
106
|
+
const compression = v.getUint16(p, false)
|
|
107
|
+
p += 2
|
|
108
|
+
|
|
109
|
+
// Only the channels that carry colour, plus one alpha. A PSD can hold spot and
|
|
110
|
+
// mask channels past those; they are stored after, so ignoring them is a matter
|
|
111
|
+
// of not reading that far.
|
|
112
|
+
const colourChannels = mode === ColorMode.RGB ? 3 : 1
|
|
113
|
+
const hasAlpha = channels > colourChannels
|
|
114
|
+
const used = colourChannels + (hasAlpha ? 1 : 0)
|
|
115
|
+
if (channels < colourChannels) throw new Error(`PSD has ${channels} channel(s), expected ${colourChannels}`)
|
|
116
|
+
|
|
117
|
+
const bytesPerSample = depth === 16 ? 2 : 1
|
|
118
|
+
const planeSamples = width * height
|
|
119
|
+
const planeBytes = planeSamples * bytesPerSample
|
|
120
|
+
const planes = new Uint8Array(used * planeBytes)
|
|
121
|
+
|
|
122
|
+
if (compression === 0) {
|
|
123
|
+
const need = used * planeBytes
|
|
124
|
+
if (p + need > buffer.byteLength) throw new Error("PSD truncated composite")
|
|
125
|
+
planes.set(new Uint8Array(buffer, p, need))
|
|
126
|
+
} else if (compression === 1) {
|
|
127
|
+
// A table of per-row compressed lengths for EVERY channel comes first,
|
|
128
|
+
// including the channels being skipped — so the table is read in full even
|
|
129
|
+
// though only the first `used` channels' rows are decoded.
|
|
130
|
+
const countBytes = version === 2 ? 4 : 2
|
|
131
|
+
const tableBytes = channels * height * countBytes
|
|
132
|
+
if (p + tableBytes > buffer.byteLength) throw new Error("PSD truncated row table")
|
|
133
|
+
const rowLengths = new Uint32Array(channels * height)
|
|
134
|
+
for (let i = 0; i < rowLengths.length; i++) {
|
|
135
|
+
rowLengths[i] = countBytes === 2 ? v.getUint16(p + i * 2, false) : v.getUint32(p + i * 4, false)
|
|
136
|
+
}
|
|
137
|
+
const src = new Uint8Array(buffer)
|
|
138
|
+
let at = p + tableBytes
|
|
139
|
+
for (let c = 0; c < channels; c++) {
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
const len = rowLengths[c * height + y]
|
|
142
|
+
if (at + len > buffer.byteLength) throw new Error("PSD truncated composite")
|
|
143
|
+
if (c < used) {
|
|
144
|
+
const rowStart = c * planeBytes + y * width * bytesPerSample
|
|
145
|
+
unpackBits(src, at, at + len, planes, rowStart, rowStart + width * bytesPerSample)
|
|
146
|
+
}
|
|
147
|
+
at += len
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// 2 and 3 are the Zip codes. They appear on layer data, effectively never on
|
|
152
|
+
// the composite, and inflating would mean shipping a decompressor.
|
|
153
|
+
throw new Error(`PSD unsupported compression ${compression}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 16-bit samples are big-endian; the high byte is the 8-bit value.
|
|
157
|
+
const sample = (plane: number, i: number): number =>
|
|
158
|
+
depth === 16 ? planes[plane * planeBytes + i * 2] : planes[plane * planeBytes + i]
|
|
159
|
+
|
|
160
|
+
const rgba = new Uint8Array(planeSamples * 4)
|
|
161
|
+
|
|
162
|
+
if (mode === ColorMode.Indexed) {
|
|
163
|
+
// The palette is the colour mode data: 256 reds, then 256 greens, then blues.
|
|
164
|
+
if (colorDataLen < 768) throw new Error("PSD indexed image has no palette")
|
|
165
|
+
const pal = new Uint8Array(buffer, colorData, 768)
|
|
166
|
+
for (let i = 0; i < planeSamples; i++) {
|
|
167
|
+
const idx = sample(0, i)
|
|
168
|
+
rgba[i * 4] = pal[idx]
|
|
169
|
+
rgba[i * 4 + 1] = pal[256 + idx]
|
|
170
|
+
rgba[i * 4 + 2] = pal[512 + idx]
|
|
171
|
+
rgba[i * 4 + 3] = hasAlpha ? sample(1, i) : 255
|
|
172
|
+
}
|
|
173
|
+
return { rgba, width, height }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < planeSamples; i++) {
|
|
177
|
+
if (mode === ColorMode.RGB) {
|
|
178
|
+
rgba[i * 4] = sample(0, i)
|
|
179
|
+
rgba[i * 4 + 1] = sample(1, i)
|
|
180
|
+
rgba[i * 4 + 2] = sample(2, i)
|
|
181
|
+
} else {
|
|
182
|
+
// Grayscale and duotone: one plane across all three. Duotone's ink colours
|
|
183
|
+
// live in the colour mode data, and the spec's own advice is to treat the
|
|
184
|
+
// data as grayscale — which is what every other reader does.
|
|
185
|
+
const g = sample(0, i)
|
|
186
|
+
rgba[i * 4] = g
|
|
187
|
+
rgba[i * 4 + 1] = g
|
|
188
|
+
rgba[i * 4 + 2] = g
|
|
189
|
+
}
|
|
190
|
+
rgba[i * 4 + 3] = hasAlpha ? sample(colourChannels, i) : 255
|
|
191
|
+
}
|
|
192
|
+
return { rgba, width, height }
|
|
193
|
+
}
|
|
@@ -60,7 +60,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
60
60
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
61
61
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
62
62
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
63
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
63
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
64
64
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
65
65
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
66
66
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -208,6 +208,24 @@ fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
|
208
208
|
/** The camera's world position. */
|
|
209
209
|
fn bgCameraPos() -> vec3f { return viewU[10].xyz; }
|
|
210
210
|
|
|
211
|
+
/** How many characters are in the scene, up to four. */
|
|
212
|
+
fn bgSubjectCount() -> i32 { return i32(viewU[10].w); }
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Where a character is standing, in world space.
|
|
216
|
+
*
|
|
217
|
+
* An effect that wants to RESPOND to the cast — ripples under the feet, a glow
|
|
218
|
+
* that follows someone, dust kicked up where they are — needs to know where
|
|
219
|
+
* they are, and the ray and the depth cannot tell it: they describe the pixel,
|
|
220
|
+
* not the scene. This is the model's root, which for a PMX is between the feet
|
|
221
|
+
* on the floor, so it is already the contact point a ripple wants.
|
|
222
|
+
*
|
|
223
|
+
* Clamped rather than bounds-checked: an effect looping past the count reads the
|
|
224
|
+
* last subject instead of sampling whatever follows the array, which is a wrong
|
|
225
|
+
* ripple rather than an undefined one.
|
|
226
|
+
*/
|
|
227
|
+
fn bgSubjectPos(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
|
|
228
|
+
|
|
211
229
|
/** Where in the WORLD the scene drew this pixel — the depth handed to
|
|
212
230
|
* foreground() turned into a place. Without it an effect can only think in
|
|
213
231
|
* distances from the lens, which is no use to anything that belongs somewhere:
|