reze-engine 0.8.3 → 0.8.4
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 +3 -3
- package/dist/engine.d.ts +22 -25
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +109 -325
- package/dist/model.d.ts +1 -10
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +3 -28
- package/package.json +1 -1
- package/src/engine.ts +129 -348
- package/src/model.ts +3 -35
package/src/engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
2
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
|
+
import { PmxLoader } from "./pmx-loader"
|
|
4
5
|
import { Physics } from "./physics"
|
|
5
6
|
|
|
6
7
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
|
|
@@ -14,7 +15,6 @@ export type EngineOptions = {
|
|
|
14
15
|
cameraTarget?: Vec3
|
|
15
16
|
cameraFov?: number
|
|
16
17
|
onRaycast?: RaycastCallback
|
|
17
|
-
multisampleCount?: 1 | 4
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
|
|
@@ -28,7 +28,6 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
|
|
|
28
28
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
29
29
|
cameraFov: Math.PI / 4,
|
|
30
30
|
onRaycast: undefined,
|
|
31
|
-
multisampleCount: 4,
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export interface EngineStats {
|
|
@@ -78,7 +77,7 @@ export class Engine {
|
|
|
78
77
|
|
|
79
78
|
public static getInstance(): Engine {
|
|
80
79
|
if (!Engine.instance) {
|
|
81
|
-
throw new Error("Engine not ready: create Engine, await init(), then load models via
|
|
80
|
+
throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().")
|
|
82
81
|
}
|
|
83
82
|
return Engine.instance
|
|
84
83
|
}
|
|
@@ -104,19 +103,18 @@ export class Engine {
|
|
|
104
103
|
private hairPipelineOverEyes!: GPURenderPipeline
|
|
105
104
|
private hairPipelineOverNonEyes!: GPURenderPipeline
|
|
106
105
|
private hairDepthPipeline!: GPURenderPipeline
|
|
107
|
-
// Ground
|
|
108
|
-
private
|
|
109
|
-
private
|
|
110
|
-
private reflectionPipeline!: GPURenderPipeline
|
|
106
|
+
// Ground (shadow only)
|
|
107
|
+
private groundShadowPipeline!: GPURenderPipeline
|
|
108
|
+
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
111
109
|
// Outline pipelines
|
|
112
110
|
private outlinePipeline!: GPURenderPipeline
|
|
113
111
|
private hairOutlinePipeline!: GPURenderPipeline
|
|
114
112
|
private mainBindGroupLayout!: GPUBindGroupLayout
|
|
115
113
|
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
116
114
|
private multisampleTexture!: GPUTexture
|
|
117
|
-
private
|
|
115
|
+
private static readonly MULTISAMPLE_COUNT = 4
|
|
118
116
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
119
|
-
//
|
|
117
|
+
// Post-alpha eye: eyes write stencil, hair-over-eyes reads it for see-through bangs (MMD-style).
|
|
120
118
|
private readonly STENCIL_EYE_VALUE = 1
|
|
121
119
|
|
|
122
120
|
// Ambient light settings
|
|
@@ -126,23 +124,15 @@ export class Engine {
|
|
|
126
124
|
// Rim light settings
|
|
127
125
|
private rimLightIntensity!: number
|
|
128
126
|
|
|
129
|
-
// Ground
|
|
127
|
+
// Ground properties (shadow only)
|
|
130
128
|
private groundVertexBuffer?: GPUBuffer
|
|
131
129
|
private groundIndexBuffer?: GPUBuffer
|
|
132
|
-
private
|
|
133
|
-
private groundReflectionResolveTexture?: GPUTexture // Resolve target for multisampled texture
|
|
134
|
-
private groundReflectionDepthTexture?: GPUTexture
|
|
135
|
-
private groundReflectionBindGroup?: GPUBindGroup
|
|
136
|
-
private groundMaterialUniformBuffer?: GPUBuffer
|
|
137
|
-
private groundHasReflections = false
|
|
138
|
-
private groundMode: "reflection" | "shadow" = "reflection"
|
|
130
|
+
private hasGround = false
|
|
139
131
|
private shadowMapTexture?: GPUTexture
|
|
140
132
|
private shadowMapDepthView?: GPUTextureView
|
|
141
133
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
142
134
|
private shadowLightVPBuffer!: GPUBuffer
|
|
143
135
|
private shadowLightVPMatrix = new Float32Array(16)
|
|
144
|
-
private groundShadowPipeline!: GPURenderPipeline
|
|
145
|
-
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
146
136
|
private groundShadowBindGroup?: GPUBindGroup
|
|
147
137
|
private shadowComparisonSampler!: GPUSampler
|
|
148
138
|
private groundShadowMaterialBuffer?: GPUBuffer
|
|
@@ -159,7 +149,16 @@ export class Engine {
|
|
|
159
149
|
private modelInstances = new Map<string, ModelInstance>()
|
|
160
150
|
private materialSampler!: GPUSampler
|
|
161
151
|
private textureCache = new Map<string, GPUTexture>()
|
|
162
|
-
private
|
|
152
|
+
private _nextDefaultModelId = 0
|
|
153
|
+
|
|
154
|
+
// IK and physics enabled at engine level (same for all models)
|
|
155
|
+
private ikEnabled = true
|
|
156
|
+
private physicsEnabled = true
|
|
157
|
+
|
|
158
|
+
// Camera target binding (Babylon/Three style: camera follows model)
|
|
159
|
+
private cameraTargetModel: Model | null = null
|
|
160
|
+
private cameraTargetBoneName = "全ての親"
|
|
161
|
+
private cameraTargetOffset: Vec3 = new Vec3(0, 0, 0)
|
|
163
162
|
|
|
164
163
|
private lastFpsUpdate = performance.now()
|
|
165
164
|
private framesSinceLastUpdate = 0
|
|
@@ -175,7 +174,6 @@ export class Engine {
|
|
|
175
174
|
|
|
176
175
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
177
176
|
this.canvas = canvas
|
|
178
|
-
this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount
|
|
179
177
|
if (options) {
|
|
180
178
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor!
|
|
181
179
|
this.directionalLightIntensity =
|
|
@@ -246,7 +244,7 @@ export class Engine {
|
|
|
246
244
|
: undefined,
|
|
247
245
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
248
246
|
depthStencil: config.depthStencil,
|
|
249
|
-
multisample: config.multisample ?? { count:
|
|
247
|
+
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
250
248
|
})
|
|
251
249
|
}
|
|
252
250
|
|
|
@@ -496,71 +494,6 @@ export class Engine {
|
|
|
496
494
|
},
|
|
497
495
|
})
|
|
498
496
|
|
|
499
|
-
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
500
|
-
label: "ground bind group layout",
|
|
501
|
-
entries: [
|
|
502
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
503
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
504
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
505
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
506
|
-
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
507
|
-
],
|
|
508
|
-
})
|
|
509
|
-
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
510
|
-
label: "ground pipeline layout",
|
|
511
|
-
bindGroupLayouts: [this.groundBindGroupLayout],
|
|
512
|
-
})
|
|
513
|
-
const groundShaderModule = this.device.createShaderModule({
|
|
514
|
-
label: "ground shaders",
|
|
515
|
-
code: /* wgsl */ `
|
|
516
|
-
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
517
|
-
struct Light { direction: vec4f, color: vec4f, };
|
|
518
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
519
|
-
struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
|
|
520
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
521
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
522
|
-
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
523
|
-
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
524
|
-
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
525
|
-
struct VertexOutput {
|
|
526
|
-
@builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
|
|
527
|
-
};
|
|
528
|
-
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
|
|
529
|
-
var o: VertexOutput;
|
|
530
|
-
o.worldPos = position;
|
|
531
|
-
o.position = camera.projection * camera.view * vec4f(position, 1.0);
|
|
532
|
-
o.normal = normal; o.uv = uv; return o;
|
|
533
|
-
}
|
|
534
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
535
|
-
let n = normalize(input.normal);
|
|
536
|
-
let centerDist = length(input.worldPos.xz);
|
|
537
|
-
let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
|
|
538
|
-
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
539
|
-
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
540
|
-
let ndcPos = clipPos.xyz / clipPos.w;
|
|
541
|
-
let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
542
|
-
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
543
|
-
let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
544
|
-
let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
545
|
-
let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
|
|
546
|
-
let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
|
|
547
|
-
var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
|
|
548
|
-
let l = -light.lights[0].direction.xyz;
|
|
549
|
-
let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
|
|
550
|
-
return vec4f(finalColor * lightAccum, edgeFade);
|
|
551
|
-
}
|
|
552
|
-
`,
|
|
553
|
-
})
|
|
554
|
-
this.groundPipeline = this.createRenderPipeline({
|
|
555
|
-
label: "ground pipeline",
|
|
556
|
-
layout: groundPipelineLayout,
|
|
557
|
-
shaderModule: groundShaderModule,
|
|
558
|
-
vertexBuffers: fullVertexBuffers,
|
|
559
|
-
fragmentTarget: standardBlend,
|
|
560
|
-
cullMode: "back",
|
|
561
|
-
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
562
|
-
})
|
|
563
|
-
|
|
564
497
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
565
498
|
size: 64,
|
|
566
499
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -677,25 +610,6 @@ export class Engine {
|
|
|
677
610
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
678
611
|
})
|
|
679
612
|
|
|
680
|
-
// Create reflection pipeline (multisampled version for higher quality)
|
|
681
|
-
this.reflectionPipeline = this.createRenderPipeline({
|
|
682
|
-
label: "reflection pipeline",
|
|
683
|
-
layout: mainPipelineLayout,
|
|
684
|
-
shaderModule,
|
|
685
|
-
vertexBuffers: fullVertexBuffers,
|
|
686
|
-
fragmentTarget: {
|
|
687
|
-
format: this.presentationFormat,
|
|
688
|
-
blend: standardBlend.blend,
|
|
689
|
-
},
|
|
690
|
-
multisample: { count: this.sampleCount }, // Use same multisampling as main render
|
|
691
|
-
cullMode: "none",
|
|
692
|
-
depthStencil: {
|
|
693
|
-
format: "depth24plus-stencil8",
|
|
694
|
-
depthWriteEnabled: true,
|
|
695
|
-
depthCompare: "less-equal",
|
|
696
|
-
},
|
|
697
|
-
})
|
|
698
|
-
|
|
699
613
|
// Create bind group layout for outline pipelines
|
|
700
614
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
701
615
|
label: "outline bind group layout",
|
|
@@ -974,7 +888,7 @@ export class Engine {
|
|
|
974
888
|
this.multisampleTexture = this.device.createTexture({
|
|
975
889
|
label: "multisample render target",
|
|
976
890
|
size: [width, height],
|
|
977
|
-
sampleCount:
|
|
891
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
978
892
|
format: this.presentationFormat,
|
|
979
893
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
980
894
|
})
|
|
@@ -982,29 +896,20 @@ export class Engine {
|
|
|
982
896
|
this.depthTexture = this.device.createTexture({
|
|
983
897
|
label: "depth texture",
|
|
984
898
|
size: [width, height],
|
|
985
|
-
sampleCount:
|
|
899
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
986
900
|
format: "depth24plus-stencil8",
|
|
987
901
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
988
902
|
})
|
|
989
903
|
|
|
990
904
|
const depthTextureView = this.depthTexture.createView()
|
|
991
905
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
this.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
loadOp: "clear",
|
|
1000
|
-
storeOp: "store",
|
|
1001
|
-
}
|
|
1002
|
-
: {
|
|
1003
|
-
view: this.context.getCurrentTexture().createView(),
|
|
1004
|
-
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1005
|
-
loadOp: "clear",
|
|
1006
|
-
storeOp: "store",
|
|
1007
|
-
}
|
|
906
|
+
const colorAttachment: GPURenderPassColorAttachment = {
|
|
907
|
+
view: this.multisampleTexture.createView(),
|
|
908
|
+
resolveTarget: this.context.getCurrentTexture().createView(),
|
|
909
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
910
|
+
loadOp: "clear",
|
|
911
|
+
storeOp: "store",
|
|
912
|
+
}
|
|
1008
913
|
|
|
1009
914
|
this.renderPassDescriptor = {
|
|
1010
915
|
label: "renderPass",
|
|
@@ -1038,6 +943,29 @@ export class Engine {
|
|
|
1038
943
|
this.camera.attachControl(this.canvas)
|
|
1039
944
|
}
|
|
1040
945
|
|
|
946
|
+
/** Set camera look-at target to a point. Clears any model binding. */
|
|
947
|
+
public setCameraTarget(v: Vec3): void
|
|
948
|
+
/** Bind camera target to a model's bone. Engine updates target each frame. Bone not found → (0,0,0) + offset. Pass null to unbind. */
|
|
949
|
+
public setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
950
|
+
public setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
951
|
+
if (modelOrVec === null) {
|
|
952
|
+
this.cameraTargetModel = null
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
|
|
956
|
+
this.cameraTargetModel = null
|
|
957
|
+
this.camera.target.x = modelOrVec.x
|
|
958
|
+
this.camera.target.y = modelOrVec.y
|
|
959
|
+
this.camera.target.z = modelOrVec.z
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
this.cameraTargetModel = modelOrVec
|
|
963
|
+
this.cameraTargetBoneName = boneName ?? ""
|
|
964
|
+
this.cameraTargetOffset.x = offset?.x ?? 0
|
|
965
|
+
this.cameraTargetOffset.y = offset?.y ?? 0
|
|
966
|
+
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
967
|
+
}
|
|
968
|
+
|
|
1041
969
|
// Step 5: Create lighting buffers
|
|
1042
970
|
private setupLighting() {
|
|
1043
971
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1096,11 +1024,8 @@ export class Engine {
|
|
|
1096
1024
|
width?: number
|
|
1097
1025
|
height?: number
|
|
1098
1026
|
diffuseColor?: Vec3
|
|
1099
|
-
reflectionLevel?: number
|
|
1100
|
-
reflectionTextureSize?: number
|
|
1101
1027
|
fadeStart?: number
|
|
1102
1028
|
fadeEnd?: number
|
|
1103
|
-
mode?: "reflection" | "shadow"
|
|
1104
1029
|
shadowMapSize?: number
|
|
1105
1030
|
shadowStrength?: number
|
|
1106
1031
|
}): void {
|
|
@@ -1108,29 +1033,20 @@ export class Engine {
|
|
|
1108
1033
|
width: 100,
|
|
1109
1034
|
height: 100,
|
|
1110
1035
|
diffuseColor: new Vec3(1, 1, 1),
|
|
1111
|
-
reflectionLevel: 0.5,
|
|
1112
|
-
reflectionTextureSize: 1024,
|
|
1113
1036
|
fadeStart: 5.0,
|
|
1114
1037
|
fadeEnd: 60.0,
|
|
1115
|
-
mode: "reflection" as const,
|
|
1116
1038
|
shadowMapSize: 4096,
|
|
1117
1039
|
shadowStrength: 1.0,
|
|
1118
1040
|
...options,
|
|
1119
1041
|
}
|
|
1120
|
-
this.groundMode = opts.mode
|
|
1121
1042
|
this.createGroundGeometry(opts.width, opts.height)
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
this.createReflectionTexture(opts.reflectionTextureSize)
|
|
1125
|
-
} else {
|
|
1126
|
-
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
|
|
1127
|
-
}
|
|
1128
|
-
this.groundHasReflections = true
|
|
1043
|
+
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
|
|
1044
|
+
this.hasGround = true
|
|
1129
1045
|
this.groundDrawCall = {
|
|
1130
1046
|
type: "ground",
|
|
1131
1047
|
count: 6,
|
|
1132
1048
|
firstIndex: 0,
|
|
1133
|
-
bindGroup:
|
|
1049
|
+
bindGroup: this.groundShadowBindGroup!,
|
|
1134
1050
|
materialName: "Ground",
|
|
1135
1051
|
}
|
|
1136
1052
|
}
|
|
@@ -1185,6 +1101,17 @@ export class Engine {
|
|
|
1185
1101
|
}
|
|
1186
1102
|
}
|
|
1187
1103
|
|
|
1104
|
+
public async loadModel(path: string): Promise<Model>
|
|
1105
|
+
public async loadModel(name: string, path: string): Promise<Model>
|
|
1106
|
+
public async loadModel(nameOrPath: string, path?: string): Promise<Model> {
|
|
1107
|
+
const pmxPath = path === undefined ? nameOrPath : path
|
|
1108
|
+
const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath
|
|
1109
|
+
const model = await PmxLoader.load(pmxPath)
|
|
1110
|
+
model.setName(name)
|
|
1111
|
+
await this.addModel(model, pmxPath, name)
|
|
1112
|
+
return model
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1188
1115
|
public async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
|
|
1189
1116
|
const requested = name ?? model.name
|
|
1190
1117
|
let key = requested
|
|
@@ -1249,12 +1176,20 @@ export class Engine {
|
|
|
1249
1176
|
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
|
1250
1177
|
}
|
|
1251
1178
|
|
|
1252
|
-
public
|
|
1253
|
-
this.
|
|
1179
|
+
public setIKEnabled(enabled: boolean): void {
|
|
1180
|
+
this.ikEnabled = enabled
|
|
1254
1181
|
}
|
|
1255
1182
|
|
|
1256
|
-
public
|
|
1257
|
-
this.
|
|
1183
|
+
public getIKEnabled(): boolean {
|
|
1184
|
+
return this.ikEnabled
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
public setPhysicsEnabled(enabled: boolean): void {
|
|
1188
|
+
this.physicsEnabled = enabled
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
public getPhysicsEnabled(): boolean {
|
|
1192
|
+
return this.physicsEnabled
|
|
1258
1193
|
}
|
|
1259
1194
|
|
|
1260
1195
|
public resetPhysics(): void {
|
|
@@ -1275,9 +1210,9 @@ export class Engine {
|
|
|
1275
1210
|
|
|
1276
1211
|
private updateInstances(deltaTime: number): void {
|
|
1277
1212
|
this.forEachInstance((inst) => {
|
|
1278
|
-
const verticesChanged = inst.model.update(deltaTime)
|
|
1213
|
+
const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
|
|
1279
1214
|
if (verticesChanged) inst.vertexBufferNeedsUpdate = true
|
|
1280
|
-
if (inst.physics &&
|
|
1215
|
+
if (inst.physics && this.physicsEnabled) {
|
|
1281
1216
|
inst.physics.step(
|
|
1282
1217
|
deltaTime,
|
|
1283
1218
|
inst.model.getWorldMatrices(),
|
|
@@ -1454,58 +1389,6 @@ export class Engine {
|
|
|
1454
1389
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
|
|
1455
1390
|
}
|
|
1456
1391
|
|
|
1457
|
-
private createGroundMaterialBuffer(diffuseColor: Vec3, reflectionLevel: number, fadeStart: number, fadeEnd: number) {
|
|
1458
|
-
const u = new Float32Array(8)
|
|
1459
|
-
u[0] = diffuseColor.x
|
|
1460
|
-
u[1] = diffuseColor.y
|
|
1461
|
-
u[2] = diffuseColor.z
|
|
1462
|
-
u[3] = reflectionLevel
|
|
1463
|
-
u[4] = fadeStart
|
|
1464
|
-
u[5] = fadeEnd
|
|
1465
|
-
u[6] = 0
|
|
1466
|
-
u[7] = 0
|
|
1467
|
-
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1468
|
-
label: "ground material uniform buffer",
|
|
1469
|
-
size: 64,
|
|
1470
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1471
|
-
})
|
|
1472
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u)
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
private createReflectionTexture(size: number) {
|
|
1476
|
-
this.groundReflectionTexture = this.device.createTexture({
|
|
1477
|
-
label: "ground reflection texture",
|
|
1478
|
-
size: [size, size],
|
|
1479
|
-
sampleCount: this.sampleCount,
|
|
1480
|
-
format: this.presentationFormat,
|
|
1481
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1482
|
-
})
|
|
1483
|
-
this.groundReflectionResolveTexture = this.device.createTexture({
|
|
1484
|
-
label: "ground reflection resolve texture",
|
|
1485
|
-
size: [size, size],
|
|
1486
|
-
format: this.presentationFormat,
|
|
1487
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1488
|
-
})
|
|
1489
|
-
this.groundReflectionDepthTexture = this.device.createTexture({
|
|
1490
|
-
label: "ground reflection depth texture",
|
|
1491
|
-
size: [size, size],
|
|
1492
|
-
sampleCount: this.sampleCount,
|
|
1493
|
-
format: "depth24plus-stencil8",
|
|
1494
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1495
|
-
})
|
|
1496
|
-
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1497
|
-
label: "ground reflection bind group",
|
|
1498
|
-
layout: this.groundBindGroupLayout,
|
|
1499
|
-
entries: [
|
|
1500
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1501
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1502
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1503
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1504
|
-
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer! } },
|
|
1505
|
-
],
|
|
1506
|
-
})
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
1392
|
private createShadowGroundResources(
|
|
1510
1393
|
shadowMapSize: number,
|
|
1511
1394
|
diffuseColor: Vec3,
|
|
@@ -1776,85 +1659,29 @@ export class Engine {
|
|
|
1776
1659
|
}
|
|
1777
1660
|
}
|
|
1778
1661
|
|
|
1779
|
-
//
|
|
1780
|
-
private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
} else {
|
|
1790
|
-
pass.setPipeline(this.eyePipeline)
|
|
1791
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1792
|
-
for (const draw of inst.drawCalls) {
|
|
1793
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1794
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1795
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1796
|
-
}
|
|
1662
|
+
// Post-alpha eye: render eye draws; main pass writes stencil so hair-over-eyes can use it for see-through bangs.
|
|
1663
|
+
private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance) {
|
|
1664
|
+
pass.setPipeline(this.eyePipeline)
|
|
1665
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1666
|
+
for (const draw of inst.drawCalls) {
|
|
1667
|
+
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1668
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1669
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1797
1670
|
}
|
|
1798
1671
|
}
|
|
1799
1672
|
}
|
|
1800
1673
|
|
|
1801
1674
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
1802
|
-
if (!this.
|
|
1803
|
-
|
|
1804
|
-
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline)
|
|
1675
|
+
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
1676
|
+
pass.setPipeline(this.groundShadowPipeline)
|
|
1805
1677
|
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
1806
1678
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
|
|
1807
1679
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup)
|
|
1808
1680
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
|
|
1809
1681
|
}
|
|
1810
1682
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0)
|
|
1815
|
-
this.updateCameraUniforms()
|
|
1816
|
-
|
|
1817
|
-
const reflectionEncoder = this.device.createCommandEncoder()
|
|
1818
|
-
const reflectionPassDescriptor: GPURenderPassDescriptor = {
|
|
1819
|
-
label: "reflection render pass",
|
|
1820
|
-
colorAttachments: [
|
|
1821
|
-
{
|
|
1822
|
-
view: this.groundReflectionTexture!.createView(),
|
|
1823
|
-
resolveTarget: this.groundReflectionResolveTexture!.createView(),
|
|
1824
|
-
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
|
|
1825
|
-
loadOp: "clear",
|
|
1826
|
-
storeOp: "store",
|
|
1827
|
-
},
|
|
1828
|
-
],
|
|
1829
|
-
depthStencilAttachment: {
|
|
1830
|
-
view: this.groundReflectionDepthTexture!.createView(),
|
|
1831
|
-
depthClearValue: 1.0,
|
|
1832
|
-
depthLoadOp: "clear",
|
|
1833
|
-
depthStoreOp: "store",
|
|
1834
|
-
stencilClearValue: 0,
|
|
1835
|
-
stencilLoadOp: "clear",
|
|
1836
|
-
stencilStoreOp: "discard",
|
|
1837
|
-
},
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor)
|
|
1841
|
-
this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix))
|
|
1842
|
-
reflectionPass.end()
|
|
1843
|
-
this.device.queue.submit([reflectionEncoder.finish()])
|
|
1844
|
-
this.updateSkinMatrices()
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
|
|
1848
|
-
if (useReflectionPipeline) {
|
|
1849
|
-
pass.setPipeline(this.reflectionPipeline)
|
|
1850
|
-
for (const draw of inst.drawCalls) {
|
|
1851
|
-
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1852
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1853
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
return
|
|
1857
|
-
}
|
|
1683
|
+
// Post-alpha eye: hair-over-eyes uses stencil (from renderEyes) for 50% alpha; hair-over-non-eyes uses inverse stencil.
|
|
1684
|
+
private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance) {
|
|
1858
1685
|
|
|
1859
1686
|
const hasHair = inst.drawCalls.some(
|
|
1860
1687
|
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d)
|
|
@@ -2052,18 +1879,29 @@ export class Engine {
|
|
|
2052
1879
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
2053
1880
|
this.lastFrameTime = currentTime
|
|
2054
1881
|
|
|
2055
|
-
this.updateCameraUniforms()
|
|
2056
1882
|
this.updateRenderTarget()
|
|
2057
1883
|
|
|
2058
1884
|
const hasModels = this.modelInstances.size > 0
|
|
2059
1885
|
if (hasModels) {
|
|
2060
1886
|
this.updateInstances(deltaTime)
|
|
2061
1887
|
this.updateSkinMatrices()
|
|
1888
|
+
// Update camera target from bound model (bone not found → 0,0,0 + offset)
|
|
1889
|
+
if (this.cameraTargetModel) {
|
|
1890
|
+
const pos = this.cameraTargetModel.getBoneWorldPosition(this.cameraTargetBoneName)
|
|
1891
|
+
const px = pos?.x ?? 0
|
|
1892
|
+
const py = pos?.y ?? 0
|
|
1893
|
+
const pz = pos?.z ?? 0
|
|
1894
|
+
this.camera.target.x = px + this.cameraTargetOffset.x
|
|
1895
|
+
this.camera.target.y = py + this.cameraTargetOffset.y
|
|
1896
|
+
this.camera.target.z = pz + this.cameraTargetOffset.z
|
|
1897
|
+
}
|
|
2062
1898
|
}
|
|
2063
|
-
|
|
1899
|
+
|
|
1900
|
+
this.updateCameraUniforms()
|
|
1901
|
+
if (this.hasGround) this.updateShadowLightVP()
|
|
2064
1902
|
|
|
2065
1903
|
const encoder = this.device.createCommandEncoder()
|
|
2066
|
-
if (hasModels && this.
|
|
1904
|
+
if (hasModels && this.hasGround && this.shadowMapDepthView) {
|
|
2067
1905
|
const sp = encoder.beginRenderPass({
|
|
2068
1906
|
colorAttachments: [],
|
|
2069
1907
|
depthStencilAttachment: {
|
|
@@ -2079,8 +1917,8 @@ export class Engine {
|
|
|
2079
1917
|
}
|
|
2080
1918
|
|
|
2081
1919
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
2082
|
-
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst
|
|
2083
|
-
if (this.
|
|
1920
|
+
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst))
|
|
1921
|
+
if (this.hasGround) this.renderGround(pass)
|
|
2084
1922
|
|
|
2085
1923
|
pass.end()
|
|
2086
1924
|
this.device.queue.submit([encoder.finish()])
|
|
@@ -2098,50 +1936,37 @@ export class Engine {
|
|
|
2098
1936
|
}
|
|
2099
1937
|
}
|
|
2100
1938
|
|
|
2101
|
-
private
|
|
2102
|
-
pass.
|
|
2103
|
-
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
2104
|
-
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
2105
|
-
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
2106
|
-
|
|
2107
|
-
if (useReflection && mirrorMatrix) {
|
|
2108
|
-
this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix)
|
|
2109
|
-
pass.setPipeline(this.reflectionPipeline)
|
|
2110
|
-
for (const draw of inst.drawCalls) {
|
|
2111
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2112
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2113
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
this.renderEyes(pass, inst, true)
|
|
2117
|
-
this.renderHair(pass, inst, true)
|
|
2118
|
-
for (const draw of inst.drawCalls) {
|
|
2119
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2120
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2121
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
this.drawOutlines(pass, inst, true, true)
|
|
2125
|
-
return
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
pass.setPipeline(this.modelPipeline)
|
|
1939
|
+
private drawOpaque(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
|
|
1940
|
+
pass.setPipeline(pipeline)
|
|
2129
1941
|
for (const draw of inst.drawCalls) {
|
|
2130
1942
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2131
1943
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2132
1944
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2133
1945
|
}
|
|
2134
1946
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
pass.setPipeline(
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
private drawTransparent(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
|
|
1950
|
+
pass.setPipeline(pipeline)
|
|
2139
1951
|
for (const draw of inst.drawCalls) {
|
|
2140
1952
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2141
1953
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2142
1954
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2143
1955
|
}
|
|
2144
1956
|
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1960
|
+
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1961
|
+
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1962
|
+
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1963
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1964
|
+
|
|
1965
|
+
this.drawOpaque(pass, inst, this.modelPipeline)
|
|
1966
|
+
this.renderEyes(pass, inst)
|
|
1967
|
+
this.drawOutlines(pass, inst, false)
|
|
1968
|
+
this.renderHair(pass, inst)
|
|
1969
|
+
this.drawTransparent(pass, inst, this.modelPipeline)
|
|
2145
1970
|
this.drawOutlines(pass, inst, true)
|
|
2146
1971
|
}
|
|
2147
1972
|
|
|
@@ -2159,13 +1984,8 @@ export class Engine {
|
|
|
2159
1984
|
}
|
|
2160
1985
|
|
|
2161
1986
|
private updateRenderTarget() {
|
|
2162
|
-
// Update render target to use current canvas texture
|
|
2163
1987
|
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
2164
|
-
|
|
2165
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
2166
|
-
} else {
|
|
2167
|
-
colorAttachment.view = this.context.getCurrentTexture().createView()
|
|
2168
|
-
}
|
|
1988
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
2169
1989
|
}
|
|
2170
1990
|
|
|
2171
1991
|
private updateSkinMatrices() {
|
|
@@ -2181,8 +2001,7 @@ export class Engine {
|
|
|
2181
2001
|
})
|
|
2182
2002
|
}
|
|
2183
2003
|
|
|
2184
|
-
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean
|
|
2185
|
-
if (useReflectionPipeline) return
|
|
2004
|
+
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean) {
|
|
2186
2005
|
pass.setPipeline(this.outlinePipeline)
|
|
2187
2006
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2188
2007
|
for (const draw of inst.drawCalls) {
|
|
@@ -2218,42 +2037,4 @@ export class Engine {
|
|
|
2218
2037
|
}
|
|
2219
2038
|
}
|
|
2220
2039
|
|
|
2221
|
-
private createMirrorMatrix(planeNormal: Vec3, planeDistance: number): Mat4 {
|
|
2222
|
-
// Create reflection matrix across a plane
|
|
2223
|
-
const n = planeNormal.normalize()
|
|
2224
|
-
|
|
2225
|
-
return new Mat4(
|
|
2226
|
-
new Float32Array([
|
|
2227
|
-
1 - 2 * n.x * n.x,
|
|
2228
|
-
-2 * n.x * n.y,
|
|
2229
|
-
-2 * n.x * n.z,
|
|
2230
|
-
0,
|
|
2231
|
-
-2 * n.y * n.x,
|
|
2232
|
-
1 - 2 * n.y * n.y,
|
|
2233
|
-
-2 * n.y * n.z,
|
|
2234
|
-
0,
|
|
2235
|
-
-2 * n.z * n.x,
|
|
2236
|
-
-2 * n.z * n.y,
|
|
2237
|
-
1 - 2 * n.z * n.z,
|
|
2238
|
-
0,
|
|
2239
|
-
-2 * planeDistance * n.x,
|
|
2240
|
-
-2 * planeDistance * n.y,
|
|
2241
|
-
-2 * planeDistance * n.z,
|
|
2242
|
-
1,
|
|
2243
|
-
])
|
|
2244
|
-
)
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
private writeMirrorTransformedSkinMatrices(inst: ModelInstance, mirrorMatrix: Mat4) {
|
|
2248
|
-
const originalMatrices = inst.model.getSkinMatrices()
|
|
2249
|
-
const transformedMatrices = new Float32Array(originalMatrices.length)
|
|
2250
|
-
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
2251
|
-
const boneMatrixValues = new Float32Array(16)
|
|
2252
|
-
for (let j = 0; j < 16; j++) boneMatrixValues[j] = originalMatrices[i + j]
|
|
2253
|
-
const boneMatrix = new Mat4(boneMatrixValues)
|
|
2254
|
-
const transformed = mirrorMatrix.multiply(boneMatrix)
|
|
2255
|
-
for (let j = 0; j < 16; j++) transformedMatrices[i + j] = transformed.values[j]
|
|
2256
|
-
}
|
|
2257
|
-
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices)
|
|
2258
|
-
}
|
|
2259
2040
|
}
|