@holoscript/core 8.0.12 → 8.0.14
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 +14 -0
- package/bin/holo-decision.mjs +133 -0
- package/dist/{GLTFPipeline-L76YTXKA.js → GLTFPipeline-6LS3UDYX.js} +6 -6
- package/dist/{GLTFPipeline-ZWJN7GI6.cjs → GLTFPipeline-VTI6PRRQ.cjs} +10 -10
- package/dist/{HoloCompositionParser-FLBZKFD3.js → HoloCompositionParser-GILABNN3.js} +5 -5
- package/dist/{HoloCompositionParser-3XEC7IJC.cjs → HoloCompositionParser-ZVRYVEA5.cjs} +12 -12
- package/dist/{HoloScriptPlusParser-RNUCHOAJ.cjs → HoloScriptPlusParser-WTPRPRPR.cjs} +8 -8
- package/dist/{HoloScriptPlusParser-55AY4MUF.js → HoloScriptPlusParser-ZP53ECSL.js} +5 -5
- package/dist/{SceneIRCompiler-KWUFHZ4O.cjs → SceneIRCompiler-7KQ5NKAU.cjs} +11 -11
- package/dist/{SceneIRCompiler-63KA3M2F.js → SceneIRCompiler-VX75RB6H.js} +6 -6
- package/dist/{chunk-EM2OGPYE.js → chunk-2GKF7JQ6.js} +3 -3
- package/dist/{chunk-HVX7QQYK.js → chunk-3KHRZTNW.js} +3 -3
- package/dist/{chunk-UTUD6GJI.js → chunk-42Z3SVHQ.js} +3 -3
- package/dist/{chunk-Z4IUBW46.js → chunk-47XZ62KM.js} +112 -55
- package/dist/{chunk-GGR7QUKK.cjs → chunk-4CRMVEON.cjs} +37 -37
- package/dist/{chunk-QBY7LRN2.js → chunk-4NEPHK2K.js} +3 -3
- package/dist/{chunk-65S62L4D.cjs → chunk-5B2AJKA5.cjs} +20 -20
- package/dist/{chunk-PJFTTSKE.js → chunk-5T6BWIB7.js} +4 -4
- package/dist/{chunk-KFVNCW4Q.js → chunk-62OYZT6Q.js} +3 -3
- package/dist/{chunk-3KMANSQJ.cjs → chunk-645EWDZW.cjs} +4 -4
- package/dist/{chunk-BP4KTMLY.js → chunk-6FCNMTL5.js} +4 -4
- package/dist/{chunk-77QI552M.js → chunk-73EDVY5D.js} +3 -3
- package/dist/{chunk-7IDJDMPU.cjs → chunk-7FYSSI3C.cjs} +27 -2
- package/dist/{chunk-WNYXT4TE.cjs → chunk-7ITJP33Q.cjs} +3 -3
- package/dist/chunk-7ZKQ32XK.js +1571 -0
- package/dist/{chunk-P7JEKJTZ.cjs → chunk-B6JHTTMR.cjs} +25 -14
- package/dist/{chunk-FG6PYGIZ.js → chunk-BLCWQ225.js} +4 -4
- package/dist/{chunk-7NPZZZD7.js → chunk-C73GVAYC.js} +16 -9
- package/dist/{chunk-RMOEL2GD.cjs → chunk-CESMPUEZ.cjs} +3794 -1137
- package/dist/{chunk-SQXAZHDG.js → chunk-CHIZSMWD.js} +3 -3
- package/dist/{chunk-G35CAQ7I.cjs → chunk-D44CEFY4.cjs} +5 -5
- package/dist/chunk-ERYKKPHW.cjs +1884 -0
- package/dist/{chunk-TYNRXRHF.js → chunk-EYHPTJ3F.js} +3695 -1045
- package/dist/{chunk-J3UHV5T4.cjs → chunk-FGXJPJ3W.cjs} +4 -4
- package/dist/{chunk-IJU6AS67.cjs → chunk-FHR2AVMA.cjs} +23 -23
- package/dist/{chunk-LMB555QB.cjs → chunk-FOM36K5I.cjs} +18 -18
- package/dist/{chunk-JBYBMBMW.js → chunk-FWKEY4LD.js} +3 -2
- package/dist/{chunk-ZRPALBQB.cjs → chunk-GGH3ZYDB.cjs} +5 -5
- package/dist/{chunk-2BYAUHBO.js → chunk-GI5YYZH6.js} +3 -3
- package/dist/{chunk-VTEXJOSD.cjs → chunk-GYJLP4BO.cjs} +30 -30
- package/dist/{chunk-OC6GV5QH.cjs → chunk-HCONR4J6.cjs} +38 -38
- package/dist/{chunk-B3CWTCK2.js → chunk-IA7FCSRY.js} +27 -2
- package/dist/{chunk-5FYMCVBY.js → chunk-IXFNA5TT.js} +4 -4
- package/dist/{chunk-G6KACNMO.js → chunk-JCVSCKUH.js} +4 -4
- package/dist/{chunk-FHVPN7BN.js → chunk-JQCXLKAU.js} +3 -3
- package/dist/{chunk-F3YT6GUF.cjs → chunk-KLM6QD6D.cjs} +14 -14
- package/dist/{chunk-UGQ3QNV6.js → chunk-KOGPVAP6.js} +24 -8
- package/dist/{chunk-CKV5NYAE.js → chunk-KYJD2UMV.js} +4 -4
- package/dist/{chunk-LB3ZZMKT.cjs → chunk-L4DBUX3N.cjs} +10 -10
- package/dist/{chunk-RXQNRZ5F.js → chunk-LGRKBTPM.js} +3 -3
- package/dist/{chunk-OMNHTCS4.cjs → chunk-LHUDAJQT.cjs} +3 -2
- package/dist/{chunk-PDXX5UI4.cjs → chunk-LQNL3IYV.cjs} +38 -38
- package/dist/{chunk-WWNCM473.js → chunk-M7XR3F53.js} +4 -4
- package/dist/{chunk-IFW5IW4L.cjs → chunk-ML4V5PVU.cjs} +670 -627
- package/dist/{chunk-656IAS7B.cjs → chunk-N7IYRAFG.cjs} +21 -14
- package/dist/chunk-N7MIIDLI.cjs +1605 -0
- package/dist/{chunk-USOUXQYD.js → chunk-NWKARJM2.js} +130 -22
- package/dist/{chunk-VVAAZITA.cjs → chunk-OOP4S3GU.cjs} +5 -5
- package/dist/{chunk-TX6ZBIF4.js → chunk-ORBVCOTC.js} +3 -3
- package/dist/{chunk-RRQIAJLQ.cjs → chunk-OSKGCXAQ.cjs} +5 -5
- package/dist/{chunk-QEDMOGF3.cjs → chunk-P65OIMJT.cjs} +7 -7
- package/dist/{chunk-QM7FJB5W.js → chunk-PSWCJQS4.js} +4 -4
- package/dist/{chunk-7N63JHUN.cjs → chunk-PZ46WRHX.cjs} +8 -8
- package/dist/{chunk-AATYYLYK.js → chunk-Q4J2K7FI.js} +16 -5
- package/dist/chunk-QLPRI3QW.js +1875 -0
- package/dist/{chunk-WTH23ISI.js → chunk-R5VKLUMR.js} +4 -4
- package/dist/{chunk-NQN5LFN4.cjs → chunk-REN3LHO6.cjs} +19 -19
- package/dist/{chunk-JEJ7TT54.js → chunk-RVN7OPCP.js} +4 -4
- package/dist/{chunk-ZK7KX5CD.cjs → chunk-SE2Y5BSL.cjs} +6 -6
- package/dist/{chunk-2X3IPCBK.cjs → chunk-SKVBUGMC.cjs} +7 -7
- package/dist/{chunk-GZPUA2TL.cjs → chunk-SNMQUWIO.cjs} +10 -10
- package/dist/{chunk-OIBRWLPY.cjs → chunk-TKDDIE3T.cjs} +54 -3
- package/dist/{chunk-5H5GFBMG.cjs → chunk-TMNAKNV2.cjs} +112 -55
- package/dist/{chunk-5LVFTGKH.js → chunk-TZQKJOPC.js} +3 -3
- package/dist/{chunk-TSTFYGGX.js → chunk-UKEK2NZJ.js} +3 -3
- package/dist/{chunk-3IUHTGDD.cjs → chunk-VA7THINF.cjs} +38 -38
- package/dist/{chunk-Z557DVYN.js → chunk-VMCR5QVI.js} +4 -4
- package/dist/{chunk-IS7NTKY5.cjs → chunk-VTFLKSJL.cjs} +14 -14
- package/dist/{chunk-VO52AJD5.js → chunk-VXHB6ZHY.js} +4 -4
- package/dist/{chunk-S4O7CFHQ.cjs → chunk-VZI7S7P6.cjs} +7 -7
- package/dist/{chunk-BQJ3YBF3.js → chunk-W4AJMMQR.js} +667 -628
- package/dist/{chunk-2KKOYZYI.cjs → chunk-XZC75AJ4.cjs} +5 -5
- package/dist/{chunk-FWCHPCD4.js → chunk-Y2OCKXVD.js} +4 -4
- package/dist/{chunk-EI5G5JLY.cjs → chunk-YBM34FUJ.cjs} +134 -22
- package/dist/{chunk-DUG3OQAS.js → chunk-YDSNDCV3.js} +3 -3
- package/dist/{chunk-566ZB4LZ.cjs → chunk-YM7UUYR5.cjs} +5 -5
- package/dist/{chunk-BCP4E7MD.cjs → chunk-YMI3ED3B.cjs} +26 -10
- package/dist/{chunk-CP7V3ATL.js → chunk-Z7NUL73U.js} +51 -4
- package/dist/cli/holoscript-runner.cjs +11 -11
- package/dist/cli/holoscript-runner.js +4 -4
- package/dist/compiler/agent-inference.cjs +8 -8
- package/dist/compiler/agent-inference.js +4 -4
- package/dist/compiler/android-xr.cjs +7 -7
- package/dist/compiler/android-xr.js +5 -5
- package/dist/compiler/android.cjs +6 -6
- package/dist/compiler/android.js +4 -4
- package/dist/compiler/context.cjs +6 -6
- package/dist/compiler/context.js +3 -3
- package/dist/compiler/daimon-seed.cjs +9 -9
- package/dist/compiler/daimon-seed.js +4 -4
- package/dist/compiler/domain-block-utils.cjs +165 -165
- package/dist/compiler/domain-block-utils.js +4 -4
- package/dist/compiler/dtdl.cjs +7 -7
- package/dist/compiler/dtdl.js +4 -4
- package/dist/compiler/gltf-pipeline.cjs +8 -8
- package/dist/compiler/gltf-pipeline.js +1 -1
- package/dist/compiler/godot.cjs +6 -6
- package/dist/compiler/godot.js +5 -5
- package/dist/compiler/incremental.cjs +8 -8
- package/dist/compiler/incremental.js +3 -3
- package/dist/compiler/index.cjs +871 -207
- package/dist/compiler/index.d.ts +21 -1
- package/dist/compiler/index.js +696 -60
- package/dist/compiler/ios.cjs +6 -6
- package/dist/compiler/ios.js +4 -4
- package/dist/compiler/llm-provider-capabilities.cjs +6 -6
- package/dist/compiler/llm-provider-capabilities.js +3 -3
- package/dist/compiler/openxr.cjs +6 -6
- package/dist/compiler/openxr.js +5 -5
- package/dist/compiler/sdf.cjs +7 -7
- package/dist/compiler/sdf.js +5 -5
- package/dist/compiler/state.cjs +6 -6
- package/dist/compiler/state.js +4 -4
- package/dist/compiler/trait-composition.cjs +7 -7
- package/dist/compiler/trait-composition.js +3 -3
- package/dist/compiler/unity.cjs +6 -6
- package/dist/compiler/unity.js +5 -5
- package/dist/compiler/unreal.cjs +7 -7
- package/dist/compiler/unreal.js +5 -5
- package/dist/compiler/urdf.cjs +14 -14
- package/dist/compiler/urdf.js +5 -5
- package/dist/compiler/usd-physics.cjs +7 -7
- package/dist/compiler/usd-physics.js +4 -4
- package/dist/compiler/visionos.cjs +6 -6
- package/dist/compiler/visionos.js +5 -5
- package/dist/compiler/vrchat.cjs +7 -7
- package/dist/compiler/vrchat.js +5 -5
- package/dist/compiler/wasm.cjs +10 -10
- package/dist/compiler/wasm.js +5 -5
- package/dist/compiler/webgpu.cjs +6 -6
- package/dist/compiler/webgpu.js +5 -5
- package/dist/constants.cjs +3 -3
- package/dist/constants.js +1 -1
- package/dist/entries/scripting.cjs +6 -6
- package/dist/entries/scripting.js +2 -2
- package/dist/evolution/index.cjs +21 -17
- package/dist/evolution/index.d.ts +9 -0
- package/dist/evolution/index.js +6 -6
- package/dist/index.cjs +930 -703
- package/dist/index.d.ts +70 -5
- package/dist/index.js +192 -109
- package/dist/parser.cjs +19 -19
- package/dist/parser.js +5 -5
- package/dist/reconstruction/index.cjs +127 -38
- package/dist/reconstruction/index.d.ts +159 -0
- package/dist/reconstruction/index.js +11 -2
- package/dist/runtime.d.ts +1 -0
- package/dist/traitDocs/traitDocs.cjs +8 -8
- package/dist/traitDocs/traitDocs.js +2 -2
- package/dist/traits/index.cjs +566 -471
- package/dist/traits/index.js +93 -11
- package/package.json +25 -41
- package/dist/chunk-G3BM3CT4.cjs +0 -639
- package/dist/chunk-KEYG7HGY.js +0 -1062
- package/dist/chunk-QX34ELJ7.js +0 -625
- package/dist/chunk-W2GA3EQB.cjs +0 -1065
|
@@ -0,0 +1,1884 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkP65OIMJT_cjs = require('./chunk-P65OIMJT.cjs');
|
|
4
|
+
var chunkSE2Y5BSL_cjs = require('./chunk-SE2Y5BSL.cjs');
|
|
5
|
+
var chunkZHPMP447_cjs = require('./chunk-ZHPMP447.cjs');
|
|
6
|
+
var ans = require('@holoscript/core-types/ans');
|
|
7
|
+
|
|
8
|
+
// src/compiler/render-modules/geometry-registry.ts
|
|
9
|
+
function resolveGeometry(meshType) {
|
|
10
|
+
const key = String(meshType ?? "").trim().toLowerCase();
|
|
11
|
+
const kind = ALIAS[key] ?? "box";
|
|
12
|
+
return PRIMITIVE[kind];
|
|
13
|
+
}
|
|
14
|
+
var PRIMITIVE, ALIAS;
|
|
15
|
+
var init_geometry_registry = chunkZHPMP447_cjs.__esm({
|
|
16
|
+
"src/compiler/render-modules/geometry-registry.ts"() {
|
|
17
|
+
PRIMITIVE = {
|
|
18
|
+
box: { kind: "box", webgpuVertexFn: "generateCubeVertices(1.0)" },
|
|
19
|
+
sphere: { kind: "sphere", webgpuVertexFn: "generateSphereVertices(0.5, 16, 16)" },
|
|
20
|
+
plane: { kind: "plane", webgpuVertexFn: "generatePlaneVertices(1.0, 1.0)" },
|
|
21
|
+
cylinder: { kind: "cylinder", webgpuVertexFn: "generateCylinderVertices(0.5, 1.0, 16)" },
|
|
22
|
+
cone: { kind: "cone", webgpuVertexFn: "generateConeVertices(0.5, 1.0, 16)" },
|
|
23
|
+
torus: { kind: "torus", webgpuVertexFn: "generateTorusVertices(0.5, 0.15, 16, 32)" }
|
|
24
|
+
};
|
|
25
|
+
ALIAS = {
|
|
26
|
+
// box family (panels, rounded boxes read as boxes)
|
|
27
|
+
cube: "box",
|
|
28
|
+
box: "box",
|
|
29
|
+
rounded_panel: "box",
|
|
30
|
+
rounded_box: "box",
|
|
31
|
+
panel: "box",
|
|
32
|
+
card: "box",
|
|
33
|
+
// sphere family
|
|
34
|
+
sphere: "sphere",
|
|
35
|
+
orb: "sphere",
|
|
36
|
+
ball: "sphere",
|
|
37
|
+
bubble: "sphere",
|
|
38
|
+
procedural_upper_body: "sphere",
|
|
39
|
+
avatar: "sphere",
|
|
40
|
+
// plane / surface family
|
|
41
|
+
plane: "plane",
|
|
42
|
+
quad: "plane",
|
|
43
|
+
surface: "plane",
|
|
44
|
+
full_screen_fluid_plane: "plane",
|
|
45
|
+
fluid_plane: "plane",
|
|
46
|
+
water: "plane",
|
|
47
|
+
// cylinder family
|
|
48
|
+
cylinder: "cylinder",
|
|
49
|
+
tube: "cylinder",
|
|
50
|
+
pillar: "cylinder",
|
|
51
|
+
capsule: "cylinder",
|
|
52
|
+
// cone family
|
|
53
|
+
cone: "cone",
|
|
54
|
+
pyramid: "cone",
|
|
55
|
+
spike: "cone",
|
|
56
|
+
crystal: "cone",
|
|
57
|
+
diamond: "cone",
|
|
58
|
+
gem: "cone",
|
|
59
|
+
// torus family
|
|
60
|
+
torus: "torus",
|
|
61
|
+
ring: "torus",
|
|
62
|
+
donut: "torus"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/compiler/render-modules/skybox-registry.ts
|
|
68
|
+
function hexToRgb(hex) {
|
|
69
|
+
const h = hex.replace("#", "");
|
|
70
|
+
return [
|
|
71
|
+
Number((parseInt(h.substring(0, 2), 16) / 255).toFixed(4)),
|
|
72
|
+
Number((parseInt(h.substring(2, 4), 16) / 255).toFixed(4)),
|
|
73
|
+
Number((parseInt(h.substring(4, 6), 16) / 255).toFixed(4))
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
function resolveSkyboxColor(name) {
|
|
77
|
+
const key = String(name ?? "").trim().toLowerCase();
|
|
78
|
+
const hex = SKYBOX_HEX[key];
|
|
79
|
+
return hex ? hexToRgb(hex) : FALLBACK;
|
|
80
|
+
}
|
|
81
|
+
var SKYBOX_HEX, FALLBACK;
|
|
82
|
+
var init_skybox_registry = chunkZHPMP447_cjs.__esm({
|
|
83
|
+
"src/compiler/render-modules/skybox-registry.ts"() {
|
|
84
|
+
SKYBOX_HEX = {
|
|
85
|
+
// ── deep / space / void ─────────────────────────────────────────────
|
|
86
|
+
deep_machine_ocean: "#052430",
|
|
87
|
+
deep_space: "#05060d",
|
|
88
|
+
space: "#05060d",
|
|
89
|
+
nebula: "#0b0618",
|
|
90
|
+
night_sky: "#0b1020",
|
|
91
|
+
night_overcast: "#14161c",
|
|
92
|
+
void: "#050507",
|
|
93
|
+
midnight: "#070a12",
|
|
94
|
+
// ── water / underwater ──────────────────────────────────────────────
|
|
95
|
+
ocean: "#063240",
|
|
96
|
+
underwater: "#062a3a",
|
|
97
|
+
// ── studio / neutral (light backdrops kept light, just not pure white) ─
|
|
98
|
+
studio: "#c4c8cf",
|
|
99
|
+
neutral_studio: "#c4c8cf",
|
|
100
|
+
studio_soft: "#d2d5da",
|
|
101
|
+
hdr_studio: "#c9ccd2",
|
|
102
|
+
gallery: "#20222a",
|
|
103
|
+
indoor_museum: "#1c1e24",
|
|
104
|
+
interior_dark: "#14151a",
|
|
105
|
+
classroom: "#d2cfc6",
|
|
106
|
+
warehouse: "#3a3c40",
|
|
107
|
+
asset_pipeline_warehouse: "#3a3c40",
|
|
108
|
+
// ── sky / daylight ──────────────────────────────────────────────────
|
|
109
|
+
procedural_sky: "#8fbce6",
|
|
110
|
+
clear_sky: "#8fbce6",
|
|
111
|
+
daylight: "#9ec6ea",
|
|
112
|
+
fantasy_sky: "#2a3a6a",
|
|
113
|
+
sunset: "#46283a",
|
|
114
|
+
dusk: "#2e2740",
|
|
115
|
+
desert_dawn: "#caa079",
|
|
116
|
+
// ── tech / abstract ─────────────────────────────────────────────────
|
|
117
|
+
digital_grid: "#0a0f1e",
|
|
118
|
+
gradient: "#1a2030",
|
|
119
|
+
// ── environments / scenes ───────────────────────────────────────────
|
|
120
|
+
industrial: "#2a2d30",
|
|
121
|
+
urban_courtyard: "#3a3d42",
|
|
122
|
+
ancient_ruins: "#3a3428",
|
|
123
|
+
concert_venue: "#0e0a14",
|
|
124
|
+
lounge: "#1a1220",
|
|
125
|
+
farm: "#7fae5a",
|
|
126
|
+
vault_archive: "#1a140a",
|
|
127
|
+
passthrough: "#0c0c0e",
|
|
128
|
+
default: "#0a0e14"
|
|
129
|
+
};
|
|
130
|
+
FALLBACK = hexToRgb("#0a0e14");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// src/compiler/render-modules/geometry-purpose.ts
|
|
135
|
+
function coerceBool(v) {
|
|
136
|
+
if (v === true || v === "true") return true;
|
|
137
|
+
if (v === false || v === "false") return false;
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
function resolveGeometryRole(opts) {
|
|
141
|
+
let purpose = "render";
|
|
142
|
+
const p = String(opts.purpose ?? "").trim().toLowerCase();
|
|
143
|
+
if (p && ALL_PURPOSES.has(p)) {
|
|
144
|
+
purpose = p;
|
|
145
|
+
} else {
|
|
146
|
+
for (const t of opts.traitNames ?? []) {
|
|
147
|
+
const mapped = PURPOSE_TRAITS[String(t).trim().toLowerCase()];
|
|
148
|
+
if (mapped) {
|
|
149
|
+
purpose = mapped;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const explicit = coerceBool(opts.visible);
|
|
155
|
+
const visible = explicit !== void 0 ? explicit : purpose === "render";
|
|
156
|
+
return { purpose, visible };
|
|
157
|
+
}
|
|
158
|
+
var INVISIBLE_PURPOSES, PURPOSE_TRAITS, ALL_PURPOSES;
|
|
159
|
+
var init_geometry_purpose = chunkZHPMP447_cjs.__esm({
|
|
160
|
+
"src/compiler/render-modules/geometry-purpose.ts"() {
|
|
161
|
+
INVISIBLE_PURPOSES = /* @__PURE__ */ new Set([
|
|
162
|
+
"collision",
|
|
163
|
+
"trigger",
|
|
164
|
+
"vfx_emitter",
|
|
165
|
+
"sim_domain",
|
|
166
|
+
"navmesh",
|
|
167
|
+
"occluder",
|
|
168
|
+
"audio_zone"
|
|
169
|
+
]);
|
|
170
|
+
PURPOSE_TRAITS = {
|
|
171
|
+
collider: "collision",
|
|
172
|
+
collision: "collision",
|
|
173
|
+
physics_collider: "collision",
|
|
174
|
+
rigidbody_collider: "collision",
|
|
175
|
+
trigger: "trigger",
|
|
176
|
+
trigger_volume: "trigger",
|
|
177
|
+
interaction_volume: "trigger",
|
|
178
|
+
emitter: "vfx_emitter",
|
|
179
|
+
vfx_emitter: "vfx_emitter",
|
|
180
|
+
particle_emitter: "vfx_emitter",
|
|
181
|
+
spawn_volume: "vfx_emitter",
|
|
182
|
+
sim_domain: "sim_domain",
|
|
183
|
+
sim_volume: "sim_domain",
|
|
184
|
+
fluid_domain: "sim_domain",
|
|
185
|
+
field_region: "sim_domain",
|
|
186
|
+
navmesh: "navmesh",
|
|
187
|
+
nav_mesh: "navmesh",
|
|
188
|
+
occluder: "occluder",
|
|
189
|
+
occlusion: "occluder",
|
|
190
|
+
audio_zone: "audio_zone",
|
|
191
|
+
audio_volume: "audio_zone"
|
|
192
|
+
};
|
|
193
|
+
ALL_PURPOSES = /* @__PURE__ */ new Set([
|
|
194
|
+
"render",
|
|
195
|
+
...Array.from(INVISIBLE_PURPOSES)
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// src/compiler/WebGPUCompiler.ts
|
|
201
|
+
var WebGPUCompiler_exports = {};
|
|
202
|
+
chunkZHPMP447_cjs.__export(WebGPUCompiler_exports, {
|
|
203
|
+
WebGPUCompiler: () => exports.WebGPUCompiler
|
|
204
|
+
});
|
|
205
|
+
function isGpuParticleTraitName(name) {
|
|
206
|
+
return typeof name === "string" && (name === "gpu_particle" || name.startsWith("vfx_particle_"));
|
|
207
|
+
}
|
|
208
|
+
function findGpuParticleTrait(traits) {
|
|
209
|
+
return traits?.find((t) => isGpuParticleTraitName(t.name));
|
|
210
|
+
}
|
|
211
|
+
exports.WebGPUCompiler = void 0;
|
|
212
|
+
var init_WebGPUCompiler = chunkZHPMP447_cjs.__esm({
|
|
213
|
+
"src/compiler/WebGPUCompiler.ts"() {
|
|
214
|
+
chunkSE2Y5BSL_cjs.init_CompilerBase();
|
|
215
|
+
init_geometry_registry();
|
|
216
|
+
init_skybox_registry();
|
|
217
|
+
init_geometry_purpose();
|
|
218
|
+
chunkP65OIMJT_cjs.init_DomainBlockCompilerMixin();
|
|
219
|
+
exports.WebGPUCompiler = class extends chunkSE2Y5BSL_cjs.CompilerBase {
|
|
220
|
+
constructor(options = {}) {
|
|
221
|
+
super();
|
|
222
|
+
this.compilerName = "WebGPUCompiler";
|
|
223
|
+
this.lines = [];
|
|
224
|
+
this.indentLevel = 0;
|
|
225
|
+
this.objectIndex = 0;
|
|
226
|
+
// Resolved once per compile() so findObjProp can fall back to template defaults.
|
|
227
|
+
this.templatesByName = /* @__PURE__ */ new Map();
|
|
228
|
+
// True when any object is a water/fluid surface — gates the animated water
|
|
229
|
+
// pipeline, its WGSL, the tessellated-plane helper, and the shared time uniform.
|
|
230
|
+
this.hasWater = false;
|
|
231
|
+
/** World facts collected at emission time, serialized as the HoloScene Manifest. */
|
|
232
|
+
this.manifestObjects = [];
|
|
233
|
+
/** Accumulated spatial-group translation for the objects currently being
|
|
234
|
+
* emitted — manifest positions are WORLD-space (render applies the same
|
|
235
|
+
* offsets via GroupXform; local coords would be a false fact for grouped
|
|
236
|
+
* scenes). */
|
|
237
|
+
this.manifestGroupOffset = [0, 0, 0];
|
|
238
|
+
// The named channels the water surface's clarity/ripple are bound to (from the
|
|
239
|
+
// .holo's water_clarity_binding / ripple_intensity_binding props). Host state
|
|
240
|
+
// (e.g. Brittney receipts) drives these BY NAME via hsSetChannel.
|
|
241
|
+
this.waterClarityChannel = "environmentWaterClarity";
|
|
242
|
+
this.waterRippleChannel = "environmentRippleIntensity";
|
|
243
|
+
this.options = {
|
|
244
|
+
entryPoint: options.entryPoint || "main",
|
|
245
|
+
enableCompute: options.enableCompute ?? true,
|
|
246
|
+
msaa: options.msaa || 4,
|
|
247
|
+
indent: options.indent || " ",
|
|
248
|
+
provenanceHash: options.provenanceHash ?? ""
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
getRequiredCapability() {
|
|
252
|
+
return ans.ANSCapabilityPath.WEBGPU;
|
|
253
|
+
}
|
|
254
|
+
compile(composition, agentToken, outputPath) {
|
|
255
|
+
this.validateCompilerAccess(agentToken, outputPath);
|
|
256
|
+
this.lines = [];
|
|
257
|
+
this.indentLevel = 0;
|
|
258
|
+
this.objectIndex = 0;
|
|
259
|
+
this.manifestObjects = [];
|
|
260
|
+
this.manifestGroupOffset = [0, 0, 0];
|
|
261
|
+
this.templatesByName = new Map(
|
|
262
|
+
(composition.templates ?? []).map((t) => [t.name, t])
|
|
263
|
+
);
|
|
264
|
+
this.hasWater = this.compositionHasWater(composition);
|
|
265
|
+
if (this.hasWater) this.resolveWaterChannels(composition);
|
|
266
|
+
this.emit("// Auto-generated by HoloScript WebGPUCompiler");
|
|
267
|
+
this.emit(
|
|
268
|
+
`// Source: composition "${this.escapeStringValue(composition.name, "TypeScript")}"`
|
|
269
|
+
);
|
|
270
|
+
this.emit("// Do not edit manually \u2014 regenerate from .holo source");
|
|
271
|
+
if (this.options.provenanceHash) {
|
|
272
|
+
this.emit(`// Provenance Hash: ${this.options.provenanceHash}`);
|
|
273
|
+
}
|
|
274
|
+
this.emit("");
|
|
275
|
+
this.emitDeviceInit();
|
|
276
|
+
this.emitGeometryHelpers();
|
|
277
|
+
this.emitShaderSources();
|
|
278
|
+
this.emitEnvironment(composition.environment ?? { type: "Environment", properties: [] });
|
|
279
|
+
this.emitCamera(composition);
|
|
280
|
+
this.emitSceneLights(composition);
|
|
281
|
+
if (this.hasWater) {
|
|
282
|
+
const cc = JSON.stringify(this.waterClarityChannel);
|
|
283
|
+
const rc = JSON.stringify(this.waterRippleChannel);
|
|
284
|
+
this.emit(`const hsChannels = { [${cc}]: 1.0, [${rc}]: 0.0 };`);
|
|
285
|
+
this.emit("let hsWaterAmp = 1.0;");
|
|
286
|
+
this.emit(
|
|
287
|
+
"function hsSetChannel(name, value) { if (name in hsChannels) hsChannels[name] = Math.max(0, Math.min(1, value)); return hsChannels[name]; }"
|
|
288
|
+
);
|
|
289
|
+
this.emit(
|
|
290
|
+
`function hsSetWater(o) { if (o) { if (o.clarity !== undefined) hsSetChannel(${cc}, o.clarity); if (o.ripple !== undefined) hsSetChannel(${rc}, o.ripple); if (o.amplitude !== undefined) hsWaterAmp = Math.max(0, o.amplitude); } return { clarity: hsChannels[${cc}], ripple: hsChannels[${rc}], amplitude: hsWaterAmp }; }`
|
|
291
|
+
);
|
|
292
|
+
this.emit(
|
|
293
|
+
'if (typeof window !== "undefined") { window.hsSetChannel = hsSetChannel; window.hsSetWater = hsSetWater; }'
|
|
294
|
+
);
|
|
295
|
+
this.emit(
|
|
296
|
+
`const waterFrameU = createBuffer(device, new Float32Array([0,0,0,0, hsChannels[${cc}], hsChannels[${rc}], hsWaterAmp, 0]), GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);`
|
|
297
|
+
);
|
|
298
|
+
this.emit("");
|
|
299
|
+
}
|
|
300
|
+
this.emit("const holoGraphObjects = [];");
|
|
301
|
+
this.emit("");
|
|
302
|
+
if (composition.objects) {
|
|
303
|
+
for (const obj of composition.objects) this.emitObject(obj);
|
|
304
|
+
}
|
|
305
|
+
if (composition.spatialGroups) {
|
|
306
|
+
for (const group of composition.spatialGroups) this.emitGroup(group);
|
|
307
|
+
}
|
|
308
|
+
this.emitSceneManifest(composition);
|
|
309
|
+
if (this.options.enableCompute) this.emitComputeShaders(composition);
|
|
310
|
+
this.emitWebGPUDomainBlocks(composition);
|
|
311
|
+
this.emitViewportRuntime();
|
|
312
|
+
this.emitRenderLoop(composition);
|
|
313
|
+
return this.lines.join("\n");
|
|
314
|
+
}
|
|
315
|
+
emitWebGPUDomainBlocks(composition) {
|
|
316
|
+
const domainBlocks = composition.domainBlocks ?? [];
|
|
317
|
+
if (domainBlocks.length === 0) return;
|
|
318
|
+
this.emit("");
|
|
319
|
+
this.emit("// === v4.2 Domain Blocks ===");
|
|
320
|
+
for (let i = 0; i < domainBlocks.length; i++) {
|
|
321
|
+
const block = domainBlocks[i];
|
|
322
|
+
const prefix = this.domainBlockVar(block, i);
|
|
323
|
+
switch (block.domain) {
|
|
324
|
+
case "material":
|
|
325
|
+
this.emitMaterialDomainBlock(block, prefix);
|
|
326
|
+
break;
|
|
327
|
+
case "dataviz":
|
|
328
|
+
this.emitMultiline(chunkP65OIMJT_cjs.datavizToWebGPU(chunkP65OIMJT_cjs.compileDataVizBlock(block), prefix));
|
|
329
|
+
break;
|
|
330
|
+
case "vfx":
|
|
331
|
+
this.emitParticleDomainBlock(block, prefix);
|
|
332
|
+
break;
|
|
333
|
+
case "postfx":
|
|
334
|
+
this.emitPostFXDomainBlock(block, prefix);
|
|
335
|
+
break;
|
|
336
|
+
case "procedural":
|
|
337
|
+
this.emitProceduralDomainBlock(block, prefix);
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
this.emitDomainMetadataBlock(block, prefix);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
this.emit("");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
emitMaterialDomainBlock(block, prefix) {
|
|
347
|
+
const mat = chunkP65OIMJT_cjs.compileMaterialBlock(block);
|
|
348
|
+
this.emitMultiline(chunkP65OIMJT_cjs.materialToWebGPU(mat, prefix));
|
|
349
|
+
if (Object.keys(mat.textureMaps).length > 0) {
|
|
350
|
+
this.emit(`const ${prefix}TextureMaps = ${this.json(mat.textureMaps)};`);
|
|
351
|
+
this.emit(`const ${prefix}Textures = await loadMaterialTextureSet(${prefix}TextureMaps);`);
|
|
352
|
+
this.emit(
|
|
353
|
+
`const ${prefix}TextureReceipt = { name: "${this.escapeStringValue(mat.name, "TypeScript")}", maps: Object.keys(${prefix}TextureMaps), loaded: Object.values(${prefix}Textures).filter((t) => t.loaded).length };`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
emitParticleDomainBlock(block, prefix) {
|
|
358
|
+
const ps = chunkP65OIMJT_cjs.compileParticleBlock(block);
|
|
359
|
+
const count = Math.max(
|
|
360
|
+
1,
|
|
361
|
+
Math.floor(
|
|
362
|
+
this.numberProp(block, ["max_particles", "maxParticles", "count", "rate"], 512)
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
const seed = this.hashSeed(`${ps.name}:${ps.keyword}:${count}`);
|
|
366
|
+
this.emit(`// Domain particles: ${this.escapeStringValue(ps.name, "TypeScript")}`);
|
|
367
|
+
this.emit(`const ${prefix}ParticleConfig = ${this.json({
|
|
368
|
+
name: ps.name,
|
|
369
|
+
keyword: ps.keyword,
|
|
370
|
+
traits: ps.traits,
|
|
371
|
+
properties: ps.properties,
|
|
372
|
+
modules: ps.modules
|
|
373
|
+
})};`);
|
|
374
|
+
this.emit(`const ${prefix}ParticleCount = ${count};`);
|
|
375
|
+
this.emit(`const ${prefix}ParticleData = makeParticleCloud(${prefix}ParticleCount, ${seed});`);
|
|
376
|
+
this.emit(
|
|
377
|
+
`const ${prefix}ParticleBuf = createBuffer(device, ${prefix}ParticleData, GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE);`
|
|
378
|
+
);
|
|
379
|
+
this.emit(`const ${prefix}ParticlePipeline = device.createRenderPipeline({`);
|
|
380
|
+
this.indent();
|
|
381
|
+
this.emit('layout: "auto",');
|
|
382
|
+
this.emit(
|
|
383
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_PARTICLE_RENDER }), entryPoint: "vs_particle",'
|
|
384
|
+
);
|
|
385
|
+
this.emit(
|
|
386
|
+
' buffers: [{ arrayStride: 32, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }, { shaderLocation: 2, offset: 24, format: "float32" }] }] },'
|
|
387
|
+
);
|
|
388
|
+
this.emit(
|
|
389
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_PARTICLE_RENDER }), entryPoint: "fs_particle",'
|
|
390
|
+
);
|
|
391
|
+
this.emit(
|
|
392
|
+
' targets: [{ format, blend: { color: { srcFactor: "src-alpha", dstFactor: "one", operation: "add" }, alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" } } }] },'
|
|
393
|
+
);
|
|
394
|
+
this.emit(
|
|
395
|
+
'primitive: { topology: "point-list" }, depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "less" },'
|
|
396
|
+
);
|
|
397
|
+
this.dedent();
|
|
398
|
+
this.emit("});");
|
|
399
|
+
}
|
|
400
|
+
emitPostFXDomainBlock(block, prefix) {
|
|
401
|
+
const pp = chunkP65OIMJT_cjs.compilePostProcessingBlock(block);
|
|
402
|
+
this.emit(`// Domain post-processing: ${this.escapeStringValue(pp.name ?? block.name, "TypeScript")}`);
|
|
403
|
+
this.emit(`const ${prefix}PostFXConfig = ${this.json({
|
|
404
|
+
name: pp.name,
|
|
405
|
+
keyword: pp.keyword,
|
|
406
|
+
effects: pp.effects
|
|
407
|
+
})};`);
|
|
408
|
+
this.emit(`const ${prefix}PostFXPipeline = device.createRenderPipeline({`);
|
|
409
|
+
this.indent();
|
|
410
|
+
this.emit('layout: "auto",');
|
|
411
|
+
this.emit(
|
|
412
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_POSTFX }), entryPoint: "vs_postfx" },'
|
|
413
|
+
);
|
|
414
|
+
this.emit(
|
|
415
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_POSTFX }), entryPoint: "fs_postfx", targets: [{ format, blend: { color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } } }] },'
|
|
416
|
+
);
|
|
417
|
+
this.emit('primitive: { topology: "triangle-list" }, multisample: { count: MSAA_COUNT },');
|
|
418
|
+
this.dedent();
|
|
419
|
+
this.emit("});");
|
|
420
|
+
}
|
|
421
|
+
emitProceduralDomainBlock(block, prefix) {
|
|
422
|
+
const proc = chunkP65OIMJT_cjs.compileProceduralBlock(block);
|
|
423
|
+
this.emit(`// Domain procedural: ${this.escapeStringValue(proc.name, "TypeScript")}`);
|
|
424
|
+
this.emit(`const ${prefix}ProceduralConfig = ${this.json({
|
|
425
|
+
name: proc.name,
|
|
426
|
+
keyword: proc.keyword,
|
|
427
|
+
genType: proc.genType,
|
|
428
|
+
seed: proc.seed,
|
|
429
|
+
density: proc.density,
|
|
430
|
+
scaleRange: proc.scaleRange,
|
|
431
|
+
noise: proc.noise,
|
|
432
|
+
sourceMesh: proc.sourceMesh,
|
|
433
|
+
traits: proc.traits,
|
|
434
|
+
properties: proc.properties
|
|
435
|
+
})};`);
|
|
436
|
+
this.emit(
|
|
437
|
+
`const ${prefix}ProceduralUniform = createBuffer(device, new Float32Array([${proc.seed ?? 1}, ${proc.density ?? 1}, ${this.numberProp(block, ["count"], 1)}, 0]), GPUBufferUsage.UNIFORM);`
|
|
438
|
+
);
|
|
439
|
+
if (block.keyword === "scatter" || block.keyword === "distribute" || block.properties?.count) {
|
|
440
|
+
const count = Math.max(1, Math.floor(this.numberProp(block, ["count"], 128)));
|
|
441
|
+
this.emit(`const ${prefix}ScatterInstanceCount = ${count};`);
|
|
442
|
+
this.emit(`const ${prefix}ScatterVertices = generateConeVertices(0.08, 0.36, 8);`);
|
|
443
|
+
this.emit(`const ${prefix}ScatterVBO = createBuffer(device, ${prefix}ScatterVertices, GPUBufferUsage.VERTEX);`);
|
|
444
|
+
this.emit(`const ${prefix}ScatterVertexCount = ${prefix}ScatterVertices.length / 6;`);
|
|
445
|
+
this.emit(`const ${prefix}ScatterPipeline = device.createRenderPipeline({`);
|
|
446
|
+
this.indent();
|
|
447
|
+
this.emit('layout: "auto",');
|
|
448
|
+
this.emit(
|
|
449
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_SCATTER }), entryPoint: "vs_scatter",'
|
|
450
|
+
);
|
|
451
|
+
this.emit(
|
|
452
|
+
' buffers: [{ arrayStride: 24, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }] }] },'
|
|
453
|
+
);
|
|
454
|
+
this.emit(
|
|
455
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_SCATTER }), entryPoint: "fs_scatter", targets: [{ format }] },'
|
|
456
|
+
);
|
|
457
|
+
this.emit(
|
|
458
|
+
'primitive: { topology: "triangle-list", cullMode: "back" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" }, multisample: { count: MSAA_COUNT },'
|
|
459
|
+
);
|
|
460
|
+
this.dedent();
|
|
461
|
+
this.emit("});");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
emitDomainMetadataBlock(block, prefix) {
|
|
465
|
+
this.emit(`const ${prefix}DomainBlock = ${this.json({
|
|
466
|
+
domain: block.domain,
|
|
467
|
+
keyword: block.keyword,
|
|
468
|
+
name: block.name,
|
|
469
|
+
traits: block.traits,
|
|
470
|
+
properties: block.properties,
|
|
471
|
+
childCount: block.children?.length ?? 0
|
|
472
|
+
})};`);
|
|
473
|
+
}
|
|
474
|
+
// ─── Device Init ──────────────────────────────────────────────────────────
|
|
475
|
+
emitDeviceInit() {
|
|
476
|
+
this.emit("// === WebGPU Device Initialization ===");
|
|
477
|
+
this.emit('const canvas = document.querySelector("canvas") as HTMLCanvasElement;');
|
|
478
|
+
this.emit("const adapter = await navigator.gpu.requestAdapter();");
|
|
479
|
+
this.emit('if (!adapter) throw new Error("WebGPU not supported");');
|
|
480
|
+
this.emit("const device = await adapter.requestDevice();");
|
|
481
|
+
this.emit('document.body.dataset.holoscriptWebgpu = "device-ready";');
|
|
482
|
+
this.emit('const context = canvas.getContext("webgpu") as GPUCanvasContext;');
|
|
483
|
+
this.emit("const format = navigator.gpu.getPreferredCanvasFormat();");
|
|
484
|
+
this.emit('context.configure({ device, format, alphaMode: "premultiplied" });');
|
|
485
|
+
this.emit(`const MSAA_COUNT = ${this.options.msaa};`);
|
|
486
|
+
this.emit(
|
|
487
|
+
"const msaaTexture = device.createTexture({ size: [canvas.width, canvas.height], sampleCount: MSAA_COUNT, format, usage: GPUTextureUsage.RENDER_ATTACHMENT });"
|
|
488
|
+
);
|
|
489
|
+
this.emit(
|
|
490
|
+
'const depthTexture = device.createTexture({ size: [canvas.width, canvas.height], sampleCount: MSAA_COUNT, format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT });'
|
|
491
|
+
);
|
|
492
|
+
this.emit("");
|
|
493
|
+
this.emit(
|
|
494
|
+
"function createBuffer(device: GPUDevice, data: Float32Array, usage: number): GPUBuffer {"
|
|
495
|
+
);
|
|
496
|
+
this.indent();
|
|
497
|
+
this.emit(
|
|
498
|
+
"const buf = device.createBuffer({ size: data.byteLength, usage: usage | GPUBufferUsage.COPY_DST });"
|
|
499
|
+
);
|
|
500
|
+
this.emit("device.queue.writeBuffer(buf, 0, data); return buf;");
|
|
501
|
+
this.dedent();
|
|
502
|
+
this.emit("}");
|
|
503
|
+
this.emit("function createStorageBuffer(device: GPUDevice, size: number): GPUBuffer {");
|
|
504
|
+
this.indent();
|
|
505
|
+
this.emit(
|
|
506
|
+
"return device.createBuffer({ size, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });"
|
|
507
|
+
);
|
|
508
|
+
this.dedent();
|
|
509
|
+
this.emit("}");
|
|
510
|
+
this.emit("function createFallbackTexture(device, rgba) {");
|
|
511
|
+
this.indent();
|
|
512
|
+
this.emit('const tex = device.createTexture({ size: [1, 1, 1], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });');
|
|
513
|
+
this.emit("device.queue.writeTexture({ texture: tex }, new Uint8Array(rgba), { bytesPerRow: 4 }, [1, 1, 1]);");
|
|
514
|
+
this.emit("return tex;");
|
|
515
|
+
this.dedent();
|
|
516
|
+
this.emit("}");
|
|
517
|
+
this.emit("async function loadTexture2D(src) {");
|
|
518
|
+
this.indent();
|
|
519
|
+
this.emit('const sampler = device.createSampler({ magFilter: "linear", minFilter: "linear", mipmapFilter: "linear", addressModeU: "repeat", addressModeV: "repeat" });');
|
|
520
|
+
this.emit("try {");
|
|
521
|
+
this.indent();
|
|
522
|
+
this.emit("const resp = await fetch(src);");
|
|
523
|
+
this.emit("if (!resp.ok) throw new Error(`texture fetch ${resp.status}`);");
|
|
524
|
+
this.emit("const bitmap = await createImageBitmap(await resp.blob());");
|
|
525
|
+
this.emit('const texture = device.createTexture({ size: [bitmap.width, bitmap.height, 1], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT });');
|
|
526
|
+
this.emit("device.queue.copyExternalImageToTexture({ source: bitmap }, { texture }, [bitmap.width, bitmap.height]);");
|
|
527
|
+
this.emit("return { src, texture, view: texture.createView(), sampler, loaded: true };");
|
|
528
|
+
this.dedent();
|
|
529
|
+
this.emit("} catch {");
|
|
530
|
+
this.indent();
|
|
531
|
+
this.emit("const texture = createFallbackTexture(device, [255, 255, 255, 255]);");
|
|
532
|
+
this.emit("return { src, texture, view: texture.createView(), sampler, loaded: false };");
|
|
533
|
+
this.dedent();
|
|
534
|
+
this.emit("}");
|
|
535
|
+
this.dedent();
|
|
536
|
+
this.emit("}");
|
|
537
|
+
this.emit("async function loadMaterialTextureSet(maps) {");
|
|
538
|
+
this.indent();
|
|
539
|
+
this.emit("const entries = await Promise.all(Object.entries(maps).map(async ([kind, src]) => [kind, await loadTexture2D(src)]));");
|
|
540
|
+
this.emit("return Object.fromEntries(entries);");
|
|
541
|
+
this.dedent();
|
|
542
|
+
this.emit("}");
|
|
543
|
+
this.emit("const assetLoader = {");
|
|
544
|
+
this.indent();
|
|
545
|
+
this.emit("async load(src) {");
|
|
546
|
+
this.indent();
|
|
547
|
+
this.emit('return { src, kind: "external-model", loaded: false };');
|
|
548
|
+
this.dedent();
|
|
549
|
+
this.emit("},");
|
|
550
|
+
this.dedent();
|
|
551
|
+
this.emit("};");
|
|
552
|
+
this.emit("function makeParticleCloud(count, seed) {");
|
|
553
|
+
this.indent();
|
|
554
|
+
this.emit("let s = seed || 1;");
|
|
555
|
+
this.emit("const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };");
|
|
556
|
+
this.emit("const data = new Float32Array(count * 8);");
|
|
557
|
+
this.emit("for (let i = 0; i < count; i++) {");
|
|
558
|
+
this.indent();
|
|
559
|
+
this.emit("const o = i * 8;");
|
|
560
|
+
this.emit("data[o + 0] = rnd() * 1.8 - 0.9; data[o + 1] = rnd() * 1.2 - 0.6; data[o + 2] = rnd() * 1.8 - 0.9;");
|
|
561
|
+
this.emit("data[o + 3] = 0.0; data[o + 4] = 0.25 + rnd(); data[o + 5] = 0.0; data[o + 6] = rnd(); data[o + 7] = 0.0;");
|
|
562
|
+
this.dedent();
|
|
563
|
+
this.emit("}");
|
|
564
|
+
this.emit("return data;");
|
|
565
|
+
this.dedent();
|
|
566
|
+
this.emit("}");
|
|
567
|
+
this.emit("");
|
|
568
|
+
}
|
|
569
|
+
emitGeometryHelpers() {
|
|
570
|
+
this.emit("// === Geometry Helpers ===");
|
|
571
|
+
this.emit("function generateCubeVertices(size) {");
|
|
572
|
+
this.indent();
|
|
573
|
+
this.emit("const s = size / 2;");
|
|
574
|
+
this.emit("return new Float32Array([");
|
|
575
|
+
this.indent();
|
|
576
|
+
this.emit("-s,-s, s, 0,0,1, s,-s, s, 0,0,1, s, s, s, 0,0,1, -s,-s, s, 0,0,1, s, s, s, 0,0,1, -s, s, s, 0,0,1,");
|
|
577
|
+
this.emit(" s,-s,-s, 0,0,-1, -s,-s,-s, 0,0,-1, -s, s,-s, 0,0,-1, s,-s,-s, 0,0,-1, -s, s,-s, 0,0,-1, s, s,-s, 0,0,-1,");
|
|
578
|
+
this.emit("-s,-s,-s, -1,0,0, -s,-s, s, -1,0,0, -s, s, s, -1,0,0, -s,-s,-s, -1,0,0, -s, s, s, -1,0,0, -s, s,-s, -1,0,0,");
|
|
579
|
+
this.emit(" s,-s, s, 1,0,0, s,-s,-s, 1,0,0, s, s,-s, 1,0,0, s,-s, s, 1,0,0, s, s,-s, 1,0,0, s, s, s, 1,0,0,");
|
|
580
|
+
this.emit("-s, s, s, 0,1,0, s, s, s, 0,1,0, s, s,-s, 0,1,0, -s, s, s, 0,1,0, s, s,-s, 0,1,0, -s, s,-s, 0,1,0,");
|
|
581
|
+
this.emit("-s,-s,-s, 0,-1,0, s,-s,-s, 0,-1,0, s,-s, s, 0,-1,0, -s,-s,-s, 0,-1,0, s,-s, s, 0,-1,0, -s,-s, s, 0,-1,0");
|
|
582
|
+
this.dedent();
|
|
583
|
+
this.emit("]);");
|
|
584
|
+
this.dedent();
|
|
585
|
+
this.emit("}");
|
|
586
|
+
this.emit("function generatePlaneVertices(width, depth) {");
|
|
587
|
+
this.indent();
|
|
588
|
+
this.emit("const x = width / 2, z = depth / 2;");
|
|
589
|
+
this.emit("return new Float32Array([-x,0,-z,0,1,0, x,0,-z,0,1,0, x,0,z,0,1,0, -x,0,-z,0,1,0, x,0,z,0,1,0, -x,0,z,0,1,0]);");
|
|
590
|
+
this.dedent();
|
|
591
|
+
this.emit("}");
|
|
592
|
+
if (this.hasWater) {
|
|
593
|
+
this.emit("function generateWaterPlaneVertices(w, h, n) {");
|
|
594
|
+
this.indent();
|
|
595
|
+
this.emit("const out = [], hw = w/2, hh = h/2, N = Math.max(2, n|0);");
|
|
596
|
+
this.emit("for (let iy=0; iy<N; iy++) { for (let ix=0; ix<N; ix++) {");
|
|
597
|
+
this.emit(" const x0=-hw+w*ix/N, x1=-hw+w*(ix+1)/N, y0=-hh+h*iy/N, y1=-hh+h*(iy+1)/N;");
|
|
598
|
+
this.emit(" const a=[x0,y0,0,0,0,1], b=[x1,y0,0,0,0,1], c=[x1,y1,0,0,0,1], d=[x0,y1,0,0,0,1];");
|
|
599
|
+
this.emit(" out.push(...a,...b,...c, ...a,...c,...d);");
|
|
600
|
+
this.emit("} } return new Float32Array(out);");
|
|
601
|
+
this.dedent();
|
|
602
|
+
this.emit("}");
|
|
603
|
+
}
|
|
604
|
+
this.emit("function generateSphereVertices(radius, lat, lon) {");
|
|
605
|
+
this.indent();
|
|
606
|
+
this.emit("const rings = Math.max(3, lat | 0), segs = Math.max(3, lon | 0); const out = [];");
|
|
607
|
+
this.emit("const P = (ri, si) => {");
|
|
608
|
+
this.emit(" const phi = Math.PI * ri / rings, theta = 2 * Math.PI * si / segs;");
|
|
609
|
+
this.emit(" const nx = Math.sin(phi) * Math.cos(theta), ny = Math.cos(phi), nz = Math.sin(phi) * Math.sin(theta);");
|
|
610
|
+
this.emit(" return [nx * radius, ny * radius, nz * radius, nx, ny, nz];");
|
|
611
|
+
this.emit("};");
|
|
612
|
+
this.emit("for (let ri = 0; ri < rings; ri++) { for (let si = 0; si < segs; si++) {");
|
|
613
|
+
this.emit(" const a = P(ri, si), b = P(ri + 1, si), c = P(ri + 1, si + 1), d = P(ri, si + 1);");
|
|
614
|
+
this.emit(" out.push(...a, ...b, ...c, ...a, ...c, ...d);");
|
|
615
|
+
this.emit("} } return new Float32Array(out);");
|
|
616
|
+
this.dedent();
|
|
617
|
+
this.emit("}");
|
|
618
|
+
this.emit("function generateCylinderVertices(radius, height, segments) {");
|
|
619
|
+
this.indent();
|
|
620
|
+
this.emit("const segs = Math.max(3, segments | 0), hy = height / 2, out = [];");
|
|
621
|
+
this.emit("for (let i = 0; i < segs; i++) {");
|
|
622
|
+
this.emit(" const t0 = 2*Math.PI*i/segs, t1 = 2*Math.PI*(i+1)/segs;");
|
|
623
|
+
this.emit(" const c0 = Math.cos(t0), s0 = Math.sin(t0), c1 = Math.cos(t1), s1 = Math.sin(t1);");
|
|
624
|
+
this.emit(" const b0 = [radius*c0, -hy, radius*s0, c0, 0, s0], t0v = [radius*c0, hy, radius*s0, c0, 0, s0];");
|
|
625
|
+
this.emit(" const b1 = [radius*c1, -hy, radius*s1, c1, 0, s1], t1v = [radius*c1, hy, radius*s1, c1, 0, s1];");
|
|
626
|
+
this.emit(" out.push(...b0, ...b1, ...t1v, ...b0, ...t1v, ...t0v);");
|
|
627
|
+
this.emit(" out.push(0, hy, 0, 0, 1, 0, radius*c0, hy, radius*s0, 0, 1, 0, radius*c1, hy, radius*s1, 0, 1, 0);");
|
|
628
|
+
this.emit(" out.push(0, -hy, 0, 0, -1, 0, radius*c1, -hy, radius*s1, 0, -1, 0, radius*c0, -hy, radius*s0, 0, -1, 0);");
|
|
629
|
+
this.emit("} return new Float32Array(out);");
|
|
630
|
+
this.dedent();
|
|
631
|
+
this.emit("}");
|
|
632
|
+
this.emit("function generateConeVertices(radius, height, segments) {");
|
|
633
|
+
this.indent();
|
|
634
|
+
this.emit("const segs = Math.max(3, segments | 0), hy = height / 2, out = [];");
|
|
635
|
+
this.emit("const ny = radius / Math.hypot(radius, height) || 0, nyx = height / Math.hypot(radius, height) || 1;");
|
|
636
|
+
this.emit("for (let i = 0; i < segs; i++) {");
|
|
637
|
+
this.emit(" const t0 = 2*Math.PI*i/segs, t1 = 2*Math.PI*(i+1)/segs;");
|
|
638
|
+
this.emit(" const c0 = Math.cos(t0), s0 = Math.sin(t0), c1 = Math.cos(t1), s1 = Math.sin(t1);");
|
|
639
|
+
this.emit(" out.push(0, hy, 0, (c0+c1)*0.5*nyx, ny, (s0+s1)*0.5*nyx);");
|
|
640
|
+
this.emit(" out.push(radius*c0, -hy, radius*s0, c0*nyx, ny, s0*nyx);");
|
|
641
|
+
this.emit(" out.push(radius*c1, -hy, radius*s1, c1*nyx, ny, s1*nyx);");
|
|
642
|
+
this.emit(" out.push(0, -hy, 0, 0, -1, 0, radius*c1, -hy, radius*s1, 0, -1, 0, radius*c0, -hy, radius*s0, 0, -1, 0);");
|
|
643
|
+
this.emit("} return new Float32Array(out);");
|
|
644
|
+
this.dedent();
|
|
645
|
+
this.emit("}");
|
|
646
|
+
this.emit("function generateTorusVertices(radius, tube, radial, tubular) {");
|
|
647
|
+
this.indent();
|
|
648
|
+
this.emit("const R = Math.max(3, radial | 0), T = Math.max(3, tubular | 0), out = [];");
|
|
649
|
+
this.emit("const P = (ri, ti) => {");
|
|
650
|
+
this.emit(" const u = 2*Math.PI*ri/R, v = 2*Math.PI*ti/T;");
|
|
651
|
+
this.emit(" const cu = Math.cos(u), su = Math.sin(u), cv = Math.cos(v), sv = Math.sin(v);");
|
|
652
|
+
this.emit(" const x = (radius + tube*cv)*cu, y = tube*sv, z = (radius + tube*cv)*su;");
|
|
653
|
+
this.emit(" return [x, y, z, cv*cu, sv, cv*su];");
|
|
654
|
+
this.emit("};");
|
|
655
|
+
this.emit("for (let ri = 0; ri < R; ri++) { for (let ti = 0; ti < T; ti++) {");
|
|
656
|
+
this.emit(" const a = P(ri, ti), b = P(ri + 1, ti), c = P(ri + 1, ti + 1), d = P(ri, ti + 1);");
|
|
657
|
+
this.emit(" out.push(...a, ...b, ...c, ...a, ...c, ...d);");
|
|
658
|
+
this.emit("} } return new Float32Array(out);");
|
|
659
|
+
this.dedent();
|
|
660
|
+
this.emit("}");
|
|
661
|
+
this.emit("");
|
|
662
|
+
}
|
|
663
|
+
// ─── Environment ──────────────────────────────────────────────────────────
|
|
664
|
+
emitEnvironment(env) {
|
|
665
|
+
this.emit("// === Environment ===");
|
|
666
|
+
const envData = {};
|
|
667
|
+
for (const prop of env.properties) envData[prop.key] = prop.value;
|
|
668
|
+
const bg = envData.background || envData.skybox || "#000000";
|
|
669
|
+
const bgColor = typeof bg === "string" && bg.startsWith("#") || Array.isArray(bg) ? this.parseColor(bg) : resolveSkyboxColor(bg);
|
|
670
|
+
this.emit(
|
|
671
|
+
`const clearColor: GPUColor = { r: ${bgColor[0]}, g: ${bgColor[1]}, b: ${bgColor[2]}, a: 1.0 };`
|
|
672
|
+
);
|
|
673
|
+
if (envData.fog && typeof envData.fog === "object") {
|
|
674
|
+
const fog = envData.fog;
|
|
675
|
+
const fc = this.parseColor(fog.color || "#cccccc");
|
|
676
|
+
this.emit(
|
|
677
|
+
`const fogUniforms = createBuffer(device, new Float32Array([${fc[0]}, ${fc[1]}, ${fc[2]}, 1.0, ${fog.near ?? 10}, ${fog.far ?? 100}, 0.0, 0.0]), GPUBufferUsage.UNIFORM);`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (envData.ambient_light !== void 0) {
|
|
681
|
+
this.emit(
|
|
682
|
+
`const ambientIntensity = ${typeof envData.ambient_light === "number" ? envData.ambient_light : 0.5};`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
this.emit("");
|
|
686
|
+
}
|
|
687
|
+
// ─── Object ───────────────────────────────────────────────────────────────
|
|
688
|
+
emitObject(obj) {
|
|
689
|
+
this.objectIndex++;
|
|
690
|
+
const v = this.sanitizeName(obj.name);
|
|
691
|
+
const meshType = this.findObjProp(obj, "geometry") || this.findObjProp(obj, "mesh") || this.findObjProp(obj, "type") || "cube";
|
|
692
|
+
const modelSrc = this.findObjProp(obj, "source") || this.findObjProp(obj, "src") || this.findObjProp(obj, "model");
|
|
693
|
+
const isModel = meshType === "model" || !!modelSrc;
|
|
694
|
+
const traits = obj.traits || [];
|
|
695
|
+
this.emit(`// Object: ${this.escapeStringValue(obj.name, "TypeScript")}`);
|
|
696
|
+
const role = resolveGeometryRole({
|
|
697
|
+
purpose: this.findObjProp(obj, "purpose"),
|
|
698
|
+
visible: this.findObjProp(obj, "visible"),
|
|
699
|
+
traitNames: traits.map((t) => t.name)
|
|
700
|
+
});
|
|
701
|
+
if (!role.visible) {
|
|
702
|
+
const prim = resolveGeometry(meshType).kind;
|
|
703
|
+
const pos = this.findObjProp(obj, "position");
|
|
704
|
+
const [px, py, pz] = Array.isArray(pos) ? pos : [0, 0, 0];
|
|
705
|
+
this.emit(
|
|
706
|
+
`// Functional geometry \u2014 purpose="${role.purpose}", not rendered; primitive="${prim}" is shared with physics/vfx/sim consumers.`
|
|
707
|
+
);
|
|
708
|
+
this.emit(
|
|
709
|
+
`holoGraphObjects.push({ id: "${this.escapeStringValue(obj.name, "TypeScript")}", name: "${this.escapeStringValue(obj.name, "TypeScript")}", geometry: "${this.escapeStringValue(meshType, "TypeScript")}", primitive: "${prim}", purpose: "${role.purpose}", visible: false, traits: ${this.json(traits.map((t) => t.name))}, basePosition: [${px},${py},${pz}], position: [${px},${py},${pz}] });`
|
|
710
|
+
);
|
|
711
|
+
this.manifestObjects.push({
|
|
712
|
+
id: String(obj.name ?? ""),
|
|
713
|
+
geometry: meshType,
|
|
714
|
+
primitive: prim,
|
|
715
|
+
purpose: role.purpose,
|
|
716
|
+
visible: false,
|
|
717
|
+
traits: traits.map((t) => t.name),
|
|
718
|
+
affordances: this.extractAffordanceNames(obj),
|
|
719
|
+
position: [
|
|
720
|
+
px + this.manifestGroupOffset[0],
|
|
721
|
+
py + this.manifestGroupOffset[1],
|
|
722
|
+
pz + this.manifestGroupOffset[2]
|
|
723
|
+
]
|
|
724
|
+
});
|
|
725
|
+
this.emit("");
|
|
726
|
+
if (obj.children) {
|
|
727
|
+
for (const child of obj.children) this.emitObject(child);
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const isGaussianSplat = traits.some((t) => t.name === "gaussian_splat");
|
|
732
|
+
const isPointCloud = traits.some((t) => t.name === "point_cloud");
|
|
733
|
+
const isGpuParticle = traits.some((t) => isGpuParticleTraitName(t.name));
|
|
734
|
+
if (isModel && modelSrc) {
|
|
735
|
+
this.emit(`const ${v}Asset = await assetLoader.load("${modelSrc}");`);
|
|
736
|
+
this.emit(
|
|
737
|
+
`// Model asset "${this.escapeStringValue(modelSrc, "TypeScript")}" is tracked in ${v}Asset; fallback mesh keeps the render path defined.`
|
|
738
|
+
);
|
|
739
|
+
this.emitMeshObject(v, obj, "cube");
|
|
740
|
+
} else if (isModel) {
|
|
741
|
+
this.emit(`// WARNING: geometry "model" without source URL \u2014 placeholder cube`);
|
|
742
|
+
this.emitMeshObject(v, obj, "cube");
|
|
743
|
+
} else if (isGaussianSplat) {
|
|
744
|
+
this.emitGaussianSplatObject(v, obj);
|
|
745
|
+
} else if (isPointCloud) {
|
|
746
|
+
this.emitPointCloudObject(v, obj);
|
|
747
|
+
} else if (isGpuParticle) {
|
|
748
|
+
this.emitGpuParticleObject(v, obj);
|
|
749
|
+
} else if (this.isWaterObject(obj)) {
|
|
750
|
+
this.emitWaterObject(v, obj);
|
|
751
|
+
} else {
|
|
752
|
+
this.emitMeshObject(v, obj, meshType);
|
|
753
|
+
}
|
|
754
|
+
if (traits.some((t) => t.name === "gpu_physics")) {
|
|
755
|
+
this.emit(`const ${v}PhysicsBuf = createStorageBuffer(device, 1024 * 16); // gpu_physics`);
|
|
756
|
+
}
|
|
757
|
+
if (traits.some((t) => t.name === "compute" || t.name === "gpu_buffer")) {
|
|
758
|
+
const ct = traits.find((t) => t.name === "compute" || t.name === "gpu_buffer");
|
|
759
|
+
this.emit(
|
|
760
|
+
`const ${v}ComputeBuf = createStorageBuffer(device, ${ct?.config?.buffer_size || 4096});`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
this.emit("");
|
|
764
|
+
if (obj.children) {
|
|
765
|
+
for (const child of obj.children) this.emitObject(child);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// ─── Water / fluid surfaces ────────────────────────────────────────────────
|
|
769
|
+
/**
|
|
770
|
+
* Resolve which named channels the water surface's clarity/ripple are bound to,
|
|
771
|
+
* from the first water object's `water_clarity_binding` / `ripple_intensity_binding`
|
|
772
|
+
* props. These names match the .holo's environment state fields, so host state
|
|
773
|
+
* (Brittney receipts) driving those fields drives the water by name.
|
|
774
|
+
*/
|
|
775
|
+
resolveWaterChannels(composition) {
|
|
776
|
+
const all = [...composition.objects ?? []];
|
|
777
|
+
for (const g of composition.spatialGroups ?? []) all.push(...g.objects ?? []);
|
|
778
|
+
const water = all.find((o) => this.isWaterObject(o));
|
|
779
|
+
if (!water) return;
|
|
780
|
+
const clarity = this.findObjProp(water, "water_clarity_binding");
|
|
781
|
+
const ripple = this.findObjProp(water, "ripple_intensity_binding");
|
|
782
|
+
if (typeof clarity === "string" && clarity.trim()) this.waterClarityChannel = clarity.trim();
|
|
783
|
+
if (typeof ripple === "string" && ripple.trim()) this.waterRippleChannel = ripple.trim();
|
|
784
|
+
}
|
|
785
|
+
/** True if any object (incl. nested groups/children) is a water/fluid surface. */
|
|
786
|
+
compositionHasWater(composition) {
|
|
787
|
+
const scan = (objs) => (objs ?? []).some((o) => this.isWaterObject(o) || scan(o.children));
|
|
788
|
+
if (scan(composition.objects)) return true;
|
|
789
|
+
const walk = (g) => scan(g.objects) || (g.groups ?? []).some(walk);
|
|
790
|
+
return (composition.spatialGroups ?? []).some(walk);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* A water surface is a fluid-plane mesh (full_screen_fluid_plane / water / ...),
|
|
794
|
+
* or a water-ish MATERIAL on a plane-like geometry. It is NOT a water material on
|
|
795
|
+
* a sphere — emitWaterObject always renders a tessellated plane.
|
|
796
|
+
*/
|
|
797
|
+
isWaterObject(obj) {
|
|
798
|
+
const mesh = String(this.findObjProp(obj, "mesh") ?? this.findObjProp(obj, "geometry") ?? "").trim().toLowerCase();
|
|
799
|
+
const material = String(this.findObjProp(obj, "material") ?? "").trim().toLowerCase();
|
|
800
|
+
const WATER_MESH = ["full_screen_fluid_plane", "fluid_plane", "water", "water_plane", "ocean"];
|
|
801
|
+
const WATER_MAT = ["liquid_glass", "water", "liquid", "fluid"];
|
|
802
|
+
if (WATER_MESH.includes(mesh)) return true;
|
|
803
|
+
return WATER_MAT.includes(material) && resolveGeometry(mesh).kind === "plane";
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Emit an animated water surface: a tessellated plane driven by the shared
|
|
807
|
+
* per-frame time uniform through the WGSL_WATER shader (Gerstner-lite height
|
|
808
|
+
* waves + analytic normals + texture-free caustics + fresnel). Binding layout
|
|
809
|
+
* mirrors the mesh path (0=Model, 2=vpUniform) plus 1=WaterStatic, 3=waterFrameU.
|
|
810
|
+
*/
|
|
811
|
+
emitWaterObject(v, obj) {
|
|
812
|
+
const pos = this.findObjProp(obj, "position");
|
|
813
|
+
const scale = this.findObjProp(obj, "scale") || this.findObjProp(obj, "size");
|
|
814
|
+
const [px, py, pz] = Array.isArray(pos) ? pos : [0, 0, 0];
|
|
815
|
+
const [sx, sy, sz] = Array.isArray(scale) ? scale : typeof scale === "number" ? [scale, scale, scale] : [1, 1, 1];
|
|
816
|
+
const deep = this.parseColor(this.findObjProp(obj, "color") ?? "#06131f");
|
|
817
|
+
const shallow = [
|
|
818
|
+
Math.min(1, deep[0] * 2.2 + 0.05),
|
|
819
|
+
Math.min(1, deep[1] * 2.2 + 0.2),
|
|
820
|
+
Math.min(1, deep[2] * 2 + 0.24)
|
|
821
|
+
];
|
|
822
|
+
const amplitude = 0.18, choppiness = 0.4, clarity = 1, ripple = 0;
|
|
823
|
+
const L = [0.3714, 0.7428, 0.5571];
|
|
824
|
+
this.emit(`// Water surface: ${this.escapeStringValue(obj.name, "TypeScript")}`);
|
|
825
|
+
this.emit(`const ${v}Vertices = generateWaterPlaneVertices(1.0, 1.0, 48);`);
|
|
826
|
+
this.emit(`const ${v}VBO = createBuffer(device, ${v}Vertices, GPUBufferUsage.VERTEX);`);
|
|
827
|
+
this.emit(`const ${v}VertexCount = ${v}Vertices.length / 6;`);
|
|
828
|
+
this.emit(
|
|
829
|
+
`const ${v}Model = createBuffer(device, new Float32Array([${sx},0,0,0, 0,${sy},0,0, 0,0,${sz},0, ${px},${py},${pz},1]), GPUBufferUsage.UNIFORM);`
|
|
830
|
+
);
|
|
831
|
+
this.emit(
|
|
832
|
+
`const ${v}WaterStatic = createBuffer(device, new Float32Array([${amplitude},${choppiness},${clarity},${ripple}, ${deep[0]},${deep[1]},${deep[2]},0, ${shallow[0]},${shallow[1]},${shallow[2]},0, ${L[0]},${L[1]},${L[2]},0]), GPUBufferUsage.UNIFORM);`
|
|
833
|
+
);
|
|
834
|
+
this.emit(`const ${v}WaterPipeline = device.createRenderPipeline({`);
|
|
835
|
+
this.indent();
|
|
836
|
+
this.emit('layout: "auto",');
|
|
837
|
+
this.emit(
|
|
838
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_WATER }), entryPoint: "vs_water",'
|
|
839
|
+
);
|
|
840
|
+
this.emit(
|
|
841
|
+
' buffers: [{ arrayStride: 24, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }] }] },'
|
|
842
|
+
);
|
|
843
|
+
this.emit(
|
|
844
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_WATER }), entryPoint: "fs_water", targets: [{ format }] },'
|
|
845
|
+
);
|
|
846
|
+
this.emit('primitive: { topology: "triangle-list", cullMode: "none" },');
|
|
847
|
+
this.emit(
|
|
848
|
+
`depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" }, multisample: { count: MSAA_COUNT },`
|
|
849
|
+
);
|
|
850
|
+
this.dedent();
|
|
851
|
+
this.emit("});");
|
|
852
|
+
this.emit(
|
|
853
|
+
`const ${v}WaterBind = device.createBindGroup({ layout: ${v}WaterPipeline.getBindGroupLayout(0), entries: [`
|
|
854
|
+
);
|
|
855
|
+
this.emit(
|
|
856
|
+
` { binding: 0, resource: { buffer: ${v}Model } }, { binding: 1, resource: { buffer: ${v}WaterStatic } }, { binding: 2, resource: { buffer: vpUniform } }, { binding: 3, resource: { buffer: waterFrameU } }] });`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
/** Emit the shared WGSL_WATER module (vs_water + fs_water + noise helpers). */
|
|
860
|
+
emitWaterShaderSource() {
|
|
861
|
+
this.emit("const WGSL_WATER = /* wgsl */`");
|
|
862
|
+
this.emit("struct Model { m: mat4x4<f32> };");
|
|
863
|
+
this.emit("struct WStatic { p: vec4<f32>, deep: vec4<f32>, shallow: vec4<f32>, lightDir: vec4<f32> };");
|
|
864
|
+
this.emit("struct Cam { viewProj: mat4x4<f32> };");
|
|
865
|
+
this.emit("struct WFrame { tc: vec4<f32>, dyn: vec4<f32> };");
|
|
866
|
+
this.emit("@group(0) @binding(0) var<uniform> model: Model;");
|
|
867
|
+
this.emit("@group(0) @binding(1) var<uniform> ws: WStatic;");
|
|
868
|
+
this.emit("@group(0) @binding(2) var<uniform> cam: Cam;");
|
|
869
|
+
this.emit("@group(0) @binding(3) var<uniform> wf: WFrame;");
|
|
870
|
+
this.emit("struct VIn { @location(0) pos: vec3<f32>, @location(1) norm: vec3<f32> };");
|
|
871
|
+
this.emit("struct VOut { @builtin(position) clip: vec4<f32>, @location(0) wNorm: vec3<f32>, @location(1) wPos: vec3<f32> };");
|
|
872
|
+
this.emit("@vertex fn vs_water(in: VIn) -> VOut {");
|
|
873
|
+
this.emit(" var o: VOut;");
|
|
874
|
+
this.emit(" let sx = length(model.m[0].xyz); let sy = length(model.m[1].xyz); let sz = max(length(model.m[2].xyz), 1e-4);");
|
|
875
|
+
this.emit(" let wp = in.pos.xy * vec2<f32>(sx, sy);");
|
|
876
|
+
this.emit(" let t = wf.tc.x; let amp = ws.p.x * wf.dyn.z * (0.4 + wf.dyn.y);");
|
|
877
|
+
this.emit(" var h = 0.0; var dhdx = 0.0; var dhdy = 0.0;");
|
|
878
|
+
this.emit(" var dx = 0.0; var dy = 0.0; var k = 0.0; var spd = 0.0; var a = 0.0;");
|
|
879
|
+
this.emit(" for (var i = 0; i < 4; i = i + 1) {");
|
|
880
|
+
this.emit(" if (i == 0) { dx = 1.0; dy = 0.0; k = 0.7; spd = 0.6; a = 1.0; }");
|
|
881
|
+
this.emit(" else if (i == 1) { dx = 0.5; dy = 0.86; k = 1.2; spd = 0.9; a = 0.55; }");
|
|
882
|
+
this.emit(" else if (i == 2) { dx = -0.8; dy = 0.6; k = 1.9; spd = 1.15; a = 0.3; }");
|
|
883
|
+
this.emit(" else { dx = 0.2; dy = -0.98; k = 2.7; spd = 1.4; a = 0.18; }");
|
|
884
|
+
this.emit(" let d = normalize(vec2<f32>(dx, dy));");
|
|
885
|
+
this.emit(" let ph = k * dot(d, wp) + spd * t;");
|
|
886
|
+
this.emit(" h = h + a * amp * sin(ph);");
|
|
887
|
+
this.emit(" dhdx = dhdx + a * amp * k * d.x * cos(ph);");
|
|
888
|
+
this.emit(" dhdy = dhdy + a * amp * k * d.y * cos(ph);");
|
|
889
|
+
this.emit(" }");
|
|
890
|
+
this.emit(" var lp = in.pos; lp.z = lp.z + h / sz;");
|
|
891
|
+
this.emit(" let world = model.m * vec4<f32>(lp, 1.0);");
|
|
892
|
+
this.emit(" o.wPos = world.xyz;");
|
|
893
|
+
this.emit(" o.wNorm = normalize(vec3<f32>(-dhdx, -dhdy, 1.0));");
|
|
894
|
+
this.emit(" o.clip = cam.viewProj * world;");
|
|
895
|
+
this.emit(" return o;");
|
|
896
|
+
this.emit("}");
|
|
897
|
+
this.emit("fn hash21(p: vec2<f32>) -> f32 { let h = dot(p, vec2<f32>(127.1, 311.7)); return fract(sin(h) * 43758.5453); }");
|
|
898
|
+
this.emit("fn vnoise(p: vec2<f32>) -> f32 {");
|
|
899
|
+
this.emit(" let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);");
|
|
900
|
+
this.emit(" let a = hash21(i); let b = hash21(i + vec2<f32>(1.0, 0.0));");
|
|
901
|
+
this.emit(" let c = hash21(i + vec2<f32>(0.0, 1.0)); let dd = hash21(i + vec2<f32>(1.0, 1.0));");
|
|
902
|
+
this.emit(" return mix(mix(a, b, u.x), mix(c, dd, u.x), u.y);");
|
|
903
|
+
this.emit("}");
|
|
904
|
+
this.emit("struct FIn { @location(0) wNorm: vec3<f32>, @location(1) wPos: vec3<f32> };");
|
|
905
|
+
this.emit("@fragment fn fs_water(in: FIn) -> @location(0) vec4<f32> {");
|
|
906
|
+
this.emit(" let N = normalize(in.wNorm);");
|
|
907
|
+
this.emit(" let V = normalize(wf.tc.yzw - in.wPos);");
|
|
908
|
+
this.emit(" let L = normalize(ws.lightDir.xyz);");
|
|
909
|
+
this.emit(" let t = wf.tc.x;");
|
|
910
|
+
this.emit(" let ndv = max(dot(N, V), 0.0);");
|
|
911
|
+
this.emit(" let fres = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);");
|
|
912
|
+
this.emit(" let tilt = clamp(1.0 - ndv, 0.0, 1.0);");
|
|
913
|
+
this.emit(" var col = mix(ws.deep.rgb, ws.shallow.rgb, clamp(tilt * 1.3, 0.0, 1.0));");
|
|
914
|
+
this.emit(" let p = in.wPos.xy;");
|
|
915
|
+
this.emit(" let n1 = vnoise(p * 0.9 + vec2<f32>(t * 0.15, t * 0.08));");
|
|
916
|
+
this.emit(" let n2 = vnoise(p * 1.7 - vec2<f32>(t * 0.11, t * 0.19));");
|
|
917
|
+
this.emit(" let caustic = pow(n1 * n2, 3.0);");
|
|
918
|
+
this.emit(" col = col + vec3<f32>(0.10, 0.18, 0.20) * caustic * wf.dyn.x;");
|
|
919
|
+
this.emit(" let H = normalize(L + V);");
|
|
920
|
+
this.emit(" let spec = pow(max(dot(N, H), 0.0), 200.0);");
|
|
921
|
+
this.emit(" col = col + vec3<f32>(0.85, 0.92, 1.0) * spec;");
|
|
922
|
+
this.emit(" let sky = vec3<f32>(0.10, 0.26, 0.36);");
|
|
923
|
+
this.emit(" col = mix(col, sky, fres * 0.3);");
|
|
924
|
+
this.emit(" return vec4<f32>(col, 1.0);");
|
|
925
|
+
this.emit("}`;");
|
|
926
|
+
this.emit("");
|
|
927
|
+
}
|
|
928
|
+
/** Tool-affordance names declared on an object's @tool traits (both trait
|
|
929
|
+
* config formats), for the scene manifest — the artifact-level answer to
|
|
930
|
+
* "which named actions does this entity offer" that the numeric registry
|
|
931
|
+
* cannot express. */
|
|
932
|
+
extractAffordanceNames(obj) {
|
|
933
|
+
const names = [];
|
|
934
|
+
for (const trait of obj.traits ?? []) {
|
|
935
|
+
const t = trait;
|
|
936
|
+
if (t.name !== "tool") continue;
|
|
937
|
+
const config = "config" in t && typeof t.config === "object" && t.config !== null ? t.config : t;
|
|
938
|
+
if (typeof config["name"] === "string" && config["name"] !== "tool") {
|
|
939
|
+
names.push(config["name"]);
|
|
940
|
+
} else {
|
|
941
|
+
names.push("unnamed_tool");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return names;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* HoloScene Manifest — a machine-readable world-facts block INSIDE the
|
|
948
|
+
* delivered artifact (g9tk). Collected at EMISSION time (each emitObject/
|
|
949
|
+
* emitMeshObject records what it actually emitted), so the manifest describes
|
|
950
|
+
* the artifact, not the input. Strict JSON payload: independent consumers
|
|
951
|
+
* (perceiver derivations, tooling) JSON.parse it instead of regexing the
|
|
952
|
+
* imperative holoGraphObjects push literals.
|
|
953
|
+
*/
|
|
954
|
+
emitSceneManifest(composition) {
|
|
955
|
+
const manifest = {
|
|
956
|
+
version: "holo-scene-manifest-v1",
|
|
957
|
+
source: String(composition.name ?? ""),
|
|
958
|
+
objects: this.manifestObjects
|
|
959
|
+
};
|
|
960
|
+
this.emit("// === HoloScene Manifest (machine-readable world facts; part of the delivered artifact) ===");
|
|
961
|
+
this.emit(`const holoSceneManifest = ${this.json(manifest)};`);
|
|
962
|
+
this.emit('if (typeof globalThis !== "undefined") { globalThis.holoSceneManifest = holoSceneManifest; }');
|
|
963
|
+
this.emit("");
|
|
964
|
+
}
|
|
965
|
+
emitMeshObject(v, obj, meshType) {
|
|
966
|
+
this.emit(`const ${v}Vertices = ${this.geometryVertexDataFn(meshType)};`);
|
|
967
|
+
this.emit(`const ${v}VBO = createBuffer(device, ${v}Vertices, GPUBufferUsage.VERTEX);`);
|
|
968
|
+
this.emit(`const ${v}VertexCount = ${v}Vertices.length / 6;`);
|
|
969
|
+
const pos = this.findObjProp(obj, "position");
|
|
970
|
+
const scale = this.findObjProp(obj, "scale") || this.findObjProp(obj, "size");
|
|
971
|
+
const [px, py, pz] = Array.isArray(pos) ? pos : [0, 0, 0];
|
|
972
|
+
const [sx, sy, sz] = Array.isArray(scale) ? scale : typeof scale === "number" ? [scale, scale, scale] : [1, 1, 1];
|
|
973
|
+
this.emit(
|
|
974
|
+
`const ${v}Model = createBuffer(device, new Float32Array([${sx},0,0,0, 0,${sy},0,0, 0,0,${sz},0, ${px},${py},${pz},1]), GPUBufferUsage.UNIFORM);`
|
|
975
|
+
);
|
|
976
|
+
const material = this.findObjProp(obj, "material");
|
|
977
|
+
const color = this.findObjProp(obj, "color");
|
|
978
|
+
const [cr, cg, cb] = this.extractMaterialColor(material, color);
|
|
979
|
+
const rough = this.extractMaterialProp(material, "roughness", 0.5);
|
|
980
|
+
const metal = this.extractMaterialProp(material, "metalness", 0);
|
|
981
|
+
let [er, eg, eb] = this.extractEmissive(material, obj);
|
|
982
|
+
let emissiveStrength = this.extractMaterialProp(material, "emissive_intensity", 1);
|
|
983
|
+
const glow = this.materialGlow(material);
|
|
984
|
+
if (glow > 0 && er === 0 && eg === 0 && eb === 0) {
|
|
985
|
+
er = cr;
|
|
986
|
+
eg = cg;
|
|
987
|
+
eb = cb;
|
|
988
|
+
emissiveStrength = glow;
|
|
989
|
+
}
|
|
990
|
+
this.emit(
|
|
991
|
+
`const ${v}Mat = createBuffer(device, new Float32Array([${cr},${cg},${cb},1.0, ${rough},${metal},${emissiveStrength},0, ${er},${eg},${eb},0]), GPUBufferUsage.UNIFORM);`
|
|
992
|
+
);
|
|
993
|
+
this.emit(
|
|
994
|
+
`holoGraphObjects.push({ id: "${this.escapeStringValue(obj.name, "TypeScript")}", name: "${this.escapeStringValue(obj.name, "TypeScript")}", geometry: "${this.escapeStringValue(meshType, "TypeScript")}", traits: ${this.json((obj.traits || []).map((t) => t.name))}, properties: ${this.json(this.extractObjectProperties(obj))}, basePosition: [${px},${py},${pz}], position: [${px},${py},${pz}], baseScale: [${sx},${sy},${sz}], scale: [${sx},${sy},${sz}], baseColor: [${cr},${cg},${cb}], color: [${cr},${cg},${cb}], material: { roughness: ${rough}, metalness: ${metal}, emissiveStrength: ${emissiveStrength}, emissive: [${er},${eg},${eb}] }, modelBuffer: ${v}Model, matBuffer: ${v}Mat, visible: true });`
|
|
995
|
+
);
|
|
996
|
+
this.manifestObjects.push({
|
|
997
|
+
id: String(obj.name ?? ""),
|
|
998
|
+
geometry: meshType,
|
|
999
|
+
visible: true,
|
|
1000
|
+
traits: (obj.traits || []).map((t) => t.name),
|
|
1001
|
+
affordances: this.extractAffordanceNames(obj),
|
|
1002
|
+
position: [
|
|
1003
|
+
px + this.manifestGroupOffset[0],
|
|
1004
|
+
py + this.manifestGroupOffset[1],
|
|
1005
|
+
pz + this.manifestGroupOffset[2]
|
|
1006
|
+
],
|
|
1007
|
+
scale: [sx, sy, sz],
|
|
1008
|
+
color: [cr, cg, cb]
|
|
1009
|
+
});
|
|
1010
|
+
this.emit(`const ${v}Pipeline = device.createRenderPipeline({`);
|
|
1011
|
+
this.indent();
|
|
1012
|
+
this.emit('layout: "auto",');
|
|
1013
|
+
this.emit(
|
|
1014
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_VERTEX }), entryPoint: "vs_main",'
|
|
1015
|
+
);
|
|
1016
|
+
this.emit(
|
|
1017
|
+
' buffers: [{ arrayStride: 24, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }] }] },'
|
|
1018
|
+
);
|
|
1019
|
+
this.emit(
|
|
1020
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_FRAGMENT }), entryPoint: "fs_main", targets: [{ format }] },'
|
|
1021
|
+
);
|
|
1022
|
+
this.emit('primitive: { topology: "triangle-list", cullMode: "back" },');
|
|
1023
|
+
this.emit(
|
|
1024
|
+
`depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" }, multisample: { count: MSAA_COUNT },`
|
|
1025
|
+
);
|
|
1026
|
+
this.dedent();
|
|
1027
|
+
this.emit("});");
|
|
1028
|
+
this.emit(
|
|
1029
|
+
`const ${v}Bind = device.createBindGroup({ layout: ${v}Pipeline.getBindGroupLayout(0), entries: [`
|
|
1030
|
+
);
|
|
1031
|
+
this.emit(
|
|
1032
|
+
` { binding: 0, resource: { buffer: ${v}Model } }, { binding: 1, resource: { buffer: ${v}Mat } }, { binding: 2, resource: { buffer: vpUniform } }, { binding: 3, resource: { buffer: sceneLights } }] });`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
emitGaussianSplatObject(v, obj) {
|
|
1036
|
+
const trait = obj.traits?.find((t) => t.name === "gaussian_splat");
|
|
1037
|
+
const src = trait?.config?.src || "scene.ply";
|
|
1038
|
+
const max = trait?.config?.max_splats || 5e5;
|
|
1039
|
+
const sorted = trait?.config?.sorted !== false;
|
|
1040
|
+
this.emit(`// Gaussian Splat \u2014 source: ${src} (${sorted ? "radix-sorted" : "unsorted"})`);
|
|
1041
|
+
if (sorted) {
|
|
1042
|
+
this.emit(
|
|
1043
|
+
`// === Wait-Free Radix Sort Pipeline (4x 8-bit Blelloch scan, no global atomics) ===`
|
|
1044
|
+
);
|
|
1045
|
+
this.emit(`import { GaussianSplatSorter } from '@holoscript/core/gpu/GaussianSplatSorter';`);
|
|
1046
|
+
this.emit(`const ${v}Sorter = new GaussianSplatSorter(device, {`);
|
|
1047
|
+
this.indent();
|
|
1048
|
+
this.emit(`maxSplats: ${max},`);
|
|
1049
|
+
this.emit(`canvasWidth: canvas.width,`);
|
|
1050
|
+
this.emit(`canvasHeight: canvas.height,`);
|
|
1051
|
+
this.dedent();
|
|
1052
|
+
this.emit(`});`);
|
|
1053
|
+
this.emit(`await ${v}Sorter.initialize();`);
|
|
1054
|
+
this.emit(`const ${v}SplatCount = { value: 0 };`);
|
|
1055
|
+
} else {
|
|
1056
|
+
this.emit(`const ${v}SplatBuf = createStorageBuffer(device, ${max} * 32);`);
|
|
1057
|
+
this.emit(`const ${v}SplatCount = { value: 0 };`);
|
|
1058
|
+
this.emit(`const ${v}SplatPipeline = device.createRenderPipeline({`);
|
|
1059
|
+
this.indent();
|
|
1060
|
+
this.emit('layout: "auto",');
|
|
1061
|
+
this.emit(
|
|
1062
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_SPLAT }), entryPoint: "vs_splat", buffers: [] },'
|
|
1063
|
+
);
|
|
1064
|
+
this.emit(
|
|
1065
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_SPLAT }), entryPoint: "fs_splat",'
|
|
1066
|
+
);
|
|
1067
|
+
this.emit(
|
|
1068
|
+
' targets: [{ format, blend: { color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } } }] },'
|
|
1069
|
+
);
|
|
1070
|
+
this.emit('primitive: { topology: "triangle-strip" },');
|
|
1071
|
+
this.emit(
|
|
1072
|
+
'depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "less" },'
|
|
1073
|
+
);
|
|
1074
|
+
this.dedent();
|
|
1075
|
+
this.emit("});");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
emitPointCloudObject(v, obj) {
|
|
1079
|
+
const trait = obj.traits?.find((t) => t.name === "point_cloud");
|
|
1080
|
+
const max = trait?.config?.max_points || 1e5;
|
|
1081
|
+
this.emit(`// Point Cloud (${max} max points)`);
|
|
1082
|
+
this.emit(`const ${v}PointBuf = createStorageBuffer(device, ${max} * 16);`);
|
|
1083
|
+
this.emit(`const ${v}PointCount = { value: 0 };`);
|
|
1084
|
+
this.emit(`const ${v}PointPipeline = device.createRenderPipeline({`);
|
|
1085
|
+
this.indent();
|
|
1086
|
+
this.emit('layout: "auto",');
|
|
1087
|
+
this.emit(
|
|
1088
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_POINT }), entryPoint: "vs_point",'
|
|
1089
|
+
);
|
|
1090
|
+
this.emit(
|
|
1091
|
+
' buffers: [{ arrayStride: 16, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "unorm8x4" }] }] },'
|
|
1092
|
+
);
|
|
1093
|
+
this.emit(
|
|
1094
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_POINT }), entryPoint: "fs_point", targets: [{ format }] },'
|
|
1095
|
+
);
|
|
1096
|
+
this.emit(
|
|
1097
|
+
`primitive: { topology: "point-list" }, depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" }, multisample: { count: MSAA_COUNT },`
|
|
1098
|
+
);
|
|
1099
|
+
this.dedent();
|
|
1100
|
+
this.emit("});");
|
|
1101
|
+
}
|
|
1102
|
+
emitGpuParticleObject(v, obj) {
|
|
1103
|
+
const trait = findGpuParticleTrait(obj.traits);
|
|
1104
|
+
const count = trait?.config?.count || 1e4;
|
|
1105
|
+
this.emit(`// GPU Particles (${count}) from @${trait?.name ?? "gpu_particle"}`);
|
|
1106
|
+
this.emit(`const ${v}ParticleCount = ${count};`);
|
|
1107
|
+
this.emit(`const ${v}ParticleBufA = createStorageBuffer(device, ${v}ParticleCount * 32);`);
|
|
1108
|
+
this.emit(`const ${v}ParticleBufB = createStorageBuffer(device, ${v}ParticleCount * 32);`);
|
|
1109
|
+
this.emit(`const ${v}ParticlePipeline = device.createRenderPipeline({`);
|
|
1110
|
+
this.indent();
|
|
1111
|
+
this.emit('layout: "auto",');
|
|
1112
|
+
this.emit(
|
|
1113
|
+
'vertex: { module: device.createShaderModule({ code: WGSL_PARTICLE_RENDER }), entryPoint: "vs_particle",'
|
|
1114
|
+
);
|
|
1115
|
+
this.emit(
|
|
1116
|
+
' buffers: [{ arrayStride: 32, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }, { shaderLocation: 2, offset: 24, format: "float32" }] }] },'
|
|
1117
|
+
);
|
|
1118
|
+
this.emit(
|
|
1119
|
+
'fragment: { module: device.createShaderModule({ code: WGSL_PARTICLE_RENDER }), entryPoint: "fs_particle",'
|
|
1120
|
+
);
|
|
1121
|
+
this.emit(
|
|
1122
|
+
' targets: [{ format, blend: { color: { srcFactor: "src-alpha", dstFactor: "one", operation: "add" }, alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" } } }] },'
|
|
1123
|
+
);
|
|
1124
|
+
this.emit(
|
|
1125
|
+
'primitive: { topology: "point-list" }, depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "less" },'
|
|
1126
|
+
);
|
|
1127
|
+
this.dedent();
|
|
1128
|
+
this.emit("});");
|
|
1129
|
+
}
|
|
1130
|
+
// ─── Spatial Group ────────────────────────────────────────────────────────
|
|
1131
|
+
emitGroup(group) {
|
|
1132
|
+
const v = this.sanitizeName(group.name);
|
|
1133
|
+
this.emit(`// Spatial Group: ${this.escapeStringValue(group.name, "TypeScript")}`);
|
|
1134
|
+
const pos = group.properties.find((p) => p.key === "position")?.value;
|
|
1135
|
+
const [px, py, pz] = Array.isArray(pos) ? pos : [0, 0, 0];
|
|
1136
|
+
this.emit(
|
|
1137
|
+
`const ${v}GroupXform = createBuffer(device, new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, ${px},${py},${pz},1]), GPUBufferUsage.UNIFORM);`
|
|
1138
|
+
);
|
|
1139
|
+
const saved = this.manifestGroupOffset;
|
|
1140
|
+
this.manifestGroupOffset = [saved[0] + px, saved[1] + py, saved[2] + pz];
|
|
1141
|
+
for (const obj of group.objects) this.emitObject(obj);
|
|
1142
|
+
if (group.groups) {
|
|
1143
|
+
for (const sub of group.groups) this.emitGroup(sub);
|
|
1144
|
+
}
|
|
1145
|
+
this.manifestGroupOffset = saved;
|
|
1146
|
+
this.emit("");
|
|
1147
|
+
}
|
|
1148
|
+
// ─── Light ────────────────────────────────────────────────────────────────
|
|
1149
|
+
// Aggregate the scene's declared lights into ONE shared uniform buffer that every
|
|
1150
|
+
// mesh binds (binding 3). Shape math — color*intensity, type tag, world position,
|
|
1151
|
+
// travel direction — is resolved here; the fragment shader consumes up to
|
|
1152
|
+
// MAX_LIGHTS. A light-less scene gets a synthesized key light so it stays lit.
|
|
1153
|
+
emitSceneLights(composition) {
|
|
1154
|
+
const MAX_LIGHTS = 4;
|
|
1155
|
+
const typeIndex = {
|
|
1156
|
+
directional: 0,
|
|
1157
|
+
point: 1,
|
|
1158
|
+
spot: 2,
|
|
1159
|
+
hemisphere: 3,
|
|
1160
|
+
ambient: 4,
|
|
1161
|
+
area: 5
|
|
1162
|
+
};
|
|
1163
|
+
const lights = (composition.lights ?? []).slice(0, MAX_LIGHTS).map((light) => {
|
|
1164
|
+
let color = [1, 1, 1], intensity = 1, pos = [0, 10, 0], dir = [0, -1, 0], hasDir = false;
|
|
1165
|
+
for (const prop of light.properties) {
|
|
1166
|
+
if (prop.key === "color") color = this.parseColor(prop.value);
|
|
1167
|
+
else if (prop.key === "intensity") intensity = prop.value;
|
|
1168
|
+
else if (prop.key === "position" && Array.isArray(prop.value)) pos = prop.value;
|
|
1169
|
+
else if (prop.key === "direction" && Array.isArray(prop.value)) {
|
|
1170
|
+
dir = prop.value;
|
|
1171
|
+
hasDir = true;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const ti = typeIndex[light.lightType] ?? 0;
|
|
1175
|
+
if (ti === 0 && !hasDir && Array.isArray(pos)) dir = [-pos[0], -pos[1], -pos[2]];
|
|
1176
|
+
return { name: String(light.name ?? ""), kind: String(light.lightType ?? ""), color, intensity, pos, dir, ti };
|
|
1177
|
+
});
|
|
1178
|
+
if (lights.length === 0) {
|
|
1179
|
+
lights.push({ name: "(synthesized key light)", kind: "directional", color: [1, 1, 1], intensity: 1, pos: [1, 2, 1.5], dir: [-1, -2, -1.5], ti: 0 });
|
|
1180
|
+
}
|
|
1181
|
+
const segs = [`${lights.length},0,0,0`];
|
|
1182
|
+
for (let i = 0; i < MAX_LIGHTS; i++) {
|
|
1183
|
+
const l = lights[i];
|
|
1184
|
+
if (l) {
|
|
1185
|
+
segs.push(`${l.color[0] * l.intensity},${l.color[1] * l.intensity},${l.color[2] * l.intensity},${l.ti}`);
|
|
1186
|
+
segs.push(`${l.pos[0]},${l.pos[1]},${l.pos[2]},0`);
|
|
1187
|
+
segs.push(`${l.dir[0]},${l.dir[1]},${l.dir[2]},0`);
|
|
1188
|
+
} else {
|
|
1189
|
+
segs.push("0,0,0,0", "0,0,0,0", "0,0,0,0");
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
this.emit("// === Scene Lights (shared uniform, bound by every mesh at binding 3) ===");
|
|
1193
|
+
for (let i = 0; i < lights.length; i++) {
|
|
1194
|
+
const safeName = lights[i].name.replace(/[\r\n*]+/g, " ");
|
|
1195
|
+
this.emit(`// Light[${i}]: "${safeName}" (${lights[i].kind}, intensity ${lights[i].intensity})`);
|
|
1196
|
+
}
|
|
1197
|
+
this.emit(
|
|
1198
|
+
`const sceneLights = createBuffer(device, new Float32Array([${segs.join(", ")}]), GPUBufferUsage.UNIFORM);`
|
|
1199
|
+
);
|
|
1200
|
+
this.emit("");
|
|
1201
|
+
}
|
|
1202
|
+
// ─── Camera ───────────────────────────────────────────────────────────────
|
|
1203
|
+
// Compute a bounding sphere (center + radius) over every object's world position,
|
|
1204
|
+
// including spatial-group offsets and per-object scale. Used to auto-frame the
|
|
1205
|
+
// camera when the author declared no explicit position. Returns null for an
|
|
1206
|
+
// object-less scene (nothing to frame → keep the neutral default).
|
|
1207
|
+
sceneBoundsForCamera(composition) {
|
|
1208
|
+
const pts = [];
|
|
1209
|
+
const R_CAP = 3;
|
|
1210
|
+
const addObj = (obj, offset) => {
|
|
1211
|
+
const pos = this.findObjProp(obj, "position");
|
|
1212
|
+
const scale = this.findObjProp(obj, "scale") || this.findObjProp(obj, "size");
|
|
1213
|
+
const [px, py, pz] = Array.isArray(pos) ? pos : [0, 0, 0];
|
|
1214
|
+
const rawR = Array.isArray(scale) ? Math.max(...scale.map((n) => Math.abs(n))) : typeof scale === "number" ? Math.abs(scale) : 1;
|
|
1215
|
+
pts.push({ p: [px + offset[0], py + offset[1], pz + offset[2]], r: Math.min(rawR, R_CAP) });
|
|
1216
|
+
};
|
|
1217
|
+
for (const obj of composition.objects ?? []) addObj(obj, [0, 0, 0]);
|
|
1218
|
+
const walkGroup = (g, parent) => {
|
|
1219
|
+
const gp = g.properties?.find((p) => p.key === "position")?.value;
|
|
1220
|
+
const [gx, gy, gz] = Array.isArray(gp) ? gp : [0, 0, 0];
|
|
1221
|
+
const off = [parent[0] + gx, parent[1] + gy, parent[2] + gz];
|
|
1222
|
+
for (const obj of g.objects ?? []) addObj(obj, off);
|
|
1223
|
+
for (const sub of g.groups ?? []) walkGroup(sub, off);
|
|
1224
|
+
};
|
|
1225
|
+
for (const g of composition.spatialGroups ?? []) walkGroup(g, [0, 0, 0]);
|
|
1226
|
+
if (pts.length === 0) return null;
|
|
1227
|
+
const min = [Infinity, Infinity, Infinity];
|
|
1228
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
1229
|
+
for (const { p, r } of pts) {
|
|
1230
|
+
for (let i = 0; i < 3; i++) {
|
|
1231
|
+
min[i] = Math.min(min[i], p[i] - r);
|
|
1232
|
+
max[i] = Math.max(max[i], p[i] + r);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const center = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
|
|
1236
|
+
let radius = 0;
|
|
1237
|
+
for (const { p, r } of pts) {
|
|
1238
|
+
radius = Math.max(radius, Math.hypot(p[0] - center[0], p[1] - center[1], p[2] - center[2]) + r);
|
|
1239
|
+
}
|
|
1240
|
+
return { center, radius: radius || 1 };
|
|
1241
|
+
}
|
|
1242
|
+
emitCamera(composition) {
|
|
1243
|
+
const cam = composition.camera;
|
|
1244
|
+
this.emit("// === Camera ===");
|
|
1245
|
+
let fov = 60, near = 0.1, far = 1e3, pos = [4, 3, 6], lookAt = [0, 0, 0], hasPos = false;
|
|
1246
|
+
for (const prop of cam?.properties ?? []) {
|
|
1247
|
+
if (prop.key === "fov" || prop.key === "field_of_view") fov = prop.value;
|
|
1248
|
+
else if (prop.key === "near") near = prop.value;
|
|
1249
|
+
else if (prop.key === "far") far = prop.value;
|
|
1250
|
+
else if (prop.key === "position" && Array.isArray(prop.value)) {
|
|
1251
|
+
pos = prop.value;
|
|
1252
|
+
hasPos = true;
|
|
1253
|
+
} else if ((prop.key === "look_at" || prop.key === "lookAt") && Array.isArray(prop.value))
|
|
1254
|
+
lookAt = prop.value;
|
|
1255
|
+
}
|
|
1256
|
+
if (!hasPos) {
|
|
1257
|
+
const b = this.sceneBoundsForCamera(composition);
|
|
1258
|
+
if (b) {
|
|
1259
|
+
const r4 = (n) => Number(n.toFixed(4));
|
|
1260
|
+
const fovRad = fov * Math.PI / 180;
|
|
1261
|
+
const dist = b.radius / Math.sin(fovRad / 2) * 1.1;
|
|
1262
|
+
const dl = Math.hypot(0.35, 0.45, 1);
|
|
1263
|
+
const dir = [0.35 / dl, 0.45 / dl, 1 / dl];
|
|
1264
|
+
pos = [
|
|
1265
|
+
r4(b.center[0] + dir[0] * dist),
|
|
1266
|
+
r4(b.center[1] + dir[1] * dist),
|
|
1267
|
+
r4(b.center[2] + dir[2] * dist)
|
|
1268
|
+
];
|
|
1269
|
+
lookAt = [r4(b.center[0]), r4(b.center[1]), r4(b.center[2])];
|
|
1270
|
+
far = Math.max(far, r4(dist + b.radius * 2 + 10));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
this.emit(`const cameraFov = ${fov} * Math.PI / 180;`);
|
|
1274
|
+
this.emit(`const cameraNear = ${near}, cameraFar = ${far};`);
|
|
1275
|
+
this.emit(`const cameraPos = new Float32Array([${pos.join(",")}]);`);
|
|
1276
|
+
this.emit(`const cameraTarget = new Float32Array([${lookAt.join(",")}]);`);
|
|
1277
|
+
this.emit("const cameraUp = new Float32Array([0, 1, 0]);");
|
|
1278
|
+
this.emit("const aspect = (canvas.width || 1) / (canvas.height || 1);");
|
|
1279
|
+
this.emit("function mat4Multiply(a: Float32Array, b: Float32Array): Float32Array {");
|
|
1280
|
+
this.indent();
|
|
1281
|
+
this.emit("const o = new Float32Array(16);");
|
|
1282
|
+
this.emit("for (let c = 0; c < 4; c++) { for (let r = 0; r < 4; r++) {");
|
|
1283
|
+
this.emit(" o[c*4+r] = a[0*4+r]*b[c*4+0] + a[1*4+r]*b[c*4+1] + a[2*4+r]*b[c*4+2] + a[3*4+r]*b[c*4+3];");
|
|
1284
|
+
this.emit("} } return o;");
|
|
1285
|
+
this.dedent();
|
|
1286
|
+
this.emit("}");
|
|
1287
|
+
this.emit("function lookAtMatrix(eye: Float32Array, target: Float32Array, up: Float32Array): Float32Array {");
|
|
1288
|
+
this.indent();
|
|
1289
|
+
this.emit("const zx = eye[0]-target[0], zy = eye[1]-target[1], zz = eye[2]-target[2];");
|
|
1290
|
+
this.emit("let zl = Math.hypot(zx, zy, zz) || 1; const fz = [zx/zl, zy/zl, zz/zl];");
|
|
1291
|
+
this.emit("const xx = up[1]*fz[2]-up[2]*fz[1], xy = up[2]*fz[0]-up[0]*fz[2], xz = up[0]*fz[1]-up[1]*fz[0];");
|
|
1292
|
+
this.emit("let xl = Math.hypot(xx, xy, xz) || 1; const rx = [xx/xl, xy/xl, xz/xl];");
|
|
1293
|
+
this.emit("const uy = [fz[1]*rx[2]-fz[2]*rx[1], fz[2]*rx[0]-fz[0]*rx[2], fz[0]*rx[1]-fz[1]*rx[0]];");
|
|
1294
|
+
this.emit("return new Float32Array([");
|
|
1295
|
+
this.emit(" rx[0], uy[0], fz[0], 0,");
|
|
1296
|
+
this.emit(" rx[1], uy[1], fz[1], 0,");
|
|
1297
|
+
this.emit(" rx[2], uy[2], fz[2], 0,");
|
|
1298
|
+
this.emit(" -(rx[0]*eye[0]+rx[1]*eye[1]+rx[2]*eye[2]),");
|
|
1299
|
+
this.emit(" -(uy[0]*eye[0]+uy[1]*eye[1]+uy[2]*eye[2]),");
|
|
1300
|
+
this.emit(" -(fz[0]*eye[0]+fz[1]*eye[1]+fz[2]*eye[2]), 1]);");
|
|
1301
|
+
this.dedent();
|
|
1302
|
+
this.emit("}");
|
|
1303
|
+
this.emit("function buildViewProjection(): Float32Array {");
|
|
1304
|
+
this.indent();
|
|
1305
|
+
this.emit("const f = 1.0 / Math.tan(cameraFov / 2), ri = 1.0 / (cameraNear - cameraFar);");
|
|
1306
|
+
this.emit("const proj = new Float32Array([f/aspect,0,0,0, 0,f,0,0, 0,0,cameraFar*ri,-1, 0,0,cameraNear*cameraFar*ri,0]);");
|
|
1307
|
+
this.emit("const view = lookAtMatrix(cameraPos, cameraTarget, cameraUp);");
|
|
1308
|
+
this.emit("return mat4Multiply(proj, view);");
|
|
1309
|
+
this.dedent();
|
|
1310
|
+
this.emit("}");
|
|
1311
|
+
this.emit(
|
|
1312
|
+
"const vpUniform = createBuffer(device, buildViewProjection(), GPUBufferUsage.UNIFORM);"
|
|
1313
|
+
);
|
|
1314
|
+
this.emit("");
|
|
1315
|
+
}
|
|
1316
|
+
// ─── Compute Pipelines ────────────────────────────────────────────────────
|
|
1317
|
+
emitComputeShaders(composition) {
|
|
1318
|
+
const gpuObjs = (composition.objects || []).filter(
|
|
1319
|
+
(o) => o.traits?.some(
|
|
1320
|
+
(t) => isGpuParticleTraitName(t.name) || t.name === "gpu_physics" || t.name === "compute"
|
|
1321
|
+
)
|
|
1322
|
+
);
|
|
1323
|
+
if (gpuObjs.length === 0) return;
|
|
1324
|
+
this.emit("// === Compute Pipelines ===");
|
|
1325
|
+
for (const obj of gpuObjs) {
|
|
1326
|
+
const v = this.sanitizeName(obj.name);
|
|
1327
|
+
const traits = obj.traits || [];
|
|
1328
|
+
if (traits.some((t) => isGpuParticleTraitName(t.name))) {
|
|
1329
|
+
const count = findGpuParticleTrait(traits)?.config?.count || 1e4;
|
|
1330
|
+
this.emit(
|
|
1331
|
+
`const ${v}ParticleCompute = device.createComputePipeline({ layout: "auto", compute: { module: device.createShaderModule({ code: WGSL_PARTICLE_COMPUTE }), entryPoint: "cs_particle_update" } });`
|
|
1332
|
+
);
|
|
1333
|
+
this.emit(`const ${v}ComputeDispatches = ${Math.ceil(count / 64)};`);
|
|
1334
|
+
}
|
|
1335
|
+
if (traits.some((t) => t.name === "gpu_physics")) {
|
|
1336
|
+
this.emit(
|
|
1337
|
+
`const ${v}PhysicsCompute = device.createComputePipeline({ layout: "auto", compute: { module: device.createShaderModule({ code: WGSL_PHYSICS_COMPUTE }), entryPoint: "cs_physics_step" } });`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
if (traits.some((t) => t.name === "compute")) {
|
|
1341
|
+
const ct = traits.find((t) => t.name === "compute");
|
|
1342
|
+
const entry = ct?.config?.shader || "custom_compute";
|
|
1343
|
+
const wg = ct?.config?.workgroups || [64, 1, 1];
|
|
1344
|
+
this.emit(
|
|
1345
|
+
`const ${v}CustomCompute = device.createComputePipeline({ layout: "auto", compute: { module: device.createShaderModule({ code: WGSL_CUSTOM_${v.toUpperCase()} }), entryPoint: "${entry}" } });`
|
|
1346
|
+
);
|
|
1347
|
+
this.emit(`const ${v}Workgroups = [${wg}];`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
this.emit("");
|
|
1351
|
+
}
|
|
1352
|
+
// ─── WGSL Shader Sources ──────────────────────────────────────────────────
|
|
1353
|
+
emitShaderSources() {
|
|
1354
|
+
this.emit("// === WGSL Shader Sources ===");
|
|
1355
|
+
this.emit("const WGSL_VERTEX = /* wgsl */`");
|
|
1356
|
+
this.emit("struct Uniforms { model: mat4x4<f32> };");
|
|
1357
|
+
this.emit("struct Camera { viewProj: mat4x4<f32> };");
|
|
1358
|
+
this.emit("@group(0) @binding(0) var<uniform> u: Uniforms;");
|
|
1359
|
+
this.emit("@group(0) @binding(2) var<uniform> cam: Camera;");
|
|
1360
|
+
this.emit("struct VIn { @location(0) pos: vec3<f32>, @location(1) norm: vec3<f32> };");
|
|
1361
|
+
this.emit(
|
|
1362
|
+
"struct VOut { @builtin(position) clip: vec4<f32>, @location(0) wNorm: vec3<f32>, @location(1) wPos: vec3<f32> };"
|
|
1363
|
+
);
|
|
1364
|
+
this.emit("@vertex fn vs_main(i: VIn) -> VOut {");
|
|
1365
|
+
this.emit(" var o: VOut; let w = u.model * vec4<f32>(i.pos, 1.0);");
|
|
1366
|
+
this.emit(
|
|
1367
|
+
" o.clip = cam.viewProj * w; o.wNorm = normalize((u.model * vec4<f32>(i.norm, 0.0)).xyz); o.wPos = w.xyz; return o; }`;"
|
|
1368
|
+
);
|
|
1369
|
+
this.emit("");
|
|
1370
|
+
this.emit("const WGSL_FRAGMENT = /* wgsl */`");
|
|
1371
|
+
this.emit("struct Mat { color: vec4<f32>, rm: vec4<f32>, emissive: vec4<f32> };");
|
|
1372
|
+
this.emit("struct Light { ct: vec4<f32>, posW: vec4<f32>, dirW: vec4<f32> };");
|
|
1373
|
+
this.emit("struct Lights { count: vec4<f32>, items: array<Light, 4> };");
|
|
1374
|
+
this.emit("@group(0) @binding(1) var<uniform> mat: Mat;");
|
|
1375
|
+
this.emit("@group(0) @binding(3) var<uniform> lights: Lights;");
|
|
1376
|
+
this.emit("struct FIn { @location(0) wNorm: vec3<f32>, @location(1) wPos: vec3<f32> };");
|
|
1377
|
+
this.emit("@fragment fn fs_main(i: FIn) -> @location(0) vec4<f32> {");
|
|
1378
|
+
this.emit(" let N = normalize(i.wNorm);");
|
|
1379
|
+
this.emit(" var acc = vec3<f32>(0.0, 0.0, 0.0);");
|
|
1380
|
+
this.emit(" let n = i32(lights.count.x);");
|
|
1381
|
+
this.emit(" for (var k = 0; k < n; k = k + 1) {");
|
|
1382
|
+
this.emit(" let lt = lights.items[k];");
|
|
1383
|
+
this.emit(" var L = normalize(-lt.dirW.xyz);");
|
|
1384
|
+
this.emit(" if (lt.ct.w > 0.5) { L = normalize(lt.posW.xyz - i.wPos); }");
|
|
1385
|
+
this.emit(" let d = max(dot(N, L), 0.0) * (1.0 - mat.rm[0] * 0.5);");
|
|
1386
|
+
this.emit(" acc = acc + lt.ct.rgb * d;");
|
|
1387
|
+
this.emit(" }");
|
|
1388
|
+
this.emit(" let lit = mat.color.rgb * (0.15 + acc);");
|
|
1389
|
+
this.emit(" let emit = mat.emissive.rgb * mat.rm[2];");
|
|
1390
|
+
this.emit(" return vec4<f32>(lit + emit, mat.color.a); }`;");
|
|
1391
|
+
this.emit("");
|
|
1392
|
+
if (this.hasWater) this.emitWaterShaderSource();
|
|
1393
|
+
this.emit("const WGSL_SPLAT = /* wgsl */`");
|
|
1394
|
+
this.emit(
|
|
1395
|
+
"struct Splat { center: vec3<f32>, opacity: f32, cov: array<f32,6>, sh: vec3<f32> };"
|
|
1396
|
+
);
|
|
1397
|
+
this.emit("@group(0) @binding(0) var<storage, read> splats: array<Splat>;");
|
|
1398
|
+
this.emit(
|
|
1399
|
+
"struct SO { @builtin(position) p: vec4<f32>, @location(0) c: vec4<f32>, @location(1) uv: vec2<f32> };"
|
|
1400
|
+
);
|
|
1401
|
+
this.emit(
|
|
1402
|
+
"@vertex fn vs_splat(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> SO {"
|
|
1403
|
+
);
|
|
1404
|
+
this.emit(" var q = array<vec2<f32>,4>(vec2(-1,-1),vec2(1,-1),vec2(-1,1),vec2(1,1));");
|
|
1405
|
+
this.emit(
|
|
1406
|
+
" var o: SO; let s = splats[ii]; o.p = vec4<f32>(s.center.xy + q[vi]*0.01, s.center[2], 1.0);"
|
|
1407
|
+
);
|
|
1408
|
+
this.emit(" o.c = vec4<f32>(s.sh, s.opacity); o.uv = q[vi]; return o; }");
|
|
1409
|
+
this.emit("@fragment fn fs_splat(i: SO) -> @location(0) vec4<f32> {");
|
|
1410
|
+
this.emit(" let a = i.c.a * exp(-dot(i.uv, i.uv) * 4.0); if (a < 0.01) { discard; }");
|
|
1411
|
+
this.emit(" return vec4<f32>(i.c.rgb * a, a); }`;");
|
|
1412
|
+
this.emit("");
|
|
1413
|
+
this.emit("const WGSL_POINT = /* wgsl */`");
|
|
1414
|
+
this.emit("struct PO { @builtin(position) p: vec4<f32>, @location(0) c: vec4<f32> };");
|
|
1415
|
+
this.emit(
|
|
1416
|
+
"@vertex fn vs_point(@location(0) pos: vec3<f32>, @location(1) col: vec4<f32>) -> PO { var o: PO; o.p = vec4<f32>(pos, 1.0); o.c = col; return o; }"
|
|
1417
|
+
);
|
|
1418
|
+
this.emit("@fragment fn fs_point(i: PO) -> @location(0) vec4<f32> { return i.c; }`;");
|
|
1419
|
+
this.emit("");
|
|
1420
|
+
this.emit("const WGSL_PARTICLE_RENDER = /* wgsl */`");
|
|
1421
|
+
this.emit("struct PVO { @builtin(position) p: vec4<f32>, @location(0) life: f32 };");
|
|
1422
|
+
this.emit(
|
|
1423
|
+
"@vertex fn vs_particle(@location(0) pos: vec3<f32>, @location(1) vel: vec3<f32>, @location(2) life: f32) -> PVO { var o: PVO; o.p = vec4<f32>(pos, 1.0); o.life = life; return o; }"
|
|
1424
|
+
);
|
|
1425
|
+
this.emit("@fragment fn fs_particle(i: PVO) -> @location(0) vec4<f32> {");
|
|
1426
|
+
this.emit(
|
|
1427
|
+
" let t = clamp(i.life, 0.0, 1.0); return vec4<f32>(mix(vec3(1.0,0.2,0.0), vec3(1.0,1.0,0.5), t), t*0.8); }`;"
|
|
1428
|
+
);
|
|
1429
|
+
this.emit("");
|
|
1430
|
+
this.emit("const WGSL_SCATTER = /* wgsl */`");
|
|
1431
|
+
this.emit("struct VIn { @location(0) pos: vec3<f32>, @location(1) norm: vec3<f32> };");
|
|
1432
|
+
this.emit("struct VOut { @builtin(position) clip: vec4<f32>, @location(0) shade: f32 };");
|
|
1433
|
+
this.emit("fn hash11(v: f32) -> f32 { return fract(sin(v * 12.9898) * 43758.5453); }");
|
|
1434
|
+
this.emit("@vertex fn vs_scatter(i: VIn, @builtin(instance_index) instance: u32) -> VOut {");
|
|
1435
|
+
this.emit(" let id = f32(instance);");
|
|
1436
|
+
this.emit(" let x = hash11(id + 1.0) * 1.8 - 0.9; let z = hash11(id + 7.0) * 1.8 - 0.9;");
|
|
1437
|
+
this.emit(" let scale = 0.35 + hash11(id + 13.0) * 0.45;");
|
|
1438
|
+
this.emit(" var o: VOut; o.clip = vec4<f32>(i.pos.x * scale + x, i.pos.y * scale - 0.65, i.pos.z * scale + z, 1.0);");
|
|
1439
|
+
this.emit(" o.shade = 0.45 + max(i.norm.y, 0.0) * 0.45; return o; }");
|
|
1440
|
+
this.emit("@fragment fn fs_scatter(i: VOut) -> @location(0) vec4<f32> { return vec4<f32>(0.08 * i.shade, 0.34 * i.shade, 0.12 * i.shade, 1.0); }`;");
|
|
1441
|
+
this.emit("");
|
|
1442
|
+
this.emit("const WGSL_POSTFX = /* wgsl */`");
|
|
1443
|
+
this.emit("struct VO { @builtin(position) pos: vec4<f32> };");
|
|
1444
|
+
this.emit("@vertex fn vs_postfx(@builtin(vertex_index) vi: u32) -> VO {");
|
|
1445
|
+
this.emit(" var p = array<vec2<f32>,3>(vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));");
|
|
1446
|
+
this.emit(" var o: VO; o.pos = vec4<f32>(p[vi], 0.0, 1.0); return o; }");
|
|
1447
|
+
this.emit("@fragment fn fs_postfx() -> @location(0) vec4<f32> { return vec4<f32>(0.04, 0.08, 0.05, 0.18); }`;");
|
|
1448
|
+
this.emit("");
|
|
1449
|
+
this.emit("const WGSL_PARTICLE_COMPUTE = /* wgsl */`");
|
|
1450
|
+
this.emit("struct P { pos: vec3<f32>, _p0: f32, vel: vec3<f32>, life: f32 };");
|
|
1451
|
+
this.emit("@group(0) @binding(0) var<storage, read> pin: array<P>;");
|
|
1452
|
+
this.emit("@group(0) @binding(1) var<storage, read_write> pout: array<P>;");
|
|
1453
|
+
this.emit("@group(0) @binding(2) var<uniform> dt: f32;");
|
|
1454
|
+
this.emit(
|
|
1455
|
+
"@compute @workgroup_size(64) fn cs_particle_update(@builtin(global_invocation_id) gid: vec3<u32>) {"
|
|
1456
|
+
);
|
|
1457
|
+
this.emit(" let i = gid[0]; if (i >= arrayLength(&pin)) { return; } var p = pin[i];");
|
|
1458
|
+
this.emit(" p.vel[1] -= 9.81 * dt; p.pos += p.vel * dt; p.life -= dt;");
|
|
1459
|
+
this.emit(
|
|
1460
|
+
" if (p.life <= 0.0) { p.pos = vec3(0.0); p.vel = vec3(0.0, 5.0, 0.0); p.life = 2.0; }"
|
|
1461
|
+
);
|
|
1462
|
+
this.emit(" pout[i] = p; }`;");
|
|
1463
|
+
this.emit("");
|
|
1464
|
+
this.emit("const WGSL_PHYSICS_COMPUTE = /* wgsl */`");
|
|
1465
|
+
this.emit("struct Body { pos: vec3<f32>, mass: f32, vel: vec3<f32>, _p: f32 };");
|
|
1466
|
+
this.emit("@group(0) @binding(0) var<storage, read_write> bodies: array<Body>;");
|
|
1467
|
+
this.emit("@group(0) @binding(1) var<uniform> dt: f32;");
|
|
1468
|
+
this.emit(
|
|
1469
|
+
"@compute @workgroup_size(64) fn cs_physics_step(@builtin(global_invocation_id) gid: vec3<u32>) {"
|
|
1470
|
+
);
|
|
1471
|
+
this.emit(" let i = gid[0]; if (i >= arrayLength(&bodies)) { return; } var b = bodies[i];");
|
|
1472
|
+
this.emit(" b.vel[1] -= 9.81 * dt; b.pos += b.vel * dt;");
|
|
1473
|
+
this.emit(
|
|
1474
|
+
" if (b.pos[1] < 0.0) { b.pos[1] = 0.0; b.vel[1] = -b.vel[1] * 0.6; } bodies[i] = b; }`;"
|
|
1475
|
+
);
|
|
1476
|
+
this.emit("");
|
|
1477
|
+
}
|
|
1478
|
+
// ─── Render Loop ──────────────────────────────────────────────────────────
|
|
1479
|
+
emitViewportRuntime() {
|
|
1480
|
+
this.emit("// === HoloGraph Viewport Runtime ===");
|
|
1481
|
+
this.emit("const holoGraphInitialCameraPos = new Float32Array(cameraPos);");
|
|
1482
|
+
this.emit("const holoGraphInitialCameraTarget = new Float32Array(cameraTarget);");
|
|
1483
|
+
this.emit(
|
|
1484
|
+
'const holoGraphViewportState = { actionCount: 0, lastAction: null, filter: "all", colorMode: "semantic", structureStyle: "flow", labelPolicy: "none", focusId: null };'
|
|
1485
|
+
);
|
|
1486
|
+
this.emit('document.body.style.position = document.body.style.position || "relative";');
|
|
1487
|
+
this.emit('const holoGraphLabelLayer = document.createElement("div");');
|
|
1488
|
+
this.emit('holoGraphLabelLayer.setAttribute("data-holograph-label-layer", "true");');
|
|
1489
|
+
this.emit('Object.assign(holoGraphLabelLayer.style, { position: "absolute", inset: "0", pointerEvents: "none", overflow: "hidden", fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace", zIndex: "5" });');
|
|
1490
|
+
this.emit('canvas.insertAdjacentElement("afterend", holoGraphLabelLayer);');
|
|
1491
|
+
this.emit("function hsVec3(v) { return [Number(v[0] || 0), Number(v[1] || 0), Number(v[2] || 0)]; }");
|
|
1492
|
+
this.emit("function hsSub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }");
|
|
1493
|
+
this.emit("function hsAdd(a, b) { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }");
|
|
1494
|
+
this.emit("function hsMul(v, s) { return [v[0] * s, v[1] * s, v[2] * s]; }");
|
|
1495
|
+
this.emit("function hsLen(v) { return Math.hypot(v[0], v[1], v[2]) || 1; }");
|
|
1496
|
+
this.emit("function hsNorm(v) { const l = hsLen(v); return [v[0] / l, v[1] / l, v[2] / l]; }");
|
|
1497
|
+
this.emit(
|
|
1498
|
+
"function hsCross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; }"
|
|
1499
|
+
);
|
|
1500
|
+
this.emit(
|
|
1501
|
+
'function hsHash(text) { let h = 2166136261; const s = String(text || ""); for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }'
|
|
1502
|
+
);
|
|
1503
|
+
this.emit(
|
|
1504
|
+
"function hsHueRgb(h) { const x = (n) => { const k = (n + h * 6) % 6; return Math.max(0, Math.min(1, Math.min(k, 4 - k, 1))); }; return [x(5), x(3), x(1)]; }"
|
|
1505
|
+
);
|
|
1506
|
+
this.emit(
|
|
1507
|
+
"function hsGraphCenter() { if (!holoGraphObjects.length) return [0, 0, 0]; const sum = holoGraphObjects.reduce((acc, o) => [acc[0] + o.basePosition[0], acc[1] + o.basePosition[1], acc[2] + o.basePosition[2]], [0, 0, 0]); return hsMul(sum, 1 / holoGraphObjects.length); }"
|
|
1508
|
+
);
|
|
1509
|
+
this.emit(
|
|
1510
|
+
"function hsWriteModel(o) { const s = o.visible ? o.scale : [0, 0, 0]; device.queue.writeBuffer(o.modelBuffer, 0, new Float32Array([s[0],0,0,0, 0,s[1],0,0, 0,0,s[2],0, o.position[0],o.position[1],o.position[2],1])); }"
|
|
1511
|
+
);
|
|
1512
|
+
this.emit(
|
|
1513
|
+
"function hsWriteMaterial(o, color) { o.color = [color[0], color[1], color[2]]; const m = o.material; device.queue.writeBuffer(o.matBuffer, 0, new Float32Array([color[0],color[1],color[2],1.0, m.roughness,m.metalness,m.emissiveStrength,0, m.emissive[0],m.emissive[1],m.emissive[2],0])); }"
|
|
1514
|
+
);
|
|
1515
|
+
this.emit("function hsCameraDistance() { return hsLen(hsSub(hsVec3(cameraTarget), hsVec3(cameraPos))); }");
|
|
1516
|
+
this.emit(
|
|
1517
|
+
"function hsSetCamera(pos, target) { cameraPos.set(pos); cameraTarget.set(target); device.queue.writeBuffer(vpUniform, 0, buildViewProjection()); document.body.dataset.holoscriptWebgpuCamera = JSON.stringify({ position: Array.from(cameraPos), target: Array.from(cameraTarget) }); }"
|
|
1518
|
+
);
|
|
1519
|
+
this.emit(
|
|
1520
|
+
"function hsCameraBasis() { const forward = hsNorm(hsSub(hsVec3(cameraTarget), hsVec3(cameraPos))); const worldUp = [0, 1, 0]; const right = hsNorm(hsCross(forward, worldUp)); const up = hsNorm(hsCross(right, forward)); return { forward, right, up }; }"
|
|
1521
|
+
);
|
|
1522
|
+
this.emit(
|
|
1523
|
+
'function hsMatchesFilter(o, filter) { const rawKind = o.properties.symbolType || o.properties.symbol_type || o.properties.kind || o.properties.role || o.properties.type || ""; const symbolType = String(rawKind).toLowerCase(); const traits = (o.traits || []).map((t) => String(t).toLowerCase()); if (filter === "all") return true; if (filter === "call") return symbolType === "function" || symbolType === "method" || symbolType === "call" || traits.includes("function") || traits.includes("method") || traits.includes("call"); if (filter === "import") return symbolType === "module" || symbolType === "namespace" || symbolType === "package" || symbolType === "import" || traits.includes("module") || traits.includes("namespace") || traits.includes("package") || traits.includes("import"); return true; }'
|
|
1524
|
+
);
|
|
1525
|
+
this.emit(
|
|
1526
|
+
'function hsColorForMode(o, mode) { if (mode === "identity_hue") return hsHueRgb((hsHash(o.id) % 10000) / 10000); if (mode === "file_hue") return hsHueRgb((hsHash(o.properties.file || o.properties.path || o.id) % 10000) / 10000); if (mode === "relation_hue") return hsHueRgb((hsHash(o.properties.symbolType || o.properties.symbol_type || o.properties.kind || o.properties.role || (o.traits || []).join(",")) % 10000) / 10000); return o.baseColor; }'
|
|
1527
|
+
);
|
|
1528
|
+
this.emit("function hsRound(n) { return Math.round(Number(n || 0) * 1000) / 1000; }");
|
|
1529
|
+
this.emit("function hsRoundVec(v) { return [hsRound(v?.[0]), hsRound(v?.[1]), hsRound(v?.[2])]; }");
|
|
1530
|
+
this.emit(
|
|
1531
|
+
'function hsHueBucket(o, mode = holoGraphViewportState.colorMode) { if (mode === "identity_hue") return hsHash(o.id) % 10000; if (mode === "file_hue") return hsHash(o.properties.file || o.properties.path || o.id) % 10000; if (mode === "relation_hue") return hsHash(o.properties.symbolType || o.properties.symbol_type || o.properties.kind || o.properties.role || (o.traits || []).join(",")) % 10000; return hsHash(JSON.stringify(o.color || o.baseColor || [])) % 10000; }'
|
|
1532
|
+
);
|
|
1533
|
+
this.emit(
|
|
1534
|
+
'function hsObjectRole(o) { return String(o.properties.symbolType || o.properties.symbol_type || o.properties.kind || o.properties.role || o.properties.type || (o.traits || [])[0] || "object"); }'
|
|
1535
|
+
);
|
|
1536
|
+
this.emit(
|
|
1537
|
+
'function hsCounts(items, keyFn, limit = 12) { const m = new Map(); for (const item of items) { const key = String(keyFn(item) || "unknown"); m.set(key, (m.get(key) || 0) + 1); } return Array.from(m.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([key, count]) => ({ key, count })); }'
|
|
1538
|
+
);
|
|
1539
|
+
this.emit(
|
|
1540
|
+
'function hsCompactObject(o, index) { return { index, id: o.id, name: o.name, geometry: o.geometry || "mesh", shape: o.geometry || "mesh", role: hsObjectRole(o), traits: o.traits || [], file: o.properties.file || null, path: o.properties.path || null, signature: o.properties.signature || null, language: o.properties.language || null, position: hsRoundVec(o.position), scale: hsRoundVec(o.scale), color: hsRoundVec(o.color || o.baseColor), hueBucket: hsHueBucket(o), visible: o.visible, projection: hsProjectPoint(o.position) }; }'
|
|
1541
|
+
);
|
|
1542
|
+
this.emit(
|
|
1543
|
+
'function hsResolveObject(payload = {}) { const key = payload && (payload.nodeId ?? payload.id ?? payload.target ?? payload.anchorId ?? payload.name); let obj = null; if (typeof payload?.anchorIndex === "number") obj = holoGraphObjects[payload.anchorIndex] || null; if (!obj && typeof key === "number") obj = holoGraphObjects[key] || null; if (!obj && key !== undefined && key !== null) obj = holoGraphObjects.find((o) => o.id === String(key) || o.name === String(key)); if (!obj) obj = holoGraphObjects.find((o) => o.visible) || holoGraphObjects[0] || null; return obj; }'
|
|
1544
|
+
);
|
|
1545
|
+
this.emit(
|
|
1546
|
+
"function hsNearestObjects(obj, limit = 8) { if (!obj) return []; return holoGraphObjects.filter((o) => o.visible && o !== obj).map((o) => ({ object: o, distance: hsLen(hsSub(o.position, obj.position)) })).sort((a, b) => a.distance - b.distance).slice(0, Math.max(0, Math.min(32, Number(limit) || 8))).map((entry) => ({ ...hsCompactObject(entry.object, holoGraphObjects.indexOf(entry.object)), distance: hsRound(entry.distance) })); }"
|
|
1547
|
+
);
|
|
1548
|
+
this.emit(
|
|
1549
|
+
'function hsDescribeScene(payload = {}) { const limit = Math.max(1, Math.min(128, Number(payload.limit || 24))); const visible = holoGraphObjects.filter((o) => o.visible); return { canonicalNames: { graphSurface: "HoloGraph", embeddingTower: "HoloEmbed", visionNavigator: "HoloLlama" }, actionContract: ["pan_left","pan_right","pan_up","pan_down","zoom_in","zoom_out","zoom_to_fit","focus_anchor","focus_node","filter_call","filter_import","filter_all","set_color_mode","set_structure_style","set_label_policy","reset_view"], projectionBridge: "renderer-native", canvas: { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight }, camera: { position: hsRoundVec(cameraPos), target: hsRoundVec(cameraTarget), distance: hsRound(hsCameraDistance()) }, actionCount: holoGraphViewportState.actionCount, lastAction: holoGraphViewportState.lastAction, filter: holoGraphViewportState.filter, colorMode: holoGraphViewportState.colorMode, structureStyle: holoGraphViewportState.structureStyle, labelPolicy: holoGraphViewportState.labelPolicy, objectCount: holoGraphObjects.length, visibleObjects: visible.length, shapes: hsCounts(visible, (o) => o.geometry || "mesh"), roles: hsCounts(visible, hsObjectRole), hueBuckets: hsCounts(visible, (o) => `hue_${hsHueBucket(o)}`, 16), files: hsCounts(visible, (o) => o.properties.file || o.properties.path || "unknown", 16), objects: visible.slice(0, limit).map((o) => hsCompactObject(o, holoGraphObjects.indexOf(o))) }; }'
|
|
1550
|
+
);
|
|
1551
|
+
this.emit(
|
|
1552
|
+
"function hsProjectObject(payload = {}) { const obj = hsResolveObject(payload); if (!obj) return null; return { ...hsCompactObject(obj, holoGraphObjects.indexOf(obj)), cameraDistance: hsRound(hsLen(hsSub(hsVec3(cameraPos), obj.position))) }; }"
|
|
1553
|
+
);
|
|
1554
|
+
this.emit(
|
|
1555
|
+
"function hsInspectObject(payload = {}) { const obj = hsResolveObject(payload); if (!obj) return null; return { ...hsProjectObject(payload), properties: obj.properties, neighbors: hsNearestObjects(obj, payload.limit || 8) }; }"
|
|
1556
|
+
);
|
|
1557
|
+
this.emit('function hsNormalizeLabelPolicy(value) { const policy = String(value || "none").toLowerCase(); return ["none","ids","signatures","details"].includes(policy) ? policy : "none"; }');
|
|
1558
|
+
this.emit(
|
|
1559
|
+
'function hsLabelText(o) { const policy = holoGraphViewportState.labelPolicy; if (policy === "none") return ""; if (policy === "ids") return o.id; if (policy === "signatures") return o.properties.signature || o.id; return `${hsObjectRole(o)}:${o.properties.signature || o.id}`; }'
|
|
1560
|
+
);
|
|
1561
|
+
this.emit(
|
|
1562
|
+
'function hsProjectPoint(p) { const m = buildViewProjection(); const x = Number(p?.[0] || 0), y = Number(p?.[1] || 0), z = Number(p?.[2] || 0); const cx = m[0]*x + m[4]*y + m[8]*z + m[12]; const cy = m[1]*x + m[5]*y + m[9]*z + m[13]; const cz = m[2]*x + m[6]*y + m[10]*z + m[14]; const cw = m[3]*x + m[7]*y + m[11]*z + m[15]; const clip = { x: hsRound(cx), y: hsRound(cy), z: hsRound(cz), w: hsRound(cw) }; if (!Number.isFinite(cw) || Math.abs(cw) < 0.00001) return { source: "renderer-native", valid: false, reason: "invalid-w", inFrame: false, clip }; const ndcX = cx / cw, ndcY = cy / cw, ndcZ = cz / cw; const normalizedX = ndcX * 0.5 + 0.5; const normalizedY = 0.5 - ndcY * 0.5; const width = canvas.clientWidth || canvas.width || 1; const height = canvas.clientHeight || canvas.height || 1; const screenX = normalizedX * width; const screenY = normalizedY * height; const centerDistance = Math.hypot(normalizedX - 0.5, normalizedY - 0.5); return { source: "renderer-native", valid: Number.isFinite(normalizedX) && Number.isFinite(normalizedY), inFrame: ndcX >= -1 && ndcX <= 1 && ndcY >= -1 && ndcY <= 1, ndc: { x: hsRound(ndcX), y: hsRound(ndcY), z: hsRound(ndcZ) }, normalizedX: hsRound(normalizedX), normalizedY: hsRound(normalizedY), screenX: hsRound(screenX), screenY: hsRound(screenY), centerDistance: hsRound(centerDistance), clip }; }'
|
|
1563
|
+
);
|
|
1564
|
+
this.emit(
|
|
1565
|
+
'function hsRenderLabels() { const policy = holoGraphViewportState.labelPolicy; document.body.dataset.holoscriptWebgpuLabels = policy; if (policy === "none") { holoGraphLabelLayer.replaceChildren(); return; } const ranked = holoGraphObjects.filter((o) => o.visible).map((o) => ({ object: o, distance: hsLen(hsSub(o.position, hsVec3(cameraTarget))) })).sort((a, b) => a.distance - b.distance).slice(0, 18); const nodes = []; for (const entry of ranked) { const o = entry.object; const text = hsLabelText(o); const point = hsProjectPoint(o.position); if (!text || !point?.valid || !point.inFrame) continue; const focus = entry.distance <= 0.001 || o.id === holoGraphViewportState.focusId; const el = document.createElement("div"); el.textContent = text.length > 72 ? `${text.slice(0, 69)}...` : text; Object.assign(el.style, { position: "absolute", left: `${point.screenX}px`, top: `${point.screenY}px`, transform: "translate(-50%, -120%)", maxWidth: "280px", padding: focus ? "4px 7px" : "3px 6px", border: focus ? "2px solid rgba(250,204,21,0.98)" : "1px solid rgba(255,255,255,0.68)", borderRadius: "3px", background: focus ? "rgba(15,23,42,0.94)" : "rgba(2,6,23,0.86)", color: focus ? "#fef9c3" : "#f8fafc", fontSize: focus ? "13px" : "12px", fontWeight: focus ? "700" : "600", lineHeight: "1.2", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", textShadow: "0 1px 2px #000" }); nodes.push(el); } const focusObj = holoGraphViewportState.focusId ? holoGraphObjects.find((o) => o.id === holoGraphViewportState.focusId && o.visible) : null; if (focusObj) { const panel = document.createElement("div"); Object.assign(panel.style, { position: "absolute", left: "12px", top: "12px", maxWidth: "560px", padding: "8px 10px", border: "2px solid rgba(250,204,21,0.98)", borderRadius: "4px", background: "rgba(15,23,42,0.96)", color: "#fef9c3", fontSize: "13px", fontWeight: "700", lineHeight: "1.35", textShadow: "0 1px 2px #000", boxShadow: "0 10px 30px rgba(0,0,0,0.34)" }); const near = holoGraphObjects.filter((o) => o.visible && o !== focusObj).map((o) => ({ object: o, distance: hsLen(hsSub(o.position, focusObj.position)) })).sort((a, b) => a.distance - b.distance).slice(0, 5); const rows = [{ kind: "target", object: focusObj }, ...near.map((entry) => ({ kind: "near", object: entry.object }))]; for (const row of rows) { const line = document.createElement("div"); const text = `${row.kind} ${hsLabelText(row.object)}`; line.textContent = text.length > 96 ? `${text.slice(0, 93)}...` : text; panel.appendChild(line); } nodes.push(panel); } holoGraphLabelLayer.replaceChildren(...nodes); }'
|
|
1566
|
+
);
|
|
1567
|
+
this.emit("function hsApplyLabelPolicy(policy) { holoGraphViewportState.labelPolicy = hsNormalizeLabelPolicy(policy); hsRenderLabels(); }");
|
|
1568
|
+
this.emit("function hsApplyFilter(filter) { holoGraphViewportState.filter = filter; for (const o of holoGraphObjects) { o.visible = hsMatchesFilter(o, filter); hsWriteModel(o); } }");
|
|
1569
|
+
this.emit("function hsApplyColorMode(mode) { holoGraphViewportState.colorMode = mode; for (const o of holoGraphObjects) hsWriteMaterial(o, hsColorForMode(o, mode)); }");
|
|
1570
|
+
this.emit(
|
|
1571
|
+
'function hsApplyStructureStyle(style) { const center = hsGraphCenter(); const n = Math.max(1, holoGraphObjects.length); const radius = Math.max(20, Math.sqrt(n) * 18); holoGraphViewportState.structureStyle = style; holoGraphObjects.forEach((o, i) => { if (style === "compact") { o.position = hsAdd(center, hsMul(hsSub(o.basePosition, center), 0.45)); } else if (style === "radial") { const a = (i / n) * Math.PI * 2; o.position = [center[0] + Math.cos(a) * radius, center[1] + (o.basePosition[1] - center[1]) * 0.18, center[2] + Math.sin(a) * radius]; } else { o.position = [...o.basePosition]; } hsWriteModel(o); }); }'
|
|
1572
|
+
);
|
|
1573
|
+
this.emit(
|
|
1574
|
+
"function hsFocusObject(payload) { const obj = hsResolveObject(payload); if (!obj) return false; holoGraphViewportState.focusId = obj.id; const target = [...obj.position]; const away = hsNorm(hsSub(hsVec3(cameraPos), hsVec3(cameraTarget))); const distance = Math.max(8, hsCameraDistance() * 0.45); hsSetCamera(hsAdd(target, hsMul(away, distance)), target); return true; }"
|
|
1575
|
+
);
|
|
1576
|
+
this.emit(
|
|
1577
|
+
'function hsViewportSnapshot() { const scene = hsDescribeScene({ limit: 8 }); return { camera: { position: Array.from(cameraPos), target: Array.from(cameraTarget), distance: hsCameraDistance() }, projectionBridge: "renderer-native", canvas: scene.canvas, actionCount: holoGraphViewportState.actionCount, lastAction: holoGraphViewportState.lastAction, filter: holoGraphViewportState.filter, colorMode: holoGraphViewportState.colorMode, structureStyle: holoGraphViewportState.structureStyle, labelPolicy: holoGraphViewportState.labelPolicy, focusId: holoGraphViewportState.focusId, objectCount: scene.objectCount, visibleObjects: scene.visibleObjects, sceneSummary: { shapes: scene.shapes, roles: scene.roles, hueBuckets: scene.hueBuckets, files: scene.files }, sampleObjects: scene.objects }; }'
|
|
1578
|
+
);
|
|
1579
|
+
this.emit("function hsApplyViewportAction(action, payload = {}) {");
|
|
1580
|
+
this.indent();
|
|
1581
|
+
this.emit('const type = typeof action === "string" ? action : String(action?.type || action?.action || "");');
|
|
1582
|
+
this.emit('const data = typeof action === "object" && action !== null ? { ...action, ...payload } : payload || {};');
|
|
1583
|
+
this.emit("const before = hsViewportSnapshot();");
|
|
1584
|
+
this.emit("const basis = hsCameraBasis();");
|
|
1585
|
+
this.emit("const distance = hsCameraDistance();");
|
|
1586
|
+
this.emit("const pan = Math.max(1, distance * 0.08);");
|
|
1587
|
+
this.emit("let ok = true;");
|
|
1588
|
+
this.emit('if (type === "pan_left") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.right, -pan)), hsAdd(hsVec3(cameraTarget), hsMul(basis.right, -pan)));');
|
|
1589
|
+
this.emit('else if (type === "pan_right") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.right, pan)), hsAdd(hsVec3(cameraTarget), hsMul(basis.right, pan)));');
|
|
1590
|
+
this.emit('else if (type === "pan_up") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.up, pan)), hsAdd(hsVec3(cameraTarget), hsMul(basis.up, pan)));');
|
|
1591
|
+
this.emit('else if (type === "pan_down") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.up, -pan)), hsAdd(hsVec3(cameraTarget), hsMul(basis.up, -pan)));');
|
|
1592
|
+
this.emit('else if (type === "zoom_in") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.forward, distance * 0.22)), hsVec3(cameraTarget));');
|
|
1593
|
+
this.emit('else if (type === "zoom_out") hsSetCamera(hsAdd(hsVec3(cameraPos), hsMul(basis.forward, -distance * 0.25)), hsVec3(cameraTarget));');
|
|
1594
|
+
this.emit('else if (type === "zoom_to_fit" || type === "reset_view") { holoGraphViewportState.focusId = null; hsSetCamera(Array.from(holoGraphInitialCameraPos), Array.from(holoGraphInitialCameraTarget)); }');
|
|
1595
|
+
this.emit('else if (type === "focus_anchor" || type === "focus_node") ok = hsFocusObject(data);');
|
|
1596
|
+
this.emit('else if (type === "filter_call") hsApplyFilter("call");');
|
|
1597
|
+
this.emit('else if (type === "filter_import") hsApplyFilter("import");');
|
|
1598
|
+
this.emit('else if (type === "filter_all") hsApplyFilter("all");');
|
|
1599
|
+
this.emit('else if (type === "set_color_mode") hsApplyColorMode(String(data.mode || data.colorMode || "semantic"));');
|
|
1600
|
+
this.emit('else if (type === "set_structure_style") hsApplyStructureStyle(String(data.style || data.structureStyle || "flow"));');
|
|
1601
|
+
this.emit('else if (type === "set_label_policy") hsApplyLabelPolicy(data.policy || data.labelPolicy || "none");');
|
|
1602
|
+
this.emit("else ok = false;");
|
|
1603
|
+
this.emit("if (!ok) return { ok: false, action: type, before, after: hsViewportSnapshot() };");
|
|
1604
|
+
this.emit("holoGraphViewportState.actionCount++;");
|
|
1605
|
+
this.emit("holoGraphViewportState.lastAction = type;");
|
|
1606
|
+
this.emit("document.body.dataset.holoscriptWebgpuLastAction = type;");
|
|
1607
|
+
this.emit("return { ok: true, action: type, before, after: hsViewportSnapshot() };");
|
|
1608
|
+
this.dedent();
|
|
1609
|
+
this.emit("}");
|
|
1610
|
+
this.emit("globalThis.holoscriptWebgpuViewport = { applyAction: hsApplyViewportAction, getState: hsViewportSnapshot, describeScene: hsDescribeScene, inspectObject: hsInspectObject, projectObject: hsProjectObject, projectPoint: hsProjectPoint };");
|
|
1611
|
+
this.emit("globalThis.HoloGraphViewport = globalThis.holoscriptWebgpuViewport;");
|
|
1612
|
+
this.emit('document.body.dataset.holoscriptWebgpuViewport = "ready";');
|
|
1613
|
+
this.emit('document.body.dataset.holoscriptWebgpuInspector = "ready";');
|
|
1614
|
+
this.emit('document.body.dataset.holoscriptWebgpuLabels = "none";');
|
|
1615
|
+
this.emit("hsSetCamera(Array.from(cameraPos), Array.from(cameraTarget));");
|
|
1616
|
+
this.emit("");
|
|
1617
|
+
}
|
|
1618
|
+
emitRenderLoop(composition) {
|
|
1619
|
+
this.emit("// === Render Loop ===");
|
|
1620
|
+
this.emit("let frameCount = 0;");
|
|
1621
|
+
this.emit("const t0 = performance.now();");
|
|
1622
|
+
this.emit("function frame() {");
|
|
1623
|
+
this.indent();
|
|
1624
|
+
this.emit("frameCount++;");
|
|
1625
|
+
this.emit("const time = (performance.now() - t0) / 1000.0;");
|
|
1626
|
+
this.emit("device.queue.writeBuffer(vpUniform, 0, buildViewProjection());");
|
|
1627
|
+
if (this.hasWater) {
|
|
1628
|
+
this.emit(
|
|
1629
|
+
`device.queue.writeBuffer(waterFrameU, 0, new Float32Array([time, cameraPos[0], cameraPos[1], cameraPos[2], hsChannels[${JSON.stringify(this.waterClarityChannel)}], hsChannels[${JSON.stringify(this.waterRippleChannel)}], hsWaterAmp, 0]));`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
this.emit("hsRenderLabels();");
|
|
1633
|
+
this.emit("const enc = device.createCommandEncoder();");
|
|
1634
|
+
const gpuObjs = (composition.objects || []).filter(
|
|
1635
|
+
(o) => o.traits?.some(
|
|
1636
|
+
(t) => isGpuParticleTraitName(t.name) || t.name === "gpu_physics" || t.name === "compute"
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1639
|
+
if (gpuObjs.length > 0 && this.options.enableCompute) {
|
|
1640
|
+
this.emit("const cp = enc.beginComputePass();");
|
|
1641
|
+
for (const obj of gpuObjs) {
|
|
1642
|
+
const v = this.sanitizeName(obj.name);
|
|
1643
|
+
if (obj.traits?.some((t) => isGpuParticleTraitName(t.name))) {
|
|
1644
|
+
this.emit(
|
|
1645
|
+
`cp.setPipeline(${v}ParticleCompute); cp.dispatchWorkgroups(${v}ComputeDispatches);`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
if (obj.traits?.some((t) => t.name === "gpu_physics")) {
|
|
1649
|
+
this.emit(`cp.setPipeline(${v}PhysicsCompute); cp.dispatchWorkgroups(16);`);
|
|
1650
|
+
}
|
|
1651
|
+
if (obj.traits?.some((t) => t.name === "compute")) {
|
|
1652
|
+
this.emit(`cp.setPipeline(${v}CustomCompute); cp.dispatchWorkgroups(...${v}Workgroups);`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
this.emit("cp.end();");
|
|
1656
|
+
}
|
|
1657
|
+
this.emit("const rp = enc.beginRenderPass({");
|
|
1658
|
+
this.indent();
|
|
1659
|
+
this.emit(
|
|
1660
|
+
'colorAttachments: [{ view: msaaTexture.createView(), resolveTarget: context.getCurrentTexture().createView(), clearValue: clearColor, loadOp: "clear", storeOp: "store" }],'
|
|
1661
|
+
);
|
|
1662
|
+
this.emit(
|
|
1663
|
+
'depthStencilAttachment: { view: depthTexture.createView(), depthClearValue: 1.0, depthLoadOp: "clear", depthStoreOp: "store" },'
|
|
1664
|
+
);
|
|
1665
|
+
this.dedent();
|
|
1666
|
+
this.emit("});");
|
|
1667
|
+
if (composition.objects) {
|
|
1668
|
+
for (const obj of composition.objects) this.emitDrawCall(obj);
|
|
1669
|
+
}
|
|
1670
|
+
if (composition.spatialGroups) {
|
|
1671
|
+
for (const group of composition.spatialGroups) {
|
|
1672
|
+
for (const obj of group.objects) this.emitDrawCall(obj);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
this.emitDomainBlockDrawCalls(composition, false);
|
|
1676
|
+
this.emitDomainBlockDrawCalls(composition, true);
|
|
1677
|
+
this.emit("rp.end();");
|
|
1678
|
+
this.emit("device.queue.submit([enc.finish()]);");
|
|
1679
|
+
this.emit("document.body.dataset.holoscriptWebgpuFrame = String(frameCount);");
|
|
1680
|
+
this.emit("requestAnimationFrame(frame);");
|
|
1681
|
+
this.dedent();
|
|
1682
|
+
this.emit("}");
|
|
1683
|
+
this.emit("requestAnimationFrame(frame);");
|
|
1684
|
+
}
|
|
1685
|
+
emitDrawCall(obj) {
|
|
1686
|
+
const v = this.sanitizeName(obj.name);
|
|
1687
|
+
const traits = obj.traits || [];
|
|
1688
|
+
const role = resolveGeometryRole({
|
|
1689
|
+
purpose: this.findObjProp(obj, "purpose"),
|
|
1690
|
+
visible: this.findObjProp(obj, "visible"),
|
|
1691
|
+
traitNames: traits.map((t) => t.name)
|
|
1692
|
+
});
|
|
1693
|
+
if (!role.visible) {
|
|
1694
|
+
if (obj.children) {
|
|
1695
|
+
for (const child of obj.children) this.emitDrawCall(child);
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (traits.some((t) => t.name === "gaussian_splat")) {
|
|
1700
|
+
this.emit(`rp.setPipeline(${v}SplatPipeline); rp.draw(4, ${v}SplatCount.value);`);
|
|
1701
|
+
} else if (traits.some((t) => t.name === "point_cloud")) {
|
|
1702
|
+
this.emit(`rp.setPipeline(${v}PointPipeline); rp.draw(${v}PointCount.value);`);
|
|
1703
|
+
} else if (traits.some((t) => isGpuParticleTraitName(t.name))) {
|
|
1704
|
+
this.emit(
|
|
1705
|
+
`rp.setPipeline(${v}ParticlePipeline); rp.setVertexBuffer(0, ${v}ParticleBufA); rp.draw(${v}ParticleCount);`
|
|
1706
|
+
);
|
|
1707
|
+
} else if (this.isWaterObject(obj)) {
|
|
1708
|
+
this.emit(
|
|
1709
|
+
`rp.setPipeline(${v}WaterPipeline); rp.setBindGroup(0, ${v}WaterBind); rp.setVertexBuffer(0, ${v}VBO); rp.draw(${v}VertexCount);`
|
|
1710
|
+
);
|
|
1711
|
+
} else {
|
|
1712
|
+
this.emit(
|
|
1713
|
+
`rp.setPipeline(${v}Pipeline); rp.setBindGroup(0, ${v}Bind); rp.setVertexBuffer(0, ${v}VBO); rp.draw(${v}VertexCount);`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (obj.children) {
|
|
1717
|
+
for (const child of obj.children) this.emitDrawCall(child);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
emitDomainBlockDrawCalls(composition, postFxOnly) {
|
|
1721
|
+
const blocks = composition.domainBlocks ?? [];
|
|
1722
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1723
|
+
const block = blocks[i];
|
|
1724
|
+
const prefix = this.domainBlockVar(block, i);
|
|
1725
|
+
if (block.domain === "postfx") {
|
|
1726
|
+
if (postFxOnly) {
|
|
1727
|
+
this.emit(`rp.setPipeline(${prefix}PostFXPipeline); rp.draw(3);`);
|
|
1728
|
+
}
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (postFxOnly) continue;
|
|
1732
|
+
if (block.domain === "vfx") {
|
|
1733
|
+
this.emit(
|
|
1734
|
+
`rp.setPipeline(${prefix}ParticlePipeline); rp.setVertexBuffer(0, ${prefix}ParticleBuf); rp.draw(${prefix}ParticleCount);`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
if (block.domain === "procedural" && (block.keyword === "scatter" || block.keyword === "distribute" || block.properties?.count)) {
|
|
1738
|
+
this.emit(
|
|
1739
|
+
`rp.setPipeline(${prefix}ScatterPipeline); rp.setVertexBuffer(0, ${prefix}ScatterVBO); rp.draw(${prefix}ScatterVertexCount, ${prefix}ScatterInstanceCount);`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
1745
|
+
emit(line) {
|
|
1746
|
+
this.lines.push(this.options.indent.repeat(this.indentLevel) + line);
|
|
1747
|
+
}
|
|
1748
|
+
emitMultiline(code) {
|
|
1749
|
+
for (const line of code.split("\n")) this.emit(line);
|
|
1750
|
+
}
|
|
1751
|
+
indent() {
|
|
1752
|
+
this.indentLevel++;
|
|
1753
|
+
}
|
|
1754
|
+
dedent() {
|
|
1755
|
+
this.indentLevel = Math.max(0, this.indentLevel - 1);
|
|
1756
|
+
}
|
|
1757
|
+
sanitizeName(name) {
|
|
1758
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1759
|
+
}
|
|
1760
|
+
domainBlockVar(block, index) {
|
|
1761
|
+
const raw = `${block.domain}_${block.keyword}_${block.name || index}`;
|
|
1762
|
+
const safe = this.sanitizeName(raw);
|
|
1763
|
+
return `db${index}_${safe || "domain"}`;
|
|
1764
|
+
}
|
|
1765
|
+
json(value) {
|
|
1766
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1767
|
+
}
|
|
1768
|
+
numberProp(block, keys, fallback) {
|
|
1769
|
+
for (const key of keys) {
|
|
1770
|
+
const value = block.properties?.[key];
|
|
1771
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1772
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1773
|
+
const parsed = Number(value);
|
|
1774
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return fallback;
|
|
1778
|
+
}
|
|
1779
|
+
hashSeed(value) {
|
|
1780
|
+
let h = 2166136261;
|
|
1781
|
+
for (let i = 0; i < value.length; i++) {
|
|
1782
|
+
h ^= value.charCodeAt(i);
|
|
1783
|
+
h = Math.imul(h, 16777619);
|
|
1784
|
+
}
|
|
1785
|
+
return h >>> 0;
|
|
1786
|
+
}
|
|
1787
|
+
findObjProp(obj, key) {
|
|
1788
|
+
const own = obj.properties?.find((p) => p.key === key)?.value;
|
|
1789
|
+
if (own !== void 0) return own;
|
|
1790
|
+
if (obj.template) {
|
|
1791
|
+
const tpl = this.templatesByName.get(obj.template);
|
|
1792
|
+
const fromTemplate = tpl?.properties?.find((p) => p.key === key)?.value;
|
|
1793
|
+
if (fromTemplate !== void 0) return fromTemplate;
|
|
1794
|
+
}
|
|
1795
|
+
return void 0;
|
|
1796
|
+
}
|
|
1797
|
+
extractObjectProperties(obj) {
|
|
1798
|
+
const properties = {};
|
|
1799
|
+
for (const prop of obj.properties ?? []) {
|
|
1800
|
+
if (!prop.key) continue;
|
|
1801
|
+
const value = prop.value;
|
|
1802
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value) && value.every(
|
|
1803
|
+
(item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
|
|
1804
|
+
)) {
|
|
1805
|
+
properties[prop.key] = value;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return properties;
|
|
1809
|
+
}
|
|
1810
|
+
parseColor(value) {
|
|
1811
|
+
if (typeof value === "string" && value.startsWith("#")) {
|
|
1812
|
+
const h = value.slice(1);
|
|
1813
|
+
return [
|
|
1814
|
+
parseFloat((parseInt(h.substring(0, 2), 16) / 255).toFixed(4)),
|
|
1815
|
+
parseFloat((parseInt(h.substring(2, 4), 16) / 255).toFixed(4)),
|
|
1816
|
+
parseFloat((parseInt(h.substring(4, 6), 16) / 255).toFixed(4))
|
|
1817
|
+
];
|
|
1818
|
+
}
|
|
1819
|
+
if (Array.isArray(value) && value.length >= 3) return [value[0], value[1], value[2]];
|
|
1820
|
+
return [1, 1, 1];
|
|
1821
|
+
}
|
|
1822
|
+
extractMaterialColor(material, color) {
|
|
1823
|
+
if (color) return this.parseColor(color);
|
|
1824
|
+
if (material && typeof material === "object" && !Array.isArray(material)) {
|
|
1825
|
+
const m = material;
|
|
1826
|
+
if (m.color) return this.parseColor(m.color);
|
|
1827
|
+
}
|
|
1828
|
+
return [0.8, 0.8, 0.8];
|
|
1829
|
+
}
|
|
1830
|
+
extractMaterialProp(material, key, fallback) {
|
|
1831
|
+
if (material && typeof material === "object" && !Array.isArray(material)) {
|
|
1832
|
+
const m = material;
|
|
1833
|
+
if (m[key] !== void 0) return m[key];
|
|
1834
|
+
}
|
|
1835
|
+
return fallback;
|
|
1836
|
+
}
|
|
1837
|
+
extractEmissive(material, obj) {
|
|
1838
|
+
const objEmissive = this.findObjProp(obj, "emissive");
|
|
1839
|
+
if (objEmissive !== void 0) return this.parseColor(objEmissive);
|
|
1840
|
+
if (material && typeof material === "object" && !Array.isArray(material)) {
|
|
1841
|
+
const m = material;
|
|
1842
|
+
if (m.emissive !== void 0) return this.parseColor(m.emissive);
|
|
1843
|
+
}
|
|
1844
|
+
return [0, 0, 0];
|
|
1845
|
+
}
|
|
1846
|
+
geometryVertexDataFn(meshType) {
|
|
1847
|
+
return resolveGeometry(meshType).webgpuVertexFn;
|
|
1848
|
+
}
|
|
1849
|
+
materialName(material) {
|
|
1850
|
+
if (typeof material === "string") return material;
|
|
1851
|
+
if (material && typeof material === "object") {
|
|
1852
|
+
const m = material;
|
|
1853
|
+
if (typeof m.type === "string") return m.type;
|
|
1854
|
+
if (typeof m.name === "string") return m.name;
|
|
1855
|
+
}
|
|
1856
|
+
return void 0;
|
|
1857
|
+
}
|
|
1858
|
+
// Self-illumination strength for a named material when no explicit emissive is
|
|
1859
|
+
// authored. Holographic/neon/emissive surfaces glow in their own color; glass/gel
|
|
1860
|
+
// gets a soft sheen; everything else stays purely lit (0).
|
|
1861
|
+
materialGlow(material) {
|
|
1862
|
+
const name = this.materialName(material);
|
|
1863
|
+
if (!name) return 0;
|
|
1864
|
+
const n = name.toLowerCase();
|
|
1865
|
+
if (/hologram|holo|liquid_glass|liquid_hologram|neon|glow|emissive|aura|plasma|energy/.test(n)) {
|
|
1866
|
+
return 0.65;
|
|
1867
|
+
}
|
|
1868
|
+
if (/glass|crystal|gel|water/.test(n)) return 0.28;
|
|
1869
|
+
return 0;
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
exports.WebGPUCompiler_exports = WebGPUCompiler_exports;
|
|
1876
|
+
exports.init_WebGPUCompiler = init_WebGPUCompiler;
|
|
1877
|
+
exports.init_geometry_purpose = init_geometry_purpose;
|
|
1878
|
+
exports.init_geometry_registry = init_geometry_registry;
|
|
1879
|
+
exports.init_skybox_registry = init_skybox_registry;
|
|
1880
|
+
exports.resolveGeometry = resolveGeometry;
|
|
1881
|
+
exports.resolveGeometryRole = resolveGeometryRole;
|
|
1882
|
+
exports.resolveSkyboxColor = resolveSkyboxColor;
|
|
1883
|
+
//# sourceMappingURL=chunk-ERYKKPHW.cjs.map
|
|
1884
|
+
//# sourceMappingURL=chunk-ERYKKPHW.cjs.map
|