reze-engine 0.25.2 → 0.27.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/README.md +6 -4
- package/dist/animation.d.ts +9 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +4 -0
- package/dist/engine.d.ts +36 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +86 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +12 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +59 -3
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +28 -1
- package/dist/vmd-loader.d.ts +22 -0
- package/dist/vmd-loader.d.ts.map +1 -1
- package/dist/vmd-loader.js +58 -0
- package/dist/vmd-writer.d.ts.map +1 -1
- package/dist/vmd-writer.js +45 -1
- package/package.json +42 -42
- package/src/animation.ts +14 -0
- package/src/engine.ts +105 -8
- package/src/index.ts +4 -1
- package/src/model.ts +54 -3
- package/src/shaders/passes/composite.ts +28 -1
- package/src/vmd-loader.ts +64 -0
- package/src/vmd-writer.ts +43 -1
|
@@ -39,7 +39,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
39
39
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
40
40
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
41
41
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
42
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
42
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 10>;
|
|
43
43
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
44
44
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
45
45
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -62,6 +62,8 @@ override APPLY_GAMMA: bool = true;
|
|
|
62
62
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
63
63
|
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
64
64
|
// viewU[6] = (time seconds, effect on/off, canvas width, canvas height).
|
|
65
|
+
// viewU[7] = (grade offset.rgb, contrast); viewU[8] = (grade power.rgb, saturation);
|
|
66
|
+
// viewU[9] = (grade slope.rgb, grade on/off) — see grade() below.
|
|
65
67
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
66
68
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
67
69
|
|
|
@@ -81,6 +83,24 @@ fn filmic(x: f32) -> f32 {
|
|
|
81
83
|
|
|
82
84
|
/** Canvas size in pixels — for user background effects (aspect correction). */
|
|
83
85
|
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
86
|
+
|
|
87
|
+
/** Color grading, applied to the tonemapped SCENE (not the background — see the
|
|
88
|
+
* call site). The core is ASC CDL, the film-industry interchange standard:
|
|
89
|
+
*
|
|
90
|
+
* out = (in · slope + offset) ^ power then saturation (SOP → SAT)
|
|
91
|
+
*
|
|
92
|
+
* Using the real standard rather than invented controls means a look authored
|
|
93
|
+
* here maps onto any grading tool. slope/offset/power are derived on the CPU
|
|
94
|
+
* from the UI's shadow/midtone/highlight colors (see setColorGrading), so the
|
|
95
|
+
* per-pixel cost is one mul-add, one pow, one lerp. */
|
|
96
|
+
fn grade(c: vec3f) -> vec3f {
|
|
97
|
+
var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);
|
|
98
|
+
// Contrast pivots on 0.5 — display-referred midpoint, since we grade post-Filmic.
|
|
99
|
+
x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);
|
|
100
|
+
// Rec.709 luma, matching the ASC SAT node.
|
|
101
|
+
let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));
|
|
102
|
+
return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));
|
|
103
|
+
}
|
|
84
104
|
`
|
|
85
105
|
|
|
86
106
|
const COMPOSITE_BODY = /* wgsl */ `
|
|
@@ -107,6 +127,13 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
107
127
|
let exposed = combined * exp2(viewU[0].x);
|
|
108
128
|
let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
|
|
109
129
|
var disp = max(tm, vec3f(0.0));
|
|
130
|
+
// Grade the SCENE only, before the display gamma. Deliberately not applied to
|
|
131
|
+
// the background: it keeps a picked background color exactly as picked, and —
|
|
132
|
+
// load-bearing — leaves green-screen mode's key color unshifted so chroma
|
|
133
|
+
// keying still works. Skipped entirely when the grade is neutral.
|
|
134
|
+
if (viewU[9].w > 0.5) {
|
|
135
|
+
disp = grade(disp);
|
|
136
|
+
}
|
|
110
137
|
if (APPLY_GAMMA) {
|
|
111
138
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
112
139
|
}
|
package/src/vmd-loader.ts
CHANGED
|
@@ -32,6 +32,14 @@ export interface CameraKeyframe {
|
|
|
32
32
|
interpolation: Uint8Array // 24 bytes
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** A VMD "IK/display" record: one moment at which chains are switched. */
|
|
36
|
+
export interface IkFrame {
|
|
37
|
+
frame: number
|
|
38
|
+
/** Model visibility rides in the same record. Parsed, unused by playback. */
|
|
39
|
+
visible: boolean
|
|
40
|
+
states: { boneName: string; enabled: boolean }[]
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
export class VMDLoader {
|
|
36
44
|
private view: DataView
|
|
37
45
|
private offset = 0
|
|
@@ -100,6 +108,48 @@ export class VMDLoader {
|
|
|
100
108
|
return frames
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
static loadIkFromBuffer(buffer: ArrayBuffer): IkFrame[] {
|
|
112
|
+
return new VMDLoader(buffer).parseIk()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The IK/display block, at the very end of the file.
|
|
117
|
+
*
|
|
118
|
+
* MMD writes it after camera, light and self-shadow, and a motion-only VMD
|
|
119
|
+
* often stops before any of them — so every block is bounds-checked and a
|
|
120
|
+
* short file simply reports no IK data rather than throwing. Record sizes:
|
|
121
|
+
* bone 111 B, morph 23 B, camera 61 B, light 28 B, self-shadow 9 B.
|
|
122
|
+
*/
|
|
123
|
+
private parseIk(): IkFrame[] {
|
|
124
|
+
this.offset = 0
|
|
125
|
+
const header = this.getString(30)
|
|
126
|
+
if (!header.startsWith("Vocaloid Motion Data")) throw new Error("Invalid VMD file header")
|
|
127
|
+
this.skip(20)
|
|
128
|
+
const end = this.view.buffer.byteLength
|
|
129
|
+
const seek = (size: number): boolean => {
|
|
130
|
+
if (this.offset + 4 > end) return false
|
|
131
|
+
this.skip(this.getUint32() * size)
|
|
132
|
+
return this.offset <= end
|
|
133
|
+
}
|
|
134
|
+
if (!seek(111) || !seek(23) || !seek(61) || !seek(28) || !seek(9)) return []
|
|
135
|
+
if (this.offset + 4 > end) return []
|
|
136
|
+
|
|
137
|
+
const count = this.getUint32()
|
|
138
|
+
const frames: IkFrame[] = []
|
|
139
|
+
for (let i = 0; i < count && this.offset + 9 <= end; i++) {
|
|
140
|
+
const frame = this.getUint32()
|
|
141
|
+
const visible = this.getUint8() !== 0
|
|
142
|
+
const ikCount = this.getUint32()
|
|
143
|
+
const states: { boneName: string; enabled: boolean }[] = []
|
|
144
|
+
for (let j = 0; j < ikCount && this.offset + 21 <= end; j++) {
|
|
145
|
+
states.push({ boneName: this.getShiftJisName(20), enabled: this.getUint8() !== 0 })
|
|
146
|
+
}
|
|
147
|
+
frames.push({ frame, visible, states })
|
|
148
|
+
}
|
|
149
|
+
frames.sort((a, b) => a.frame - b.frame)
|
|
150
|
+
return frames
|
|
151
|
+
}
|
|
152
|
+
|
|
103
153
|
private parse(): VMDKeyFrame[] {
|
|
104
154
|
// Read header (30 bytes)
|
|
105
155
|
const header = this.getString(30)
|
|
@@ -315,6 +365,20 @@ export class VMDLoader {
|
|
|
315
365
|
return v
|
|
316
366
|
}
|
|
317
367
|
|
|
368
|
+
/** A fixed-width, NUL-padded Shift-JIS name — the form every VMD name takes. */
|
|
369
|
+
private getShiftJisName(len: number): string {
|
|
370
|
+
const bytes = new Uint8Array(this.view.buffer, this.offset, len)
|
|
371
|
+
this.offset += len
|
|
372
|
+
let end = bytes.indexOf(0)
|
|
373
|
+
if (end < 0) end = len
|
|
374
|
+
const slice = bytes.slice(0, end)
|
|
375
|
+
try {
|
|
376
|
+
return this.decoder.decode(slice)
|
|
377
|
+
} catch {
|
|
378
|
+
return String.fromCharCode(...slice)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
318
382
|
private getString(len: number): string {
|
|
319
383
|
const bytes = new Uint8Array(this.view.buffer, this.offset, len)
|
|
320
384
|
this.offset += len
|
package/src/vmd-writer.ts
CHANGED
|
@@ -7,6 +7,8 @@ const BONE_NAME_SIZE = 15
|
|
|
7
7
|
const MORPH_NAME_SIZE = 15
|
|
8
8
|
const BONE_FRAME_SIZE = BONE_NAME_SIZE + 4 + 12 + 16 + 64 // 111 bytes
|
|
9
9
|
const MORPH_FRAME_SIZE = MORPH_NAME_SIZE + 4 + 4 // 23 bytes
|
|
10
|
+
/** IK bone names get 20 bytes in the IK block, not the 15 bones get elsewhere. */
|
|
11
|
+
const IK_NAME_SIZE = 20
|
|
10
12
|
|
|
11
13
|
// Build a Unicode-to-Shift-JIS lookup by inverting the TextDecoder mapping.
|
|
12
14
|
let shiftJISTable: Map<string, number[]> | null = null
|
|
@@ -56,11 +58,30 @@ export class VMDWriter {
|
|
|
56
58
|
totalMorphFrames += frames.length
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
// IK state is stored per MOMENT, not per bone: one record lists every chain
|
|
62
|
+
// and its state at that frame. So the tracks are transposed back into the
|
|
63
|
+
// frames they were flattened from.
|
|
64
|
+
const ikByFrame = new Map<number, { boneName: string; enabled: boolean }[]>()
|
|
65
|
+
for (const [boneName, keys] of clip.ikTracks ?? []) {
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const at = ikByFrame.get(key.frame)
|
|
68
|
+
if (at) at.push({ boneName, enabled: key.enabled })
|
|
69
|
+
else ikByFrame.set(key.frame, [{ boneName, enabled: key.enabled }])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const ikFrames = [...ikByFrame.entries()].sort((a, b) => a[0] - b[0])
|
|
73
|
+
let ikSize = 0
|
|
74
|
+
for (const [, states] of ikFrames) ikSize += 4 + 1 + 4 + states.length * (IK_NAME_SIZE + 1)
|
|
75
|
+
|
|
59
76
|
const size =
|
|
60
77
|
HEADER_SIZE +
|
|
61
78
|
MODEL_NAME_SIZE +
|
|
62
79
|
4 + totalBoneFrames * BONE_FRAME_SIZE +
|
|
63
|
-
4 + totalMorphFrames * MORPH_FRAME_SIZE
|
|
80
|
+
4 + totalMorphFrames * MORPH_FRAME_SIZE +
|
|
81
|
+
// Camera, light and self-shadow counts, then the IK block. Written only
|
|
82
|
+
// when there is IK state to carry: a plain motion stays byte-identical to
|
|
83
|
+
// what this writer produced before.
|
|
84
|
+
(ikFrames.length > 0 ? 4 * 4 + ikSize : 0)
|
|
64
85
|
|
|
65
86
|
const buffer = new ArrayBuffer(size)
|
|
66
87
|
const view = new DataView(buffer)
|
|
@@ -124,6 +145,27 @@ export class VMDWriter {
|
|
|
124
145
|
}
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
if (ikFrames.length > 0) {
|
|
149
|
+
// Empty camera, light and self-shadow blocks — a reader walking the file
|
|
150
|
+
// in order has to pass through them to reach the IK block.
|
|
151
|
+
for (let i = 0; i < 3; i++, offset += 4) view.setUint32(offset, 0, true)
|
|
152
|
+
view.setUint32(offset, ikFrames.length, true)
|
|
153
|
+
offset += 4
|
|
154
|
+
for (const [frame, states] of ikFrames) {
|
|
155
|
+
view.setUint32(offset, frame, true)
|
|
156
|
+
offset += 4
|
|
157
|
+
view.setUint8(offset, 1) // model visible
|
|
158
|
+
offset += 1
|
|
159
|
+
view.setUint32(offset, states.length, true)
|
|
160
|
+
offset += 4
|
|
161
|
+
for (const state of states) {
|
|
162
|
+
offset = writeFixedShiftJIS(buffer, offset, state.boneName, IK_NAME_SIZE)
|
|
163
|
+
view.setUint8(offset, state.enabled ? 1 : 0)
|
|
164
|
+
offset += 1
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
127
169
|
return buffer
|
|
128
170
|
}
|
|
129
171
|
}
|