reze-engine 0.13.5 → 0.14.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 +104 -13
- package/dist/camera.d.ts +2 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +17 -0
- package/dist/engine.d.ts +71 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +730 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +6 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +36 -1
- package/dist/shaders/materials/body.d.ts +1 -1
- package/dist/shaders/materials/cloth_rough.d.ts +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.js +2 -2
- package/dist/shaders/materials/default.d.ts +1 -1
- package/dist/shaders/materials/eye.d.ts +1 -1
- package/dist/shaders/materials/face.d.ts +1 -1
- package/dist/shaders/materials/hair.d.ts +1 -1
- package/dist/shaders/materials/metal.d.ts +1 -1
- package/dist/shaders/materials/stockings.d.ts +1 -1
- package/dist/shaders/passes/gizmo.d.ts +2 -0
- package/dist/shaders/passes/gizmo.d.ts.map +1 -0
- package/dist/shaders/passes/gizmo.js +75 -0
- package/dist/shaders/passes/pick.d.ts +1 -1
- package/dist/shaders/passes/pick.d.ts.map +1 -1
- package/dist/shaders/passes/pick.js +29 -5
- package/dist/shaders/passes/selection.d.ts +3 -0
- package/dist/shaders/passes/selection.d.ts.map +1 -0
- package/dist/shaders/passes/selection.js +65 -0
- package/package.json +1 -1
- package/src/camera.ts +14 -0
- package/src/engine.ts +831 -10
- package/src/index.ts +3 -0
- package/src/model.ts +38 -1
- package/src/shaders/materials/common.ts +2 -2
- package/src/shaders/passes/gizmo.ts +76 -0
- package/src/shaders/passes/pick.ts +29 -5
- package/src/shaders/passes/selection.ts +67 -0
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ export {
|
|
|
9
9
|
type LoadModelFromFilesOptions,
|
|
10
10
|
type MaterialPreset,
|
|
11
11
|
type MaterialPresetMap,
|
|
12
|
+
type GizmoDragEvent,
|
|
13
|
+
type GizmoDragCallback,
|
|
14
|
+
type GizmoDragKind,
|
|
12
15
|
} from "./engine"
|
|
13
16
|
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
|
|
14
17
|
export { Model } from "./model"
|
package/src/model.ts
CHANGED
|
@@ -491,6 +491,41 @@ export class Model {
|
|
|
491
491
|
return this.skeleton
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
// Direct bone local-transform accessors (used by interactive gizmo drag).
|
|
495
|
+
// Readers return the live runtime state — callers that want a snapshot for
|
|
496
|
+
// later comparison should `.clone()` the returned Quat / copy the Vec3.
|
|
497
|
+
getBoneLocalRotation(boneIndex: number): Quat {
|
|
498
|
+
return this.runtimeSkeleton.localRotations[boneIndex]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getBoneLocalTranslation(boneIndex: number): Vec3 {
|
|
502
|
+
return this.runtimeSkeleton.localTranslations[boneIndex]
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Raw absolute-local translation write. NOT equivalent to
|
|
506
|
+
// `moveBones({ name: v }, 0)` — moveBones treats the input as VMD-relative
|
|
507
|
+
// (offset from bind pose) and runs convertVMDTranslationToLocal() over it.
|
|
508
|
+
// Use this when you already have the final local translation (e.g. the
|
|
509
|
+
// gizmo's computed target). For rotation, just use rotateBones(..., 0).
|
|
510
|
+
setBoneLocalTranslation(boneIndex: number, v: Vec3): void {
|
|
511
|
+
const t = this.runtimeSkeleton.localTranslations[boneIndex]
|
|
512
|
+
t.x = v.x; t.y = v.y; t.z = v.z
|
|
513
|
+
this.tweenState.transActive[boneIndex] = 0
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// When true, update() skips applyPoseFromClip, so whatever was last written to
|
|
517
|
+
// localRotations / localTranslations persists across frames. Used by gizmo drag
|
|
518
|
+
// and other direct-manipulation flows to prevent the currently-shown clip from
|
|
519
|
+
// overwriting manual edits each frame. Auto-cleared on play()/seek() so the user
|
|
520
|
+
// gets back to normal playback without having to manage this flag explicitly.
|
|
521
|
+
private clipApplySuspended = false
|
|
522
|
+
setClipApplySuspended(suspended: boolean): void {
|
|
523
|
+
this.clipApplySuspended = suspended
|
|
524
|
+
}
|
|
525
|
+
isClipApplySuspended(): boolean {
|
|
526
|
+
return this.clipApplySuspended
|
|
527
|
+
}
|
|
528
|
+
|
|
494
529
|
// World bone origin (world matrix col3); unknown name → null
|
|
495
530
|
getBoneWorldPosition(boneName: string): Vec3 | null {
|
|
496
531
|
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
|
@@ -981,6 +1016,7 @@ export class Model {
|
|
|
981
1016
|
play(name: string): boolean
|
|
982
1017
|
play(name: string, options?: AnimationPlayOptions): boolean
|
|
983
1018
|
play(name?: string, options?: AnimationPlayOptions): void | boolean {
|
|
1019
|
+
this.clipApplySuspended = false
|
|
984
1020
|
if (name === undefined) {
|
|
985
1021
|
this.animationState.play()
|
|
986
1022
|
return
|
|
@@ -1021,6 +1057,7 @@ export class Model {
|
|
|
1021
1057
|
|
|
1022
1058
|
// Seek by absolute timeline seconds, not frame index.
|
|
1023
1059
|
seek(seconds: number): void {
|
|
1060
|
+
this.clipApplySuspended = false
|
|
1024
1061
|
this.animationState.seek(seconds)
|
|
1025
1062
|
}
|
|
1026
1063
|
|
|
@@ -1166,7 +1203,7 @@ export class Model {
|
|
|
1166
1203
|
this.animationState.update(deltaTime)
|
|
1167
1204
|
const clip = this.animationState.getCurrentClip()
|
|
1168
1205
|
const frame = this.animationState.getCurrentFrame()
|
|
1169
|
-
if (clip !== null) {
|
|
1206
|
+
if (clip !== null && !this.clipApplySuspended) {
|
|
1170
1207
|
this.applyPoseFromClip(clip, frame)
|
|
1171
1208
|
}
|
|
1172
1209
|
|
|
@@ -69,7 +69,7 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
69
69
|
`;
|
|
70
70
|
|
|
71
71
|
// ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
|
|
72
|
-
//
|
|
72
|
+
// 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
|
|
73
73
|
// doesn't unroll nested shadow loops reliably, and the early out on back-facing
|
|
74
74
|
// fragments saves 9 texture taps per skipped pixel.
|
|
75
75
|
|
|
@@ -82,7 +82,7 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
82
82
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
83
83
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
84
84
|
let cmpZ = ndc.z - 0.001;
|
|
85
|
-
let ts = 1.0 /
|
|
85
|
+
let ts = 1.0 / 2048.0;
|
|
86
86
|
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
87
87
|
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
88
88
|
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Transform gizmo — 3 translation axes + 3 rotation rings, drawn as thick
|
|
2
|
+
// ribbons. Per-segment perpendicular (no miter) → the "tick" look at each ring
|
|
3
|
+
// vertex is intentional.
|
|
4
|
+
//
|
|
5
|
+
// axisT per-vertex: 0 at the bone origin, 1 at the axis tip. For ring verts it
|
|
6
|
+
// is set to -1 as a "not an axis" flag. The FS uses axisT to:
|
|
7
|
+
// • fade + dash the inside-ring portion of each axis (not hittable, so the
|
|
8
|
+
// user can tell to only grab the outer stub for translation).
|
|
9
|
+
// • leave rings and the outer axis stub fully solid with only the edge-to-
|
|
10
|
+
// center alpha falloff.
|
|
11
|
+
|
|
12
|
+
export const GIZMO_SHADER_WGSL = /* wgsl */ `
|
|
13
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
14
|
+
struct Transform {
|
|
15
|
+
model: mat4x4f,
|
|
16
|
+
viewport: vec2f,
|
|
17
|
+
thicknessPx: f32,
|
|
18
|
+
_pad: f32,
|
|
19
|
+
};
|
|
20
|
+
struct Color { rgba: vec4f };
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
23
|
+
@group(0) @binding(1) var<uniform> transform: Transform;
|
|
24
|
+
@group(1) @binding(0) var<uniform> col: Color;
|
|
25
|
+
|
|
26
|
+
struct VSOut {
|
|
27
|
+
@builtin(position) pos: vec4f,
|
|
28
|
+
@location(0) side: f32,
|
|
29
|
+
@location(1) axisT: f32,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@vertex fn vs(
|
|
33
|
+
@location(0) position: vec3f,
|
|
34
|
+
@location(1) segDir: vec3f,
|
|
35
|
+
@location(2) side: f32,
|
|
36
|
+
@location(3) axisT: f32,
|
|
37
|
+
) -> VSOut {
|
|
38
|
+
let vp = camera.projection * camera.view * transform.model;
|
|
39
|
+
let c0 = vp * vec4f(position, 1.0);
|
|
40
|
+
let c1 = vp * vec4f(position + segDir, 1.0);
|
|
41
|
+
let w0 = max(abs(c0.w), 1e-6);
|
|
42
|
+
let w1 = max(abs(c1.w), 1e-6);
|
|
43
|
+
let s0 = (c0.xy / w0) * 0.5 * transform.viewport;
|
|
44
|
+
let s1 = (c1.xy / w1) * 0.5 * transform.viewport;
|
|
45
|
+
let tangent = normalize(s1 - s0);
|
|
46
|
+
let normalPx = vec2f(-tangent.y, tangent.x);
|
|
47
|
+
// Axes render thinner than rings. axisT < 0 → ring (full thickness); else → axis (reduced).
|
|
48
|
+
let thicknessMul = select(0.60, 1.0, axisT < 0.0);
|
|
49
|
+
let offsetPx = normalPx * side * transform.thicknessPx * 0.5 * thicknessMul;
|
|
50
|
+
let offsetClip = (offsetPx / (0.5 * transform.viewport)) * c0.w;
|
|
51
|
+
var out: VSOut;
|
|
52
|
+
out.pos = vec4f(c0.xy + offsetClip, c0.z, c0.w);
|
|
53
|
+
out.side = side;
|
|
54
|
+
out.axisT = axisT;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
59
|
+
// Center-bright, edges fade. abs(side) is 0 at ribbon center, 1 at ribbon edges.
|
|
60
|
+
let edge = 1.0 - smoothstep(0.55, 1.0, abs(in.side));
|
|
61
|
+
var alpha = col.rgba.a * edge;
|
|
62
|
+
|
|
63
|
+
// Dash + dim the inside-ring portion of axes. axisT is -1 for ring fragments,
|
|
64
|
+
// 0..1 along axes (0 at bone center, 1 at axis tip). Boundary 0.63 ≈ where the
|
|
65
|
+
// hit zone starts (ring radius 0.8 + 0.05 margin, over axis length 1.35).
|
|
66
|
+
let isInsideRingAxis = in.axisT >= 0.0 && in.axisT < 0.63;
|
|
67
|
+
if (isInsideRingAxis) {
|
|
68
|
+
// ~8 dash cycles inside the ring — readable without feeling busy.
|
|
69
|
+
let phase = fract(in.axisT * 12.0);
|
|
70
|
+
if (phase > 0.55) { discard; }
|
|
71
|
+
alpha = alpha * 0.40;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return vec4f(col.rgba.rgb, alpha);
|
|
75
|
+
}
|
|
76
|
+
`
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// GPU picking pass: encodes (modelIndex, materialIndex) as
|
|
1
|
+
// GPU picking pass: encodes (modelIndex, materialIndex, dominantBoneIndex) as RGB8
|
|
2
|
+
// into a 1×1 readback target. Dominant bone = the joint with the largest skinning
|
|
3
|
+
// weight for the provoking vertex of the triangle (flat-interpolated to fragments).
|
|
4
|
+
// 8-bit bone range (0..255) covers standard MMD skeletons (~100–200 bones).
|
|
2
5
|
|
|
3
6
|
export const PICK_SHADER_WGSL = /* wgsl */ `
|
|
4
7
|
struct CameraUniforms {
|
|
@@ -18,23 +21,44 @@ struct PickId {
|
|
|
18
21
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
19
22
|
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
20
23
|
|
|
24
|
+
struct VSOut {
|
|
25
|
+
@builtin(position) pos: vec4f,
|
|
26
|
+
@interpolate(flat) @location(0) boneId: u32,
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
@vertex fn vs(
|
|
22
30
|
@location(0) position: vec3f,
|
|
23
31
|
@location(1) normal: vec3f,
|
|
24
32
|
@location(2) uv: vec2f,
|
|
25
33
|
@location(3) joints0: vec4<u32>,
|
|
26
34
|
@location(4) weights0: vec4<f32>
|
|
27
|
-
) ->
|
|
35
|
+
) -> VSOut {
|
|
28
36
|
let pos4 = vec4f(position, 1.0);
|
|
29
37
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
30
38
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
31
39
|
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
32
40
|
var sp = vec4f(0.0);
|
|
33
41
|
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
34
|
-
|
|
42
|
+
|
|
43
|
+
// Dominant joint for this vertex — index of max weight component.
|
|
44
|
+
var maxW: f32 = nw.x;
|
|
45
|
+
var idx: u32 = joints0.x;
|
|
46
|
+
if (nw.y > maxW) { maxW = nw.y; idx = joints0.y; }
|
|
47
|
+
if (nw.z > maxW) { maxW = nw.z; idx = joints0.z; }
|
|
48
|
+
if (nw.w > maxW) { maxW = nw.w; idx = joints0.w; }
|
|
49
|
+
|
|
50
|
+
var out: VSOut;
|
|
51
|
+
out.pos = camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
52
|
+
out.boneId = idx;
|
|
53
|
+
return out;
|
|
35
54
|
}
|
|
36
55
|
|
|
37
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
38
|
-
return vec4f(
|
|
56
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
57
|
+
return vec4f(
|
|
58
|
+
pickId.modelId / 255.0,
|
|
59
|
+
pickId.materialId / 255.0,
|
|
60
|
+
f32(in.boneId) / 255.0,
|
|
61
|
+
1.0
|
|
62
|
+
);
|
|
39
63
|
}
|
|
40
64
|
`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Selection overlay — two screen-space passes that together draw a uniform
|
|
2
|
+
// pixel-thick outline around the selected material.
|
|
3
|
+
//
|
|
4
|
+
// Pass 1 (mask): render only the selected material's triangles into an r8
|
|
5
|
+
// texture (depth-always). Fragment outputs 1.0. No per-material uniforms;
|
|
6
|
+
// reuses camera + skinMats from the outline/main bind group layouts.
|
|
7
|
+
//
|
|
8
|
+
// Pass 2 (edge): fullscreen pass over the swapchain. For each pixel, sample
|
|
9
|
+
// the mask at center + 8 neighbours in a ring of `thickness` pixels. Emit
|
|
10
|
+
// yellow where center is empty but any neighbour is filled. Result: uniform
|
|
11
|
+
// screen-space thickness, traces the complete material boundary (including
|
|
12
|
+
// through-occluder regions), independent of mesh geometry or camera angle.
|
|
13
|
+
|
|
14
|
+
export const SELECTION_MASK_SHADER_WGSL = /* wgsl */ `
|
|
15
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
16
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
17
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
18
|
+
|
|
19
|
+
@vertex fn vs(
|
|
20
|
+
@location(0) position: vec3f,
|
|
21
|
+
@location(3) joints0: vec4<u32>,
|
|
22
|
+
@location(4) weights0: vec4<f32>
|
|
23
|
+
) -> @builtin(position) vec4f {
|
|
24
|
+
let ws = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
25
|
+
let inv = select(1.0, 1.0 / ws, ws > 0.0001);
|
|
26
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * inv, ws > 0.0001);
|
|
27
|
+
var sp = vec4f(0.0);
|
|
28
|
+
for (var i = 0u; i < 4u; i++) {
|
|
29
|
+
sp += (skinMats[joints0[i]] * vec4f(position, 1.0)) * nw[i];
|
|
30
|
+
}
|
|
31
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.0, 0.0, 0.0); }
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
export const SELECTION_EDGE_SHADER_WGSL = /* wgsl */ `
|
|
38
|
+
@group(0) @binding(0) var maskTex: texture_2d<f32>;
|
|
39
|
+
@group(0) @binding(1) var maskSamp: sampler;
|
|
40
|
+
struct Params { thickness: f32, _pad0: f32, _pad1: f32, _pad2: f32 };
|
|
41
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
42
|
+
|
|
43
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
44
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
45
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
46
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
|
|
50
|
+
let dims = vec2f(textureDimensions(maskTex));
|
|
51
|
+
let uv = fragCoord.xy / dims;
|
|
52
|
+
let center = textureSample(maskTex, maskSamp, uv).r;
|
|
53
|
+
if (center > 0.5) { discard; }
|
|
54
|
+
let t = params.thickness / dims;
|
|
55
|
+
var m: f32 = 0.0;
|
|
56
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, 0.0)).r);
|
|
57
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, 0.0)).r);
|
|
58
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( 0.0, -t.y)).r);
|
|
59
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( 0.0, t.y)).r);
|
|
60
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, -t.y)).r);
|
|
61
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, -t.y)).r);
|
|
62
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, t.y)).r);
|
|
63
|
+
m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, t.y)).r);
|
|
64
|
+
if (m < 0.05) { discard; }
|
|
65
|
+
return vec4f(1.0, 1.0, 0.0, m);
|
|
66
|
+
}
|
|
67
|
+
`
|