reze-engine 0.10.1 → 0.11.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 +113 -20
- package/dist/asset-reader.d.ts +16 -0
- package/dist/asset-reader.d.ts.map +1 -0
- package/dist/asset-reader.js +74 -0
- package/dist/engine.d.ts +179 -36
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1133 -321
- package/dist/folder-upload.d.ts +24 -0
- package/dist/folder-upload.d.ts.map +1 -0
- package/dist/folder-upload.js +50 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/model.d.ts +6 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +34 -2
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +9 -2
- package/dist/shaders/body.d.ts +2 -0
- package/dist/shaders/body.d.ts.map +1 -0
- package/dist/shaders/body.js +209 -0
- package/dist/shaders/classify.d.ts +4 -0
- package/dist/shaders/classify.d.ts.map +1 -0
- package/dist/shaders/classify.js +12 -0
- package/dist/shaders/cloth_rough.d.ts +2 -0
- package/dist/shaders/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/cloth_rough.js +172 -0
- package/dist/shaders/cloth_smooth.d.ts +2 -0
- package/dist/shaders/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/cloth_smooth.js +171 -0
- package/dist/shaders/default.d.ts +2 -0
- package/dist/shaders/default.d.ts.map +1 -0
- package/dist/shaders/default.js +168 -0
- package/dist/shaders/dfg_lut.d.ts +4 -0
- package/dist/shaders/dfg_lut.d.ts.map +1 -0
- package/dist/shaders/dfg_lut.js +125 -0
- package/dist/shaders/eye.d.ts +2 -0
- package/dist/shaders/eye.d.ts.map +1 -0
- package/dist/shaders/eye.js +142 -0
- package/dist/shaders/face.d.ts +2 -0
- package/dist/shaders/face.d.ts.map +1 -0
- package/dist/shaders/face.js +211 -0
- package/dist/shaders/hair.d.ts +2 -0
- package/dist/shaders/hair.d.ts.map +1 -0
- package/dist/shaders/hair.js +186 -0
- package/dist/shaders/ltc_mag_lut.d.ts +3 -0
- package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
- package/dist/shaders/ltc_mag_lut.js +1033 -0
- package/dist/shaders/metal.d.ts +2 -0
- package/dist/shaders/metal.d.ts.map +1 -0
- package/dist/shaders/metal.js +171 -0
- package/dist/shaders/nodes.d.ts +2 -0
- package/dist/shaders/nodes.d.ts.map +1 -0
- package/dist/shaders/nodes.js +423 -0
- package/dist/shaders/stockings.d.ts +2 -0
- package/dist/shaders/stockings.d.ts.map +1 -0
- package/dist/shaders/stockings.js +229 -0
- package/package.json +1 -1
- package/src/asset-reader.ts +79 -0
- package/src/engine.ts +1352 -383
- package/src/folder-upload.ts +59 -0
- package/src/index.ts +12 -2
- package/src/model.ts +34 -2
- package/src/pmx-loader.ts +11 -2
- package/src/shaders/body.ts +211 -0
- package/src/shaders/classify.ts +25 -0
- package/src/shaders/cloth_rough.ts +174 -0
- package/src/shaders/cloth_smooth.ts +173 -0
- package/src/shaders/default.ts +169 -0
- package/src/shaders/dfg_lut.ts +127 -0
- package/src/shaders/eye.ts +143 -0
- package/src/shaders/face.ts +213 -0
- package/src/shaders/hair.ts +188 -0
- package/src/shaders/ltc_mag_lut.ts +1035 -0
- package/src/shaders/metal.ts +173 -0
- package/src/shaders/nodes.ts +424 -0
- package/src/shaders/stockings.ts +231 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { normalizeAssetPath } from "./asset-reader"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Call on `<input type="file" webkitdirectory>` `change` **before** `input.value = ""`.
|
|
5
|
+
* `FileList` is live — clearing the input empties it; this copies to a stable `File[]`.
|
|
6
|
+
*/
|
|
7
|
+
function prepareLocalFolderFiles(fileList: FileList | null | undefined): {
|
|
8
|
+
files: File[]
|
|
9
|
+
pmxRelativePaths: string[]
|
|
10
|
+
} {
|
|
11
|
+
const files = fileList?.length ? Array.from(fileList) : []
|
|
12
|
+
const pmxRelativePaths: string[] = []
|
|
13
|
+
for (const f of files) {
|
|
14
|
+
const wr = (f as File & { webkitRelativePath?: string }).webkitRelativePath
|
|
15
|
+
if (!wr || !wr.toLowerCase().endsWith(".pmx")) continue
|
|
16
|
+
pmxRelativePaths.push(normalizeAssetPath(wr))
|
|
17
|
+
}
|
|
18
|
+
pmxRelativePaths.sort((a, b) => a.localeCompare(b))
|
|
19
|
+
return { files, pmxRelativePaths }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDirectoryUpload(files: File[]): boolean {
|
|
23
|
+
return files.length > 0 && files.every((f) => !!(f as File & { webkitRelativePath?: string }).webkitRelativePath)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
|
|
27
|
+
export function pmxFileAtRelativePath(files: File[], relativePath: string): File | undefined {
|
|
28
|
+
const norm = normalizeAssetPath(relativePath)
|
|
29
|
+
for (const f of files) {
|
|
30
|
+
const wr = (f as File & { webkitRelativePath?: string }).webkitRelativePath
|
|
31
|
+
if (wr && normalizeAssetPath(wr) === norm) return f
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Result of reading a folder input — switch on `status` in your UI. */
|
|
37
|
+
export type PmxFolderInputResult =
|
|
38
|
+
| { status: "empty" }
|
|
39
|
+
| { status: "not_directory" }
|
|
40
|
+
| { status: "no_pmx" }
|
|
41
|
+
| { status: "single"; files: File[]; pmxFile: File }
|
|
42
|
+
| { status: "multiple"; files: File[]; pmxRelativePaths: string[] }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
|
|
46
|
+
* Reset the input after: `e.target.value = ""`.
|
|
47
|
+
*/
|
|
48
|
+
export function parsePmxFolderInput(fileList: FileList | null | undefined): PmxFolderInputResult {
|
|
49
|
+
const { files, pmxRelativePaths } = prepareLocalFolderFiles(fileList)
|
|
50
|
+
if (files.length === 0) return { status: "empty" }
|
|
51
|
+
if (!isDirectoryUpload(files)) return { status: "not_directory" }
|
|
52
|
+
if (pmxRelativePaths.length === 0) return { status: "no_pmx" }
|
|
53
|
+
if (pmxRelativePaths.length === 1) {
|
|
54
|
+
const pmxFile = pmxFileAtRelativePath(files, pmxRelativePaths[0]!)
|
|
55
|
+
if (!pmxFile) return { status: "no_pmx" }
|
|
56
|
+
return { status: "single", files, pmxFile }
|
|
57
|
+
}
|
|
58
|
+
return { status: "multiple", files, pmxRelativePaths }
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
Engine,
|
|
3
|
+
DEFAULT_BLOOM_OPTIONS,
|
|
4
|
+
DEFAULT_VIEW_TRANSFORM,
|
|
5
|
+
type EngineStats,
|
|
6
|
+
type EngineOptions,
|
|
7
|
+
type BloomOptions,
|
|
8
|
+
type ViewTransformOptions,
|
|
9
|
+
type LoadModelFromFilesOptions,
|
|
10
|
+
} from "./engine"
|
|
11
|
+
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
|
|
2
12
|
export { Model } from "./model"
|
|
3
13
|
export { Vec3, Quat, Mat4 } from "./math"
|
|
4
14
|
export type {
|
|
@@ -12,4 +22,4 @@ export type {
|
|
|
12
22
|
} from "./animation"
|
|
13
23
|
export { FPS } from "./animation"
|
|
14
24
|
export { Physics, type PhysicsOptions } from "./physics"
|
|
15
|
-
export {
|
|
25
|
+
export type { MaterialPreset, MaterialPresetMap } from "./shaders/classify"
|
package/src/model.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
2
2
|
import { Engine } from "./engine"
|
|
3
|
+
import { joinAssetPath, type AssetReader } from "./asset-reader"
|
|
3
4
|
import { Rigidbody, Joint } from "./physics"
|
|
4
5
|
import { IKSolverSystem } from "./ik-solver"
|
|
5
6
|
import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
|
|
@@ -206,6 +207,14 @@ export class Model {
|
|
|
206
207
|
private morphTrackIndices: Map<string, number> = new Map()
|
|
207
208
|
private lastAppliedClip: AnimationClip | null = null
|
|
208
209
|
|
|
210
|
+
private assetReader: AssetReader | null = null
|
|
211
|
+
private assetBasePath = ""
|
|
212
|
+
|
|
213
|
+
/** Called by Engine when registering the model; enables loadVmd to resolve relative paths for folder uploads. */
|
|
214
|
+
setAssetContext(reader: AssetReader, basePath: string): void {
|
|
215
|
+
this.assetReader = reader
|
|
216
|
+
this.assetBasePath = basePath
|
|
217
|
+
}
|
|
209
218
|
|
|
210
219
|
constructor(
|
|
211
220
|
vertexData: Float32Array<ArrayBuffer>,
|
|
@@ -857,8 +866,31 @@ export class Model {
|
|
|
857
866
|
return { boneTracks, morphTracks, frameCount: maxFrame }
|
|
858
867
|
}
|
|
859
868
|
|
|
860
|
-
loadVmd(name: string,
|
|
861
|
-
|
|
869
|
+
loadVmd(name: string, urlOrRelative: string): Promise<void> {
|
|
870
|
+
const loadBuffer = (): Promise<ArrayBuffer> => {
|
|
871
|
+
const u = urlOrRelative.trim()
|
|
872
|
+
const useSiteFetch =
|
|
873
|
+
u.startsWith("http://") ||
|
|
874
|
+
u.startsWith("https://") ||
|
|
875
|
+
u.startsWith("/") ||
|
|
876
|
+
u.startsWith("blob:") ||
|
|
877
|
+
u.startsWith("data:")
|
|
878
|
+
if (useSiteFetch) {
|
|
879
|
+
return fetch(u).then((r) => {
|
|
880
|
+
if (!r.ok) throw new Error(`Failed to fetch VMD ${u}: ${r.status}`)
|
|
881
|
+
return r.arrayBuffer()
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
if (this.assetReader) {
|
|
885
|
+
return this.assetReader.readBinary(joinAssetPath(this.assetBasePath, u))
|
|
886
|
+
}
|
|
887
|
+
return fetch(u).then((r) => {
|
|
888
|
+
if (!r.ok) throw new Error(`Failed to fetch VMD ${u}: ${r.status}`)
|
|
889
|
+
return r.arrayBuffer()
|
|
890
|
+
})
|
|
891
|
+
}
|
|
892
|
+
return loadBuffer().then((buf) => {
|
|
893
|
+
const vmdKeyFrames = VMDLoader.loadFromBuffer(buf)
|
|
862
894
|
const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames)
|
|
863
895
|
this.animationState.loadAnimation(name, clip)
|
|
864
896
|
})
|
package/src/pmx-loader.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./model"
|
|
14
14
|
import { Mat4, Vec3 } from "./math"
|
|
15
15
|
import { Rigidbody, Joint, RigidbodyShape, RigidbodyType } from "./physics"
|
|
16
|
+
import { createFetchAssetReader, type AssetReader } from "./asset-reader"
|
|
16
17
|
|
|
17
18
|
export class PmxLoader {
|
|
18
19
|
private view: DataView
|
|
@@ -42,8 +43,16 @@ export class PmxLoader {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
static async load(url: string): Promise<Model> {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
return PmxLoader.loadFromReader(createFetchAssetReader(), url)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static loadFromBuffer(buffer: ArrayBuffer): Model {
|
|
50
|
+
return new PmxLoader(buffer).parse()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async loadFromReader(reader: AssetReader, pmxLogicalPath: string): Promise<Model> {
|
|
54
|
+
const buffer = await reader.readBinary(pmxLogicalPath)
|
|
55
|
+
return PmxLoader.loadFromBuffer(buffer)
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
private parse(): Model {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Body"; VALTORGB / math ops from m_graphs where dump omits them.
|
|
2
|
+
|
|
3
|
+
import { NODES_WGSL } from "./nodes"
|
|
4
|
+
|
|
5
|
+
export const BODY_SHADER_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
${NODES_WGSL}
|
|
8
|
+
|
|
9
|
+
struct CameraUniforms {
|
|
10
|
+
view: mat4x4f,
|
|
11
|
+
projection: mat4x4f,
|
|
12
|
+
viewPos: vec3f,
|
|
13
|
+
_padding: f32,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
struct Light {
|
|
17
|
+
direction: vec4f,
|
|
18
|
+
color: vec4f,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
struct LightUniforms {
|
|
22
|
+
ambientColor: vec4f,
|
|
23
|
+
lights: array<Light, 4>,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
struct MaterialUniforms {
|
|
27
|
+
diffuseColor: vec3f,
|
|
28
|
+
alpha: f32,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct VertexOutput {
|
|
32
|
+
@builtin(position) position: vec4f,
|
|
33
|
+
@location(0) normal: vec3f,
|
|
34
|
+
@location(1) uv: vec2f,
|
|
35
|
+
@location(2) worldPos: vec3f,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
39
|
+
|
|
40
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
41
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
42
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
43
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
44
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
45
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
46
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
47
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
48
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
|
+
|
|
50
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
51
|
+
let biasedPos = worldPos + n * 0.08;
|
|
52
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
53
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
54
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
55
|
+
let cmpZ = ndc.z - 0.001;
|
|
56
|
+
let ts = 1.0 / 4096.0;
|
|
57
|
+
var vis = 0.0;
|
|
58
|
+
for (var y = -1; y <= 1; y++) {
|
|
59
|
+
for (var x = -1; x <= 1; x++) {
|
|
60
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return vis / 9.0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PI_B: f32 = 3.141592653589793;
|
|
67
|
+
const F0_BODY: f32 = 0.04;
|
|
68
|
+
const BODY_ROUGHNESS: f32 = 0.3;
|
|
69
|
+
// Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
|
|
70
|
+
const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
|
|
71
|
+
const BODY_RIM2_POW: f32 = 1.4300000667572021;
|
|
72
|
+
const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
|
|
73
|
+
const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
|
|
74
|
+
const BODY_MIX_NPR: f32 = 0.5;
|
|
75
|
+
|
|
76
|
+
fn ggx_d_body(ndoth: f32, a2: f32) -> f32 {
|
|
77
|
+
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
78
|
+
return a2 / (PI_B * denom * denom);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn smith_g1_body(ndotx: f32, a2: f32) -> f32 {
|
|
82
|
+
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {
|
|
86
|
+
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// smoothstep-based ramp: t*t*(3-2*t) between two color stops
|
|
90
|
+
fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
91
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
92
|
+
let ss = t * t * (3.0 - 2.0 * t);
|
|
93
|
+
return mix(c0, c1, ss);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@vertex fn vs(
|
|
97
|
+
@location(0) position: vec3f,
|
|
98
|
+
@location(1) normal: vec3f,
|
|
99
|
+
@location(2) uv: vec2f,
|
|
100
|
+
@location(3) joints0: vec4<u32>,
|
|
101
|
+
@location(4) weights0: vec4<f32>
|
|
102
|
+
) -> VertexOutput {
|
|
103
|
+
var output: VertexOutput;
|
|
104
|
+
let pos4 = vec4f(position, 1.0);
|
|
105
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
106
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
107
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
108
|
+
var skinnedPos = vec4f(0.0);
|
|
109
|
+
var skinnedNrm = vec3f(0.0);
|
|
110
|
+
for (var i = 0u; i < 4u; i++) {
|
|
111
|
+
let m = skinMats[joints0[i]];
|
|
112
|
+
let w = nw[i];
|
|
113
|
+
skinnedPos += (m * pos4) * w;
|
|
114
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
115
|
+
}
|
|
116
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
117
|
+
output.normal = normalize(skinnedNrm);
|
|
118
|
+
output.uv = uv;
|
|
119
|
+
output.worldPos = skinnedPos.xyz;
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
124
|
+
let alpha = material.alpha;
|
|
125
|
+
if (alpha < 0.001) { discard; }
|
|
126
|
+
|
|
127
|
+
let n = normalize(input.normal);
|
|
128
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
129
|
+
let l = -light.lights[0].direction.xyz;
|
|
130
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
131
|
+
|
|
132
|
+
let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
133
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
134
|
+
|
|
135
|
+
// ═══ TOON MASK: ShaderToRGB → ramp.008 CONSTANT [0→black, 0.2966→white] ═══
|
|
136
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
137
|
+
let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
|
|
138
|
+
|
|
139
|
+
// ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
|
|
140
|
+
let shadow_tint = hue_sat(0.5, 2.0, 0.3499999940395355, 1.0, tex_color);
|
|
141
|
+
let lit_tint = hue_sat(0.5, 1.5, 1.0, 1.0, tex_color);
|
|
142
|
+
let toon_color = mix_blend(toon, shadow_tint, lit_tint);
|
|
143
|
+
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
144
|
+
|
|
145
|
+
// ═══ AO CHAIN: AO → ramp CONSTANT [0→white, 0.5995→black] → Mix.003 ═══
|
|
146
|
+
let ao = ao_fake(n, v);
|
|
147
|
+
let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
|
|
148
|
+
let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));
|
|
149
|
+
|
|
150
|
+
// ═══ EMISSION.003 (strength=4.0) ═══
|
|
151
|
+
let emission3 = ao_mixed * 4.0;
|
|
152
|
+
|
|
153
|
+
// ═══ WARM: 颜色渐变.008 → 运算.006 ADD +0.5 (m_graphs) → clamp → 颜色渐变.003 ═══
|
|
154
|
+
let ao_inv = invert_f(1.0, ao_ramp);
|
|
155
|
+
let warm_str = ao_inv * BODY_WARM_AO_MUL;
|
|
156
|
+
let warm_input = clamp(toon + 0.5, 0.0, 1.0);
|
|
157
|
+
let warm_color = ramp_cardinal(warm_input, 0.2409,
|
|
158
|
+
vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
|
|
159
|
+
vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
|
|
160
|
+
let warm_emission = warm_color * warm_str;
|
|
161
|
+
|
|
162
|
+
// ═══ RIM 1: 菲涅尔 × 层权重.001 Facing Blend=0.24 → 自发光 Strength ═══
|
|
163
|
+
let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
|
|
164
|
+
let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
|
|
165
|
+
|
|
166
|
+
// ═══ RIM 2: 层权重.002 Facing → 运算.007 POWER → 颜色渐变.010 EASE → MixShader.002 Fac ═══
|
|
167
|
+
let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
|
|
168
|
+
let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
|
|
169
|
+
let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
|
|
170
|
+
let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
|
|
171
|
+
|
|
172
|
+
// ═══ NPR STACK: AddShader chain (no bright gate in body) ═══
|
|
173
|
+
let add0 = rim1 + rim2_mixed;
|
|
174
|
+
let npr_stack = add0 + warm_emission;
|
|
175
|
+
|
|
176
|
+
// ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
|
|
177
|
+
let gen = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0, 1.0, 1.5));
|
|
178
|
+
let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);
|
|
179
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
180
|
+
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
|
|
181
|
+
|
|
182
|
+
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
|
|
183
|
+
let p_emission = bc * 0.2;
|
|
184
|
+
|
|
185
|
+
let ao2 = ao_fake(n, v);
|
|
186
|
+
let sss = ramp_linear(ao2, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
|
|
187
|
+
|
|
188
|
+
let p_ndotl = max(dot(bumped_n, l), 0.0);
|
|
189
|
+
let p_ndotv = max(dot(bumped_n, v), 0.001);
|
|
190
|
+
let h = normalize(l + v);
|
|
191
|
+
let p_ndoth = max(dot(bumped_n, h), 0.0);
|
|
192
|
+
let p_vdoth = max(dot(v, h), 0.0);
|
|
193
|
+
let a2 = BODY_ROUGHNESS * BODY_ROUGHNESS;
|
|
194
|
+
let D = ggx_d_body(p_ndoth, a2);
|
|
195
|
+
let G = smith_g1_body(p_ndotl, a2) * smith_g1_body(p_ndotv, a2);
|
|
196
|
+
let F = fresnel_schlick_body(p_vdoth, F0_BODY);
|
|
197
|
+
let spec = (D * G * F) / max(4.0 * p_ndotl * p_ndotv, 0.001) * ltc_brdf_scale(p_ndotv, BODY_ROUGHNESS);
|
|
198
|
+
let kd = (1.0 - F) * principled_base / PI_B;
|
|
199
|
+
let direct = (kd + spec) * sun * p_ndotl * shadow;
|
|
200
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
201
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
202
|
+
let ambient = principled_base * light.ambientColor.xyz;
|
|
203
|
+
let principled = ambient + direct + p_emission + vec3f(sss);
|
|
204
|
+
|
|
205
|
+
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
|
|
206
|
+
let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
|
|
207
|
+
|
|
208
|
+
return vec4f(final_color, alpha);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Material preset types for NPR pipeline dispatch.
|
|
2
|
+
// Mapping is explicit — the consumer provides a MaterialPresetMap that assigns
|
|
3
|
+
// material names to presets. Unmapped materials fall back to "default" (Principled BSDF).
|
|
4
|
+
|
|
5
|
+
export type MaterialPreset =
|
|
6
|
+
| "default"
|
|
7
|
+
| "face"
|
|
8
|
+
| "hair"
|
|
9
|
+
| "body"
|
|
10
|
+
| "eye"
|
|
11
|
+
| "stockings"
|
|
12
|
+
| "metal"
|
|
13
|
+
| "cloth_smooth"
|
|
14
|
+
| "cloth_rough"
|
|
15
|
+
|
|
16
|
+
// Keys = preset name, values = array of material names that should use that preset.
|
|
17
|
+
export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
|
|
18
|
+
|
|
19
|
+
export function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): MaterialPreset {
|
|
20
|
+
if (!map) return "default"
|
|
21
|
+
for (const [preset, names] of Object.entries(map)) {
|
|
22
|
+
if (names && names.includes(materialName)) return preset as MaterialPreset
|
|
23
|
+
}
|
|
24
|
+
return "default"
|
|
25
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// M_Rough_Cloth — NPR graph identical to M_Smooth_Cloth but bump chain IS live
|
|
2
|
+
// (噪波→渐变→凹凸.Normal → 原理化BSDF.Normal in m_graphs) and Roughness=0.8187.
|
|
3
|
+
|
|
4
|
+
import { NODES_WGSL } from "./nodes"
|
|
5
|
+
|
|
6
|
+
export const CLOTH_ROUGH_SHADER_WGSL = /* wgsl */ `
|
|
7
|
+
|
|
8
|
+
${NODES_WGSL}
|
|
9
|
+
|
|
10
|
+
struct CameraUniforms {
|
|
11
|
+
view: mat4x4f,
|
|
12
|
+
projection: mat4x4f,
|
|
13
|
+
viewPos: vec3f,
|
|
14
|
+
_padding: f32,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct Light {
|
|
18
|
+
direction: vec4f,
|
|
19
|
+
color: vec4f,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct LightUniforms {
|
|
23
|
+
ambientColor: vec4f,
|
|
24
|
+
lights: array<Light, 4>,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
struct MaterialUniforms {
|
|
28
|
+
diffuseColor: vec3f,
|
|
29
|
+
alpha: f32,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
struct VertexOutput {
|
|
33
|
+
@builtin(position) position: vec4f,
|
|
34
|
+
@location(0) normal: vec3f,
|
|
35
|
+
@location(1) uv: vec2f,
|
|
36
|
+
@location(2) worldPos: vec3f,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
40
|
+
|
|
41
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
42
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
43
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
44
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
45
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
46
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
47
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
48
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
49
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
50
|
+
|
|
51
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
let biasedPos = worldPos + n * 0.08;
|
|
53
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
54
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
55
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
56
|
+
let cmpZ = ndc.z - 0.001;
|
|
57
|
+
let ts = 1.0 / 4096.0;
|
|
58
|
+
var vis = 0.0;
|
|
59
|
+
for (var y = -1; y <= 1; y++) {
|
|
60
|
+
for (var x = -1; x <= 1; x++) {
|
|
61
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return vis / 9.0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PI_CR: f32 = 3.141592653589793;
|
|
68
|
+
const CLOTH_R_SPECULAR: f32 = 0.8;
|
|
69
|
+
const CLOTH_R_ROUGHNESS: f32 = 0.8187;
|
|
70
|
+
const CLOTH_R_TOON_EDGE: f32 = 0.2966;
|
|
71
|
+
const CLOTH_R_MIX04_MUL: f32 = 0.5;
|
|
72
|
+
const CLOTH_R_EMIT_STR: f32 = 18.200000762939453;
|
|
73
|
+
const CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
74
|
+
const CLOTH_R_NOISE_SCALE: f32 = 17.7;
|
|
75
|
+
const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
76
|
+
|
|
77
|
+
@vertex fn vs(
|
|
78
|
+
@location(0) position: vec3f,
|
|
79
|
+
@location(1) normal: vec3f,
|
|
80
|
+
@location(2) uv: vec2f,
|
|
81
|
+
@location(3) joints0: vec4<u32>,
|
|
82
|
+
@location(4) weights0: vec4<f32>
|
|
83
|
+
) -> VertexOutput {
|
|
84
|
+
var output: VertexOutput;
|
|
85
|
+
let pos4 = vec4f(position, 1.0);
|
|
86
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
87
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
88
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
89
|
+
var skinnedPos = vec4f(0.0);
|
|
90
|
+
var skinnedNrm = vec3f(0.0);
|
|
91
|
+
for (var i = 0u; i < 4u; i++) {
|
|
92
|
+
let m = skinMats[joints0[i]];
|
|
93
|
+
let w = nw[i];
|
|
94
|
+
skinnedPos += (m * pos4) * w;
|
|
95
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
96
|
+
}
|
|
97
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
98
|
+
output.normal = normalize(skinnedNrm);
|
|
99
|
+
output.uv = uv;
|
|
100
|
+
output.worldPos = skinnedPos.xyz;
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
105
|
+
let n = normalize(input.normal);
|
|
106
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
107
|
+
let l = -light.lights[0].direction.xyz;
|
|
108
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
109
|
+
let amb = light.ambientColor.xyz;
|
|
110
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
111
|
+
|
|
112
|
+
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
113
|
+
let tex_rgb = tex_s.rgb;
|
|
114
|
+
let out_alpha = material.alpha * tex_s.a;
|
|
115
|
+
if (out_alpha < 0.001) { discard; }
|
|
116
|
+
|
|
117
|
+
// Shader→RGB → 颜色渐变.008 CONSTANT (edge AA terminator)
|
|
118
|
+
let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
119
|
+
let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_R_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
|
|
120
|
+
let toon_r = ramp008.r;
|
|
121
|
+
let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);
|
|
122
|
+
|
|
123
|
+
// 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
|
|
124
|
+
let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);
|
|
125
|
+
let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
|
|
126
|
+
|
|
127
|
+
// 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
|
|
128
|
+
let bevel_z = clamp(n.y, 0.0, 1.0);
|
|
129
|
+
let mix03 = mix_blend(bevel_z, mix04, dark_tex);
|
|
130
|
+
|
|
131
|
+
// 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (white/black) → 混合.002 OVERLAY Fac
|
|
132
|
+
let ao = ao_fake(n, v);
|
|
133
|
+
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
134
|
+
let mix01_fac = ao_ramp_c.r;
|
|
135
|
+
let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
|
|
136
|
+
|
|
137
|
+
// 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
|
|
138
|
+
let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);
|
|
139
|
+
let overlay_fac = mix01_rgb.r;
|
|
140
|
+
let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
|
|
141
|
+
let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
|
|
142
|
+
|
|
143
|
+
// 噪波→渐变→凹凸 (LIVE in M_Rough_Cloth — unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.
|
|
144
|
+
let noise_uv = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0));
|
|
145
|
+
let noise_val = tex_noise(noise_uv, CLOTH_R_NOISE_SCALE, 2.0, 0.5, 0.0);
|
|
146
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
147
|
+
let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
|
|
148
|
+
|
|
149
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.
|
|
150
|
+
let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
151
|
+
let NL = max(dot(bumped_n, l), 0.0);
|
|
152
|
+
let NV = max(dot(bumped_n, v), 1e-4);
|
|
153
|
+
|
|
154
|
+
let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);
|
|
155
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));
|
|
156
|
+
let split_sum = brdf_lut_baked(NV, CLOTH_R_ROUGHNESS);
|
|
157
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
|
|
158
|
+
|
|
159
|
+
let spec_direct = bsdf_ggx(bumped_n, l, v, CLOTH_R_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_R_ROUGHNESS);
|
|
160
|
+
let spec_indirect = amb;
|
|
161
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
162
|
+
|
|
163
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
164
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
165
|
+
let diffuse_radiance = principled_base * (sun * NL * shadow / PI_CR + amb);
|
|
166
|
+
let principled = diffuse_radiance + spec_radiance;
|
|
167
|
+
|
|
168
|
+
// 混合着色器.001 Fac=0.9: Shader=自发光.005, Shader_001=原理化BSDF
|
|
169
|
+
let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);
|
|
170
|
+
|
|
171
|
+
return vec4f(final_color, out_alpha);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
`
|