Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42778c183a | ||
|
|
492d123edb | ||
|
|
2531df5231 | ||
|
|
2ef556783e | ||
|
|
a580360bc8 | ||
|
|
9995ae1e71 | ||
|
|
e769703917 | ||
|
|
0e6a30eeb5 | ||
|
|
f98a37d348 | ||
|
|
c3a78af2f3 | ||
|
|
c1d250c351 | ||
|
|
0ef33d65c1 | ||
|
|
853ee9c9f5 | ||
|
|
8bcd47af88 | ||
|
|
5158bb4103 | ||
|
|
db69e28d2c | ||
|
|
65fd71c52a | ||
|
|
c1c32a0f14 | ||
|
|
35812337d7 | ||
|
|
071bfc288a | ||
|
|
bc4ca92aab | ||
|
|
ff13414230 | ||
|
|
5854630364 | ||
|
|
cc73fab705 | ||
|
|
b2946a8910 | ||
|
|
f27f3082da | ||
|
|
c169f811b1 | ||
|
|
5487a62b51 | ||
|
|
af6923ecd1 | ||
|
|
b40531508a | ||
|
|
49d27049d9 | ||
|
|
a7077e6cb5 | ||
|
|
ee1b92a94c | ||
|
|
b7b3529311 | ||
|
|
e9caacd781 | ||
|
|
9888113282 | ||
|
|
9814371313 | ||
|
|
ce423793d8 | ||
|
|
03ef2cefc1 | ||
|
|
e189884b97 | ||
|
|
4eb624988a | ||
|
|
5f368158ee | ||
|
|
d8a9088483 | ||
|
|
a94e8e05d0 | ||
|
|
eb9e6b409c | ||
|
|
231461c44a | ||
|
|
0a06f7a25f | ||
|
|
4394ee7107 | ||
|
|
51bf15e552 | ||
|
|
201b4d59ea | ||
|
|
3e97904085 | ||
|
|
de53ab72cb | ||
|
|
be63754c28 | ||
|
|
88c8c33faf | ||
|
|
3833569b4a | ||
|
|
d8eb303bbd | ||
|
|
e90828baca | ||
|
|
960c366658 | ||
|
|
64205786cc | ||
|
|
6a55921bc2 | ||
|
|
45a9f262b1 | ||
|
|
976ff1db73 | ||
|
|
6dca7d1cc7 | ||
|
|
10f9e1c2fb | ||
|
|
baa2c85add | ||
|
|
55e99641d5 | ||
|
|
70f5d49cc4 | ||
|
|
107722141d | ||
|
|
d5de205c40 | ||
|
|
5d550f4ade | ||
|
|
94474fcd3e | ||
|
|
b2ae858ce8 | ||
|
|
92659dbf20 | ||
|
|
58bfabe4b6 | ||
|
|
43df4e6f89 | ||
|
|
60987c5df1 | ||
|
|
8f03cf1f10 | ||
|
|
7a93ee41e4 | ||
|
|
4c54666da7 | ||
|
|
0c59b9f497 | ||
|
|
4795db6c0f | ||
|
|
baa77171b5 | ||
|
|
3f673a7470 | ||
|
|
061216b2d3 | ||
|
|
621280ba72 | ||
|
|
18cf9ae9d7 | ||
|
|
15f60cc725 | ||
|
|
7a09df598d | ||
|
|
a46fc26bf8 | ||
|
|
6e1eb30f7c | ||
|
|
a15facecaa | ||
|
|
696f92e47c | ||
|
|
dfcf35aa57 | ||
|
|
fb5b492c04 | ||
|
|
fc38dd0d69 | ||
|
|
bbac619027 | ||
|
|
0e0f949bd2 | ||
|
|
f64e138b9e | ||
|
|
825091ac04 | ||
|
|
84cb2b9c25 | ||
|
|
5185d5e193 | ||
|
|
68b42ba56e | ||
|
|
79aef84b11 | ||
|
|
2f48bef27e | ||
|
|
5f7e613349 | ||
|
|
259620f04b | ||
|
|
277632fb93 | ||
|
|
1e3dca5f8b | ||
|
|
c8b6db457c | ||
|
|
953ee6a13e | ||
|
|
9ae61eb5a6 | ||
|
|
61751bf4ee | ||
|
|
3ac800dbad | ||
|
|
6b166a37fa | ||
|
|
5436338583 | ||
|
|
c4fb821bdd | ||
|
|
995358d984 | ||
|
|
026ca28152 | ||
|
|
2d0ff59045 | ||
|
|
069fd120fe | ||
|
|
67960aee0a | ||
|
|
8b66f0a947 | ||
|
|
cf23cef5cd | ||
|
|
7527cade95 | ||
|
|
a2411b9971 |
116 changed files with 11650 additions and 591 deletions
Binary file not shown.
|
|
@ -11,7 +11,7 @@
|
|||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.95"
|
||||
"state_hygiene:0.92"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
- Path: `\\?\E:\Projects\Fensalir`
|
||||
- Families: cult_protocol_storage, gamecult_web_lore_ops, unity_runtime_body
|
||||
- Confidence: 1.00
|
||||
- Dominant axes: burstiness:1.00, content_canon_bias:1.00, contract_strictness:1.00, evidence_appetite:1.00, state_hygiene:0.95
|
||||
- Dominant axes: burstiness:1.00, content_canon_bias:1.00, contract_strictness:1.00, evidence_appetite:1.00, state_hygiene:0.92
|
||||
|
||||
- Trajectory: Fensalir is currently steered by worldbuilding_depth recent 0.00, current 0.88, delta 0.00; material_grounding recent 0.28, current 0.65, delta 0.00; historical_dialectic recent 0.00, current 0.35, delta 0.00.
|
||||
- Trajectory: Fensalir is currently steered by worldbuilding_depth recent 0.00, current 0.82, delta 0.00; material_grounding recent 0.00, current 0.53, delta 0.00; historical_dialectic recent 0.00, current 0.35, delta 0.00.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth",
|
||||
"logPath": "E:\\Projects\\Fensalir\\.voidbot\\logs\\voidbot-face-birth.log",
|
||||
"error": "cargo exited with 1",
|
||||
"startedAt": "2026-05-27T17:50:48.712Z",
|
||||
"failedAt": "2026-05-27T17:50:51.745Z",
|
||||
"updatedAt": "2026-05-27T17:50:51.745Z"
|
||||
"startedAt": "2026-05-31T00:01:11.155Z",
|
||||
"failedAt": "2026-05-31T00:01:15.252Z",
|
||||
"updatedAt": "2026-05-31T00:01:15.252Z"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2756,3 +2756,193 @@ Caused by:
|
|||
invalid type: string "cultcache.store.v1", expected struct CultCacheEnvelope
|
||||
|
||||
[2026-05-27T17:50:51.745Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
[2026-05-30T21:16:09.450Z] starting repo Face birth for fensalir
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
|
||||
{
|
||||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
|
||||
"mode": "scout",
|
||||
"profileCount": 1,
|
||||
"repoCount": 1,
|
||||
"repos": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"dominantAxes": [
|
||||
"burstiness:1.00",
|
||||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.93"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
"gamecult_web_lore_ops",
|
||||
"unity_runtime_body"
|
||||
],
|
||||
"name": "Fensalir",
|
||||
"path": "\\\\?\\E:\\Projects\\Fensalir",
|
||||
"repoId": "fensalir",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"root": "E:\\Projects\\Fensalir",
|
||||
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
|
||||
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
|
||||
"trajectoryCount": 1
|
||||
}
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
|
||||
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
|
||||
[2026-05-30T21:16:19.943Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
[2026-05-30T21:16:47.234Z] starting repo Face birth for fensalir
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
|
||||
{
|
||||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
|
||||
"mode": "scout",
|
||||
"profileCount": 1,
|
||||
"repoCount": 1,
|
||||
"repos": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"dominantAxes": [
|
||||
"burstiness:1.00",
|
||||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.93"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
"gamecult_web_lore_ops",
|
||||
"unity_runtime_body"
|
||||
],
|
||||
"name": "Fensalir",
|
||||
"path": "\\\\?\\E:\\Projects\\Fensalir",
|
||||
"repoId": "fensalir",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"root": "E:\\Projects\\Fensalir",
|
||||
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
|
||||
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
|
||||
"trajectoryCount": 1
|
||||
}
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
|
||||
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
|
||||
[2026-05-30T21:16:52.031Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
[2026-05-30T22:21:20.960Z] starting repo Face birth for fensalir
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
|
||||
{
|
||||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
|
||||
"mode": "scout",
|
||||
"profileCount": 1,
|
||||
"repoCount": 1,
|
||||
"repos": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"dominantAxes": [
|
||||
"burstiness:1.00",
|
||||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.92"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
"gamecult_web_lore_ops",
|
||||
"unity_runtime_body"
|
||||
],
|
||||
"name": "Fensalir",
|
||||
"path": "\\\\?\\E:\\Projects\\Fensalir",
|
||||
"repoId": "fensalir",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"root": "E:\\Projects\\Fensalir",
|
||||
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
|
||||
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
|
||||
"trajectoryCount": 1
|
||||
}
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
|
||||
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
|
||||
[2026-05-30T22:21:23.395Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
[2026-05-30T23:06:25.344Z] starting repo Face birth for fensalir
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
|
||||
{
|
||||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
|
||||
"mode": "scout",
|
||||
"profileCount": 1,
|
||||
"repoCount": 1,
|
||||
"repos": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"dominantAxes": [
|
||||
"burstiness:1.00",
|
||||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.92"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
"gamecult_web_lore_ops",
|
||||
"unity_runtime_body"
|
||||
],
|
||||
"name": "Fensalir",
|
||||
"path": "\\\\?\\E:\\Projects\\Fensalir",
|
||||
"repoId": "fensalir",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"root": "E:\\Projects\\Fensalir",
|
||||
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
|
||||
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
|
||||
"trajectoryCount": 1
|
||||
}
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
|
||||
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
|
||||
[2026-05-30T23:06:32.368Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
[2026-05-31T00:01:11.155Z] starting repo Face birth for fensalir
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
|
||||
{
|
||||
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
|
||||
"mode": "scout",
|
||||
"profileCount": 1,
|
||||
"repoCount": 1,
|
||||
"repos": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"dominantAxes": [
|
||||
"burstiness:1.00",
|
||||
"content_canon_bias:1.00",
|
||||
"contract_strictness:1.00",
|
||||
"evidence_appetite:1.00",
|
||||
"state_hygiene:0.92"
|
||||
],
|
||||
"families": [
|
||||
"cult_protocol_storage",
|
||||
"gamecult_web_lore_ops",
|
||||
"unity_runtime_body"
|
||||
],
|
||||
"name": "Fensalir",
|
||||
"path": "\\\\?\\E:\\Projects\\Fensalir",
|
||||
"repoId": "fensalir",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"root": "E:\\Projects\\Fensalir",
|
||||
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
|
||||
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
|
||||
"trajectoryCount": 1
|
||||
}
|
||||
|
||||
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
|
||||
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
|
||||
[2026-05-31T00:01:15.252Z] failed repo Face birth for fensalir: cargo exited with 1
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -6,7 +6,7 @@
|
|||
"repoPath": "E:\\Projects\\Fensalir",
|
||||
"avatarPath": "E:/Projects/Fensalir/.voidbot/voice/fensalir.png",
|
||||
"avatarUrl": "https://raw.githubusercontent.com/GameCult/Fensalir/main/.voidbot/voice/fensalir.png",
|
||||
"createdOrRefreshedAt": "2026-05-27T17:50:48.603Z",
|
||||
"createdOrRefreshedAt": "2026-05-31T00:01:11.056Z",
|
||||
"summary": "Marsh-lit native runtime and renderer Face for Fensalir.",
|
||||
"voice": "Precise, severe, luminous, and quietly haunted. Fensalir speaks like a wetland cathedral that learned D3D12: patient about real evidence, merciless toward hidden state, false lighting, and client policy leaking into engine walls.",
|
||||
"priorities": [
|
||||
|
|
|
|||
15
Fensalir.sln
15
Fensalir.sln
|
|
@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Fensalir", "src\Aq
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saga", "src\Saga\Saga.csproj", "{30324495-56BE-4D45-80E8-CE03C24726C8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.EveSurface", "src\Aquarium.EveSurface\Aquarium.EveSurface.csproj", "{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -173,6 +175,18 @@ Global
|
|||
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -188,5 +202,6 @@ Global
|
|||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{30324495-56BE-4D45-80E8-CE03C24726C8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -33,6 +33,19 @@ audio buffers, synth patches, and frame state through
|
|||
resources: D3D12 pipelines and command lists, shader reloads, presentation,
|
||||
audio playback, debug telemetry, and headless capture.
|
||||
|
||||
Fensalir's DirectWrite/Direct2D overlay is also an Eve client surface. It should
|
||||
consume the shared Eve/CultMesh UI document contract and lower it into
|
||||
`AquariumUiDocument`, while provider repos keep ownership of dashboard state and
|
||||
commands. See `docs/eve-direct2d-client-surface.md`.
|
||||
|
||||
Run the native Eve surface client against Mimir's VoidBot dashboard:
|
||||
|
||||
```powershell
|
||||
$env:FENSALIR_EVE_BROKER = 'ws://127.0.0.1:8795/eve/deck'
|
||||
$env:FENSALIR_EVE_PROVIDER = 'voidbot.swarm'
|
||||
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.EveSurface\Aquarium.EveSurface.csproj
|
||||
```
|
||||
|
||||
## Spatial Evidence Machine
|
||||
|
||||
Fensalir's field work is not just "some fractal helpers." It is a
|
||||
|
|
@ -136,6 +149,8 @@ how WASAPI or Faust got invited to the party.
|
|||
audio/synth host, bundled assets, and renderer-owned shaders.
|
||||
- `src/Aquarium.Engine.Fractal`: `.aquageo`, IFS/fractal fields, temporal
|
||||
reservoirs, evidence tracks, contribution cache, and lowering machinery.
|
||||
- `src/Aquarium.EveSurface`: native DirectWrite Eve surface client for shared
|
||||
dashboard documents such as Mimir's `voidbot.swarm` provider.
|
||||
- `src/Saga`: stochastic planetary flow middleware for cube-sphere Transport
|
||||
fields, arbitrary forcing, Coriolis-aware advection, and learned atmosphere /
|
||||
ocean / debris flow packets.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ historical branch scar.
|
|||
|
||||
## Fields, Reservoirs, And Fractals
|
||||
|
||||
- `stateless-planetary-erosion-rendering-plan.md`: authority map and staged
|
||||
implementation plan for spherical erosion, cube-sphere pages, LOD,
|
||||
intersection, differential shading, and verification.
|
||||
- `cultmath-planetary-field-runtime-plan.md`: extraction and authority plan for
|
||||
one CultMath planetary field shared by Aetheria daemon CPU simulation,
|
||||
Fensalir and Unity 3D rendering, and Electron/Web map projections.
|
||||
- `../src/Saga/README.md`: studio-facing Saga middleware pitch and subproject
|
||||
overview for stochastic planetary flow.
|
||||
- `papers/fensalir-pipeline.tex`: living MiKTeX paper for the Fensalir
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ and UI as living objects.
|
|||
- Debug overlay is allowed, but it is explicit chrome.
|
||||
- Overlay text uses DirectWrite and stays out of the HDR scene unless a feature
|
||||
intentionally projects it into the world.
|
||||
- The DirectWrite/Direct2D overlay is a valid Eve client surface when it
|
||||
renders shared CultMesh UI documents through `AquariumUiDocument`. It does not
|
||||
own provider state or dashboard semantics.
|
||||
- Diegetic labels and menus should anchor to world objects and unfold locally.
|
||||
- Object-owned controls beat global panels.
|
||||
- Crisp text, focus, keyboard input, and accessibility are real requirements,
|
||||
|
|
|
|||
411
docs/cultmath-planetary-field-runtime-plan.md
Normal file
411
docs/cultmath-planetary-field-runtime-plan.md
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# CultMath Planetary Field Runtime Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Make CultMath the canonical, renderer-independent planetary field runtime used
|
||||
by Aetheria's daemon simulation, Unity's three-dimensional planet renderer,
|
||||
Fensalir's D3D12 renderer, and Electron/Web strategic maps.
|
||||
|
||||
The product is a deterministic, scale-aware spherical field that can be queried
|
||||
on the CPU and lowered into several presentation substrates. Rendering is one
|
||||
consumer. It is not the owner of planetary geometry.
|
||||
|
||||
The intended end state is:
|
||||
|
||||
> All deterministic planetary geometry, evidence, addressing, sampling,
|
||||
> paging, projection, and refinement live in CultMath. Aetheria owns gameplay
|
||||
> meaning and committed consequences. Renderers lower the same versioned field.
|
||||
|
||||
## Current mechanism
|
||||
|
||||
CultMath now owns the matching C# and HLSL erosion operator, physical
|
||||
frequency-band selection, scale-aware field definition and sampling,
|
||||
tangent/QSC cube-sphere topology, bordered page layouts, CPU residual baking and
|
||||
composition, projected-error LOD selection, residual residency transitions,
|
||||
renderer-neutral patch meshes, CPU ray/path/region queries, map projections,
|
||||
versioned projected tile baking, and canonical GPU page input/metadata payloads.
|
||||
Fensalir and Zyphos consume the CultMath topology, field, LOD, residency, page,
|
||||
patch-direction, refinement, and upload-contract functions while retaining
|
||||
D3D12 buffers, bindings, dispatch, scene policy, and drawing.
|
||||
|
||||
Fensalir's former cube-face, cube-position, tile-key, page-layout, page-sampling,
|
||||
and differential compatibility types have been deleted; fractal, Saga, tests,
|
||||
and projection-analysis code consume CultMath directly. Zyphos still owns its
|
||||
geological base-field preset, authored brush chart, page resource structures,
|
||||
shader entry points, and scene/material policy. CultMath now ships a runnable
|
||||
Unity package sample and a browser tile decoder. Electron integration, CultMesh
|
||||
tile publication, WGSL, and Aetheria daemon integration are not implemented by
|
||||
the restricted pass recorded below.
|
||||
|
||||
The completed Fensalir implementation proves the approach, but its ownership
|
||||
boundary is too renderer-shaped for Aetheria. The reusable algorithms should
|
||||
move into CultMath. D3D12 resource allocation and command recording should not.
|
||||
|
||||
Aetheria's daemon is the authority for committed game state and simulation.
|
||||
Unity objects, Electron canvases, local GPU pages, and projected map tiles are
|
||||
derived state. Terrain queries that affect movement, visibility, construction,
|
||||
resources, physics, or combat must be available to the daemon without booting a
|
||||
renderer or depending on client cache residency.
|
||||
|
||||
## Restricted implementation ledger
|
||||
|
||||
This ledger records the pass performed without modifying Aetheria. A phase is
|
||||
complete only when its stated exit criterion is proved; generic machinery alone
|
||||
does not impersonate a consumer integration.
|
||||
|
||||
| Phase | State | Current evidence |
|
||||
|---|---|---|
|
||||
| 0. Contract and parity corpus | Complete | Field identity, query scale, samples, errors, topology/page/projection fixtures, D3D12 QSC readback, and forward/inverse HLSL parity for all eight projections exist |
|
||||
| 1. Topology extraction | Complete | CultMath owns CPU/HLSL QSC mapping, normalized-cube baseline, tile addresses, page directions, and all edge/corner fixtures; the former Fensalir types were deleted |
|
||||
| 2. Authoritative CPU queries | In progress | Point, batch, double-position, ray, clearance, great-circle, and region queries exist; the forbidden Aetheria daemon smoke remains |
|
||||
| 3. Page machinery extraction | Complete | CultMath owns CPU baking, residuals, summaries, composition, HLSL page interpolation/residual/summary functions, and residency; Fensalir retains only resource, binding, dispatch, and entry-point adapters |
|
||||
| 4. Residency and patch extraction | Complete | CultMath owns LOD, lifecycle, mesh generation, QSC patch direction, radial refinement, and the GPU page payload; Unity and Fensalir are type-conversion lowerings of the same builders |
|
||||
| 5. Unity adapter and viewer | In progress | `CultMath.Unity` converts canonical patches/residency into Unity meshes/page payloads; a one-click sample renders the shared HLSL field and exposes matching CPU queries, and a clean Unity 6 project imports/compiles it; Aetheria binding remains out of scope |
|
||||
| 6. Projections and map tiles | In progress | All named CPU projections, versioned tile baking, the `CMPT` binary codec, and a tested TypeScript decoder exist; CultMesh publication and Electron presentation remain |
|
||||
| 7. Optional WebGPU lowering | Not started | Still correctly optional |
|
||||
| 8. Delete obsolete owners | In progress | Zyphos private page addressing, LOD, lifecycle, GPU payload calculation, triplanar erosion composition, page interpolation, and patch direction were removed or delegated; the Aetheria cut is outside this pass |
|
||||
|
||||
Current verification from this pass includes CultMath managed and browser-codec tests, the full
|
||||
Fensalir fractal suite, a zero-warning solution build, CPU lifecycle/LOD
|
||||
integration probes, advanced erosion CPU/GPU parity, QSC CPU/GPU readback, all
|
||||
eight projection forward/inverse GPU readbacks, focused shader compilation,
|
||||
page residual generation, all cube-edge/corner seam
|
||||
probes, finite summary evidence, page-backed radial-hit parity, and a clean
|
||||
Unity 6 package/sample import with C# domain reload and ShaderLab compilation.
|
||||
The final restricted-pass run was 68 CultMath tests, 170 Fensalir fractal tests,
|
||||
126 engine/GPU tests, a zero-warning solution build, a C#-produced tile decoded
|
||||
by Node's TypeScript runtime, and Unity 6.0.4.2f1 batchmode exit code zero.
|
||||
|
||||
## Authority map
|
||||
|
||||
### Owner
|
||||
|
||||
CultMath owns the canonical planetary field function and the algorithms needed
|
||||
to query or approximate it at a declared physical scale.
|
||||
|
||||
Aetheria owns planet definitions, authored parameters, gameplay interpretation,
|
||||
simulation policy, and every committed consequence of a terrain query.
|
||||
|
||||
### Inputs
|
||||
|
||||
The field runtime may read:
|
||||
|
||||
- a stable planet identifier and field version;
|
||||
- planet seed, radius, and declared coordinate frame;
|
||||
- base-field and erosion parameters;
|
||||
- a unit spherical direction or world-space query location;
|
||||
- a physical sample footprint or requested wavelength range;
|
||||
- an explicit accuracy/error budget;
|
||||
- deterministic authored contributions supplied through a narrow Aetheria
|
||||
policy boundary.
|
||||
|
||||
### Outputs
|
||||
|
||||
A point query returns a `PlanetarySurfaceSample` containing at least:
|
||||
|
||||
- radius and radial displacement;
|
||||
- planet-local and optional world-space surface position;
|
||||
- tangent gradient, geometric normal, and slope;
|
||||
- ridge, gully, drainage, and related field evidence;
|
||||
- water-relative height when a sea datum is supplied;
|
||||
- the resolved wavelength interval;
|
||||
- a conservative unresolved-height bound;
|
||||
- the planet and field versions used to produce the result.
|
||||
|
||||
Batch, region, intersection, page, and projection queries compose this same
|
||||
contract. They do not define separate terrain.
|
||||
|
||||
### Derived state
|
||||
|
||||
These are caches or projections, not owners:
|
||||
|
||||
- cube-sphere pages, borders, child residuals, and summaries;
|
||||
- quadtree residency and transition weights;
|
||||
- Unity meshes, buffers, textures, and GameObjects;
|
||||
- Fensalir D3D12 buffers and pipeline state;
|
||||
- projected Electron/Web map tiles;
|
||||
- shaded-relief, biome-color, political, or tactical map layers;
|
||||
- local collision meshes and navigation grids.
|
||||
|
||||
Eviction or regeneration may change latency or available fidelity. It must not
|
||||
change the canonical answer at the same query scale.
|
||||
|
||||
### Forbidden writers
|
||||
|
||||
These paths must not decide planetary truth:
|
||||
|
||||
- Unity scene lifetime, collision callbacks, or renderer-local noise;
|
||||
- Electron-only projection or terrain generators;
|
||||
- cube-face-local seeds or seam repair passes;
|
||||
- D3D12- or ShaderLab-only erosion equations;
|
||||
- cache residency, request order, or transition age;
|
||||
- map palettes, labels, and visualization thresholds;
|
||||
- renderer-specific gameplay compensators.
|
||||
|
||||
### Shared paths
|
||||
|
||||
Daemon point and batch queries, CPU and HLSL page baking, Unity and Fensalir
|
||||
patch rendering, map-tile generation, reload, and LOD transitions identify and
|
||||
evaluate the same field contract.
|
||||
|
||||
### Cut line
|
||||
|
||||
Move renderer-independent cube-sphere, page, residual, LOD, sampling, projection,
|
||||
and patch algorithms out of Zyphos and Fensalir into CultMath. Delete the old
|
||||
algorithm owners after each consumer delegates to CultMath. Do not preserve two
|
||||
implementations behind adapters.
|
||||
|
||||
Keep only resource allocation, descriptor layout, dispatch, camera/clip-space
|
||||
conversion, render-pipeline integration, presentation materials, and user
|
||||
interaction in renderer packages.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. The same field version, direction, and physical footprint produce the same
|
||||
terrain within the declared backend tolerance.
|
||||
2. CPU simulation never depends on renderer availability or page residency.
|
||||
3. Simulation queries explicitly declare physical footprint or wavelength
|
||||
range. There is no ambiguous full-detail `HeightAt` authority.
|
||||
4. The daemon alone commits gameplay consequences. Client evaluation is
|
||||
prediction or presentation of daemon-published field truth.
|
||||
5. Cube faces and map projections are coordinate charts, never terrain owners.
|
||||
6. Parent and child pages share identical low-frequency terrain. Children add
|
||||
only representable residual bands.
|
||||
7. Page and tile eviction affects cost and fidelity availability, not identity.
|
||||
8. A map pixel and a 3D point resolving to the same spherical direction and
|
||||
footprint sample the same field.
|
||||
9. Projection singularities and polar cutoffs are explicit contracts.
|
||||
10. Integer hashing and field identity are exact across supported backends.
|
||||
Floating-point results carry measured tolerances and error bounds.
|
||||
11. Erosion evidence does not silently claim hydrology, climate, biome,
|
||||
resource, or traversability authority.
|
||||
12. CultMath core has no Unity, D3D12, Electron, or Aetheria dependency.
|
||||
|
||||
## Canonical package shape
|
||||
|
||||
```text
|
||||
CultMath.Planetary
|
||||
|-- Definition field definition, version, coordinate frame, query scale
|
||||
|-- Field composition, erosion, differentials, conservative errors
|
||||
|-- Topology cube faces, tile addresses, direction/UV, seam rules
|
||||
|-- Query point, batch, ray, clearance, path, and region queries
|
||||
|-- Pages layouts, generation, residuals, summaries, composition
|
||||
|-- Residency projected-error selection, fallback, arrival, eviction
|
||||
|-- Projection forward/inverse transforms and projected tile sampling
|
||||
`-- Patch cube-sphere mesh, radial refinement, surface differentials
|
||||
```
|
||||
|
||||
The HLSL surface mirrors these organs with small includes. Resource declarations
|
||||
and executable compute/raster entry points remain adapter code so the common
|
||||
functions work in Unity, Fensalir, and standalone parity probes.
|
||||
|
||||
## CPU query contract
|
||||
|
||||
The primary API is scale-aware:
|
||||
|
||||
```csharp
|
||||
PlanetarySurfaceSample SampleDirection(
|
||||
in PlanetaryFieldDefinition planet,
|
||||
float3 unitDirection,
|
||||
in PlanetaryQueryScale scale);
|
||||
|
||||
PlanetarySurfaceSample SamplePosition(
|
||||
in PlanetaryFieldDefinition planet,
|
||||
double3 planetLocalPosition,
|
||||
in PlanetaryQueryScale scale);
|
||||
```
|
||||
|
||||
`PlanetaryQueryScale` declares a footprint in metres or an explicit wavelength
|
||||
range, plus an accepted unresolved-height bound. Named Aetheria policies such
|
||||
as strategic, navigation, vehicle, or character scale belong to Aetheria and
|
||||
lower to this physical contract before evaluation.
|
||||
|
||||
Required CPU compositions are:
|
||||
|
||||
- allocation-free point sampling and structure-of-arrays batches;
|
||||
- bounded ray/surface intersection and segment clearance;
|
||||
- great-circle and arbitrary path profiles;
|
||||
- region min/max/slope queries with declared error;
|
||||
- deterministic page baking identical to the GPU page contract;
|
||||
- projection-tile sampling.
|
||||
|
||||
World-space APIs use double precision for astronomical positions and reduce to
|
||||
a stable planet-local frame before float field evaluation. Floating-origin
|
||||
policy must not perturb the field.
|
||||
|
||||
## Daemon integration
|
||||
|
||||
The Aetheria daemon publishes typed planet definitions and field versions
|
||||
through CultCache/CultMesh. Simulation systems query CultMath locally in the
|
||||
daemon process. A result becomes game truth only when an Aetheria simulation
|
||||
owner uses it in a committed operation or frame.
|
||||
|
||||
The daemon may maintain CPU pages, spatial summaries, collision meshes, or
|
||||
navigation grids. Every cache key includes complete field identity and query
|
||||
scale. Cache misses evaluate the canonical field or report deferral; they never
|
||||
substitute a different terrain function.
|
||||
|
||||
The initial integration targets Aetheria's current C# daemon host. If authority
|
||||
moves to Rust, the CultMath semantics and parity corpus specify the Rust port.
|
||||
C# and HLSL remain first-class rather than becoming Unity artifacts.
|
||||
|
||||
## Unity 3D lowering
|
||||
|
||||
`CultMath.Unity` is a thin package outside core CultMath. It owns conversion to
|
||||
Unity types, graphics buffers, shader entry points, page-atlas dispatch,
|
||||
camera-relative patch submission, render-pipeline integration, and materials.
|
||||
|
||||
It consumes CultMath HLSL for page generation, residual composition, sampling,
|
||||
radial refinement, and differentials. Aetheria supplies the published planet
|
||||
definition. Unity does not retain an independent planet generator.
|
||||
|
||||
## Electron/Web 2D lowering
|
||||
|
||||
A map follows one path:
|
||||
|
||||
```text
|
||||
pixel -> inverse projection -> unit sphere direction
|
||||
-> scale-aware surface query -> evidence layer
|
||||
-> client-owned palette, labels, and interaction
|
||||
```
|
||||
|
||||
CultMath initially owns forward and inverse transforms for:
|
||||
|
||||
- equirectangular;
|
||||
- Web Mercator with an explicit polar cutoff;
|
||||
- Equal Earth or Mollweide equal-area projection;
|
||||
- orthographic globe view;
|
||||
- azimuthal equidistant and equal-area local views;
|
||||
- cube-face atlas;
|
||||
- local tangent-plane maps.
|
||||
|
||||
Two delivery modes share the same identity:
|
||||
|
||||
1. The daemon or attached worker generates versioned projection tiles and
|
||||
publishes typed descriptors plus binary assets through CultMesh/CDN. This is
|
||||
the baseline for Electron and thin browser clients.
|
||||
2. A future WebGPU lowering evaluates locally for unusual projections or highly
|
||||
interactive inspection.
|
||||
|
||||
The first mode provides one auditable CPU implementation without requiring a
|
||||
JavaScript terrain port. WebGPU should be added only with a WGSL parity harness,
|
||||
not as a hand-translated browser generator.
|
||||
|
||||
A projection-tile key includes planet and field version, projection and
|
||||
parameters, evidence layer, tile level/coordinates, dimensions and border,
|
||||
wavelength/footprint policy, and sampling/encoding version. Tiles are derived
|
||||
assets. Strategic objects, territories, visibility, orders, and simulation
|
||||
facts remain typed Aetheria state layered over them.
|
||||
|
||||
## Field definition and publication
|
||||
|
||||
The portable field definition is a typed document, not renderer configuration.
|
||||
It contains stable parameters, coordinate conventions, component identities,
|
||||
and a content fingerprint. CultCache stores it; CultMesh publishes it. JSON is
|
||||
only a schema, debug, or xenos-boundary projection.
|
||||
|
||||
- CultMath emits geometry and field evidence.
|
||||
- Aetheria decides biomes, resources, movement, settlement, and gameplay.
|
||||
- Unity decides 3D material lowering.
|
||||
- Electron decides map palettes, contours, labels, and overlays.
|
||||
|
||||
## Execution roadmap
|
||||
|
||||
### Phase 0: contract and parity corpus
|
||||
|
||||
Define field identity, coordinates, query scale, sample, and error types. Record
|
||||
canonical point, seam, corner, wavelength, and projection fixtures. Separate
|
||||
exact integer/hash requirements from bounded floating-point parity.
|
||||
|
||||
Exit: CPU, HLSL, page, and projection tests name the same field contract without
|
||||
renderer-owned types.
|
||||
|
||||
### Phase 1: topology extraction
|
||||
|
||||
Move cube-face, tile-key, direction/UV, border, and seam algorithms to CultMath.
|
||||
Replace Fensalir copies and test all edges and corners.
|
||||
|
||||
Exit: Fensalir contains no private cube-sphere terrain topology.
|
||||
|
||||
### Phase 2: authoritative CPU queries
|
||||
|
||||
Add scale-aware point/batch queries, double-to-local reduction, differentials,
|
||||
bounds, ray intersection, clearance, and path profiles. Integrate an Aetheria
|
||||
daemon simulation smoke.
|
||||
|
||||
Exit: the daemon makes a deterministic terrain decision without Unity or
|
||||
Fensalir assemblies.
|
||||
|
||||
### Phase 3: page machinery extraction
|
||||
|
||||
Move page layouts, CPU baking, residual bands, summaries, sampling, and matching
|
||||
HLSL into CultMath. Preserve Fensalir measurements and transition evidence.
|
||||
|
||||
Exit: CPU and HLSL pages match direct queries within declared unresolved bounds;
|
||||
Fensalir is only a resource/dispatch adapter.
|
||||
|
||||
### Phase 4: residency and patch extraction
|
||||
|
||||
Move projected-error selection, fallback, transition weights, patch topology,
|
||||
and refinement composition. Keep camera projection and drawing in renderers.
|
||||
|
||||
Exit: Unity and Fensalir consume the same page set and patch description for
|
||||
equivalent camera/error inputs.
|
||||
|
||||
### Phase 5: Unity adapter and Aetheria viewer
|
||||
|
||||
Build `CultMath.Unity`, bind daemon-published definitions, render from orbit to
|
||||
ground, and compare Unity readback and seams against the parity corpus.
|
||||
|
||||
Exit: Aetheria views the daemon-defined planet without a second generator.
|
||||
|
||||
### Phase 6: projections and map tiles
|
||||
|
||||
Implement projection transforms, footprint calculation, antialiased tile
|
||||
sampling, CultMesh/CDN publication, and Electron elevation, slope, shaded
|
||||
relief, plus one Aetheria strategic overlay.
|
||||
|
||||
Exit: Unity and Electron identify matching landmarks at shared directions;
|
||||
changing projection changes only the chart.
|
||||
|
||||
### Phase 7: optional WebGPU lowering
|
||||
|
||||
Add WGSL only if interactive local evaluation earns its cost, then prove point,
|
||||
seam, page, and projection parity. Daemon tiles remain production until then.
|
||||
|
||||
### Phase 8: delete obsolete owners
|
||||
|
||||
Remove superseded Zyphos/Fensalir algorithms and Aetheria's Unity-only celestial
|
||||
terrain from authoritative paths.
|
||||
|
||||
Exit: renderer code, cache state, or projection cannot change the planet.
|
||||
|
||||
## Verification matrix
|
||||
|
||||
| Claim | CPU daemon | Fensalir | Unity | Electron tiles | WebGPU later |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| Field identity/hash | exact | exact | exact | exact metadata | exact |
|
||||
| Point height/evidence | reference | bounded | bounded | sampled | bounded |
|
||||
| Six-face edges/corners | required | required | required | projection-specific | required |
|
||||
| Wavelength selection | reference | bounded | bounded | encoded in key | bounded |
|
||||
| Page residual sum | reference | required | required | optional source | required |
|
||||
| Projection round trip | reference | optional | optional | required | required |
|
||||
| Cache-independent answer | required | required | required | required | required |
|
||||
|
||||
Negative checks must prove that residency cannot change a direct query,
|
||||
projection cannot change the spherical field, Unity settings cannot alter daemon
|
||||
identity, stale tiles fail version checks, omitted wavelengths remain within
|
||||
their bound, seams/poles remain single-valued, and reload/eviction cannot commit
|
||||
gameplay state.
|
||||
|
||||
## Initial deliverable
|
||||
|
||||
1. Extract cube-sphere topology and scale-aware CPU point/batch queries.
|
||||
2. Add an Aetheria daemon smoke for height, normal, slope, and error.
|
||||
3. Add equirectangular and Equal Earth CPU projection sampling.
|
||||
4. Produce one versioned elevation tile and one slope tile.
|
||||
5. Match the same fixture directions against Fensalir's HLSL oracle.
|
||||
|
||||
This proves the daemon and map architecture before moving the larger page and
|
||||
patch machine. It produces useful Aetheria output without letting a quick tile
|
||||
generator become a second planetary authority.
|
||||
68
docs/eve-direct2d-client-surface.md
Normal file
68
docs/eve-direct2d-client-surface.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Eve Direct2D Client Surface
|
||||
|
||||
Fensalir's DirectWrite/Direct2D overlay is an Eve client surface.
|
||||
|
||||
That means it should consume the same CultMesh UI surface documents as the
|
||||
browser, iOS, Android, and Flutter clients. It should not become a second
|
||||
dashboard language or an app-specific policy owner. The provider still owns
|
||||
state and side effects; Fensalir owns the native desktop/runtime lowering.
|
||||
|
||||
## Authority Map
|
||||
|
||||
- Owner: provider repos own surface state, accepted commands, and meaning.
|
||||
- Input document: the shared Eve/CultMesh surface contract owns panels, nodes,
|
||||
assets, commands, selection, and state paths.
|
||||
- Lowering: Fensalir translates the surface document into `AquariumUiDocument`
|
||||
panels and controls.
|
||||
- Renderer: `DirectWriteOverlay` draws the resulting panels through the
|
||||
D3D11-on-12 Direct2D bridge after the D3D12 scene.
|
||||
- Output: Fensalir emits user commands back through the same Eve/CultNet command
|
||||
path as browser and mobile clients.
|
||||
|
||||
## Current Landing Zone
|
||||
|
||||
The existing engine contract already has the right narrow surface:
|
||||
|
||||
- `AquariumUiDocument`
|
||||
- `AquariumUiPanel`
|
||||
- `AquariumUiControl`
|
||||
- `D3D12Renderer.UpdateUi`
|
||||
- `DirectWriteOverlay`
|
||||
|
||||
`src/Aquarium.EveSurface` is the first native client cut. It subscribes to
|
||||
Mimir's `/eve/deck` WebSocket, opens a provider such as `voidbot.swarm`, and
|
||||
lowers the received surface document into `AquariumUiDocument` panels. It does
|
||||
not teach the renderer about VoidBot, Mimir, or any provider-specific dashboard.
|
||||
Provider specificity belongs in the provider document.
|
||||
|
||||
Run it locally:
|
||||
|
||||
```powershell
|
||||
$env:FENSALIR_EVE_BROKER = 'ws://127.0.0.1:8795/eve/deck'
|
||||
$env:FENSALIR_EVE_PROVIDER = 'voidbot.swarm'
|
||||
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.EveSurface\Aquarium.EveSurface.csproj
|
||||
```
|
||||
|
||||
## Invariants
|
||||
|
||||
- Browser Eve remains the behavior and layout reference.
|
||||
- Direct2D is a renderer for the shared contract, not the contract owner.
|
||||
- Fensalir may own native text metrics, focus, keyboard, pointer dispatch,
|
||||
opacity, panel positioning, and GPU/CPU timing telemetry.
|
||||
- Fensalir must not own app-specific command semantics.
|
||||
- Images and media assets need explicit asset handles/cache keys before they are
|
||||
renderer inputs.
|
||||
- High-frequency sensor data stays in the timestamped sensor path, not in UI
|
||||
panel state.
|
||||
|
||||
## First Cut
|
||||
|
||||
1. Move the mirrored first schema into a neutral shared Eve/CultMesh contract
|
||||
package once the browser reference stops changing daily.
|
||||
2. Expand node lowering into `AquariumUiPanel` and existing controls:
|
||||
readouts, buttons, toggles, sliders, options, text boxes, tree items, and
|
||||
detail panes.
|
||||
3. Route control activation back to a provider command sink.
|
||||
4. Add a replay smoke using a recorded VoidBot/Mimir surface fixture.
|
||||
5. Compare the Direct2D rendering against the browser reference fixture before
|
||||
adding provider-specific polish.
|
||||
|
|
@ -394,8 +394,12 @@ Add contributors in this order:
|
|||
levels, aggregation path count, census radius, and P1/P2 smoothness
|
||||
penalties. The D3D12 renderer now accounts for dispatch-ready stereo
|
||||
lowerings only when the planned Height claim and all three resolved resources
|
||||
are present; it reports that readiness separately from the not-yet-installed
|
||||
SGM kernel.
|
||||
are present. The first installed compute lane is `D3D12StereoDepth.hlsl`,
|
||||
which handles the immediate Leap shape where left and right observations are
|
||||
packed into the R/G channels of one Texture2D and writes a crude R16F
|
||||
disparity by local SAD/block matching. This is a socket-owning live depth
|
||||
proof, not the final libSGM-style census/aggregation SGM kernel and not
|
||||
calibrated metric depth.
|
||||
|
||||
Exit gate:
|
||||
|
||||
|
|
|
|||
378
docs/stateless-planetary-erosion-rendering-plan.md
Normal file
378
docs/stateless-planetary-erosion-rendering-plan.md
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
# Stateless Planetary Erosion Rendering Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Render planetary-scale, erosion-shaped terrain from one deterministic spherical
|
||||
field, from orbit to ground, without making cube faces, cache residency, request
|
||||
order, or the renderer into competing terrain authorities.
|
||||
|
||||
CultMath now carries the faithful point-evaluable C# and HLSL erosion contract,
|
||||
and Zyphos lowers it through persistent residual pages into coarse raster
|
||||
patches with bounded per-pixel refinement. The implementation and verification
|
||||
evidence for each phase are recorded below.
|
||||
|
||||
## Current mechanism
|
||||
|
||||
CultMath now owns the source-grounded erosion filter in matching C# and HLSL,
|
||||
including deterministic integer cell hashing, physical wavelength selection,
|
||||
and a measured compute-shader parity surface. Zyphos can evaluate that field
|
||||
directly and can generate bordered cube-sphere pages whose overlapping samples
|
||||
and GPU-reduced summaries have dedicated tests.
|
||||
|
||||
The renderer now owns a persistent six-root page set plus camera-selected
|
||||
residual descendants. Pages carry height, world-space tangent gradient,
|
||||
material evidence, conservative summaries, and independent content and
|
||||
presentation versions. Parent/child arrival and eviction preserve one terrain
|
||||
field by fading only the child residual.
|
||||
|
||||
The production lowering is coarse cube-sphere raster patches with bounded
|
||||
per-pixel radial refinement against those pages. The general SDF renderer is a
|
||||
debug oracle, not the primary planet backend: enabling the full planet SDF has
|
||||
removed the D3D12 device under the current workload, while page generation with
|
||||
the planet SDF disabled completes cleanly. The page-backed planet is visibly
|
||||
proven through the candidate reservoir with corrected pixel depth. Zyphos's
|
||||
legacy two-million-splat planet writer has been removed; it ran after the patch,
|
||||
erased the same surface, and consumed most of the former frame budget.
|
||||
|
||||
## Progress ledger
|
||||
|
||||
| Phase | State | Evidence |
|
||||
|---|---|---|
|
||||
| 1. Faithful filter | Complete | C# reference fixtures and adapted HLSL contract |
|
||||
| 2. CPU/GPU parity | Complete | Compute readback comparison over 2,052 samples |
|
||||
| 3. Physical bands | Complete | Wavelength selection, fractional terminal octave, unresolved bound |
|
||||
| 4. GPU pages | Complete | Spherical borders, sibling seam tests, GPU summary reduction |
|
||||
| 5. Page-backed intersection | Complete | Persistent page and summary buffers, conservative bounds/steps, bracketed refinement, radial hit parity |
|
||||
| 6. Quadtree transitions | Complete | Exact 0/0.5/1 arrival captures plus cross-face half-eviction capture and lifecycle probes |
|
||||
| 7. Unified differentials/materials | Complete | World-gradient pages, composed radial normals, shared ridge/gully material evidence |
|
||||
| 8. Profiling/backend decision | Complete | Orbit-to-ground raster path, page generation, residency, and total frame fit named budgets |
|
||||
|
||||
"Complete" means the phase exit criterion has authoritative evidence.
|
||||
|
||||
## Authority map
|
||||
|
||||
- **Owner:** CultMath owns the deterministic spherical erosion operator and its
|
||||
matching C# and HLSL semantics.
|
||||
- **Inputs:** unit planet direction, base height, world-distance tangent
|
||||
gradient, planet seed, planet radius, requested frequency band, and bounded
|
||||
erosion parameters.
|
||||
- **Outputs:** radial height residual, tangent gradient, ridge mask, gully mask,
|
||||
drainage hint, and declared unresolved error.
|
||||
- **Derived state:** cube-sphere pages, page borders, min/max reductions,
|
||||
residency records, render meshes, and debug projections are disposable
|
||||
lowerings.
|
||||
- **Client policy:** Zyphos owns terrain composition, parameter presets,
|
||||
geological meaning, biome use, and authored overrides.
|
||||
- **Forbidden writers:** face-local terrain noise, tile-local seeds, seam repair
|
||||
passes, renderer-only erosion logic, and caches that change terrain when
|
||||
evicted.
|
||||
- **Shared path:** direct compute evaluation, CPU page baking, GPU page baking,
|
||||
reload, and LOD transitions evaluate the same field contract.
|
||||
- **Deletion line:** remove direct full-spectrum erosion from the repeated
|
||||
sphere-tracing loop once page-backed geometry proves equivalent. Keep direct
|
||||
evaluation as a verification oracle. The visible fallback is the nearest
|
||||
resident ancestor band; the root fallback is the uneroded base field until a
|
||||
root page is available.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. The same seed and spherical point produce the same terrain regardless of
|
||||
cube face, tile level, request order, cache state, or evaluation backend.
|
||||
2. Parent and child pages share identical low-frequency terrain. A child adds
|
||||
only the residual band it can represent.
|
||||
3. Cube faces are charts and scheduling domains, never terrain owners.
|
||||
4. Geometry displacement, visible normals, material masks, and CPU queries
|
||||
describe the same final field.
|
||||
5. Cache eviction affects latency and fidelity availability, not world truth.
|
||||
6. Frequencies above the projected or page Nyquist limit do not enter geometry.
|
||||
7. Visual erosion does not claim watershed, sediment, lake, or river authority.
|
||||
8. SDF stepping remains conservative throughout page arrival and LOD blending.
|
||||
|
||||
## Target rendering pipeline
|
||||
|
||||
```text
|
||||
planet seed + spherical position + client terrain policy
|
||||
|
|
||||
v
|
||||
canonical spherical field
|
||||
|
|
||||
GPU compute or CPU oracle
|
||||
|
|
||||
v
|
||||
cube-sphere height/gradient/material pages
|
||||
|
|
||||
quadtree residency + frequency-band LOD
|
||||
|
|
||||
v
|
||||
coarse tessellated cube-sphere patches
|
||||
|
|
||||
bounded pixel radial refinement
|
||||
|
|
||||
v
|
||||
differential normal + material evaluation
|
||||
|
|
||||
v
|
||||
diegetic PBR presentation
|
||||
```
|
||||
|
||||
## Surface-page contract
|
||||
|
||||
Each resident page represents one cube-sphere quadtree tile and stores an
|
||||
interior region plus a measured filtering border. Initial measurements should
|
||||
start at 128 by 128 interior samples and 4--8 border texels; these values are
|
||||
experiments, not doctrine.
|
||||
|
||||
Required channels:
|
||||
|
||||
- radial displacement;
|
||||
- two-component tangent gradient in a declared tile tangent frame;
|
||||
- ridge and gully masks;
|
||||
- material or climate evidence required by the client;
|
||||
- validity/confidence and active frequency-band metadata.
|
||||
|
||||
Required summaries are minimum and maximum displacement, unresolved height
|
||||
error, maximum slope or curvature, angular texel size, included wavelength
|
||||
range, field version, and parameter identity.
|
||||
|
||||
Borders sample the canonical spherical function beyond face and tile edges.
|
||||
They do not copy a neighbor's potentially missing page. Seam truth therefore
|
||||
does not depend on residency.
|
||||
|
||||
## Geometry and intersection
|
||||
|
||||
The primary backend rasterizes coarse cube-sphere quad patches. Vertex work
|
||||
places a conservative, low-frequency surface from the resident root and
|
||||
ancestor pages. The pixel shader follows the interpolated camera ray through a
|
||||
small fixed number of radial corrections against the complete resident page
|
||||
field, then evaluates the shared differential and material contract. Fine
|
||||
terrain therefore costs only covered pixels; the rasterizer supplies coverage,
|
||||
clipping, and coarse visibility instead of idling beside a full-screen marcher.
|
||||
|
||||
Patch selection and tessellation use page-summary projected error. Fine bands
|
||||
do not force matching geometric subdivision unless their displacement error can
|
||||
move the silhouette or exceed the pixel-refinement capture range. Summary
|
||||
bounds must make the coarse patch conservative so the true surface cannot fall
|
||||
outside its rasterized coverage.
|
||||
|
||||
The general SDF path remains a direct verification oracle and diagnostic view.
|
||||
It may use expanded-sphere broad phase and bracketed radial refinement, but it
|
||||
does not own production visibility or terrain truth.
|
||||
|
||||
## Differential and normal contract
|
||||
|
||||
For unit direction `d`, planet radius `R`, radial height `h`, and
|
||||
world-distance tangent gradient `grad(h)`, the surface is
|
||||
|
||||
```text
|
||||
P(d) = (R + h) d
|
||||
```
|
||||
|
||||
and the normal is derived from the same differential, approximately
|
||||
|
||||
```text
|
||||
N = normalize((R + h) d - grad(h))
|
||||
```
|
||||
|
||||
The exact scaling must be derived and tested for the selected gradient units.
|
||||
Cube-face UV differences are not the visible-normal authority. Authored
|
||||
residuals publish compatible derivatives or use the same bounded sampling
|
||||
contract.
|
||||
|
||||
The raster lowering derives its visible normal from screen derivatives of the
|
||||
four-step refined world position, oriented toward the camera with a radial
|
||||
fallback. This differentiates the final composed surface without repeating five
|
||||
base-field evaluations per pixel. CPU queries and the direct oracle retain the
|
||||
analytic planet-space differential contract.
|
||||
|
||||
## LOD and frequency bands
|
||||
|
||||
The terrain is one spectrum evaluated through different filters, not separate
|
||||
orbital and ground worlds.
|
||||
|
||||
- Express erosion scale as physical wavelength using planet radius.
|
||||
- Select octaves from projected footprint and page sampling rate.
|
||||
- Fade bands over a narrow transition interval.
|
||||
- Store children as shared parent low frequencies plus child-resolvable
|
||||
residual bands.
|
||||
- Blend residual contribution; never blend two independent full terrains.
|
||||
- Keep parent pages renderable while children generate or stream.
|
||||
|
||||
Orbital views consume continental and major-relief bands. Flight adds regional
|
||||
drainage and ridge bands. Ground adds local geometry and may lower smaller
|
||||
omitted bands into normal/material detail from the same field.
|
||||
|
||||
## Material use
|
||||
|
||||
Ridge, gully, slope, height, climate, and authored masks are material evidence.
|
||||
They may inform exposed rock, soil, moisture, snow, vegetation, and client
|
||||
overrides. Drainage hints are not guaranteed rivers. Saga or a later hydrology
|
||||
organ owns conserved flow, watershed topology, lakes, sediment, and climate
|
||||
coupling.
|
||||
|
||||
## Implementation roadmap
|
||||
|
||||
### Phase 1: faithful filter and reference fixtures
|
||||
|
||||
- Replace the approximation with a source-grounded advanced erosion filter.
|
||||
- Implement cell pivots, fade handling, stacked fading, partial normalization,
|
||||
straight-gully slopes, ridge/crease rounding, and drainage outputs.
|
||||
- Record provenance and MPL obligations for adapted reference code.
|
||||
- Create fixed CPU fixtures from the reference implementation.
|
||||
|
||||
Exit: CultMath C# matches reference samples and exposes the complete contract.
|
||||
|
||||
### Phase 2: measured CPU/GPU parity
|
||||
|
||||
- Evaluate fixed spherical samples in a small compute shader.
|
||||
- Read results back and compare height, gradient, and masks against C#.
|
||||
- Replace transcendental hashing with integer hashing if strict cross-device
|
||||
determinism requires it.
|
||||
- Document ordinary and singular-region tolerances.
|
||||
|
||||
Exit: parity is measured over thousands of samples, not inferred from similar
|
||||
source text.
|
||||
|
||||
### Phase 3: physical scale and band selection
|
||||
|
||||
- Add planet radius and wavelength-based parameters.
|
||||
- Select octaves from requested world-space footprint.
|
||||
- Prove low-frequency identity when higher bands are absent.
|
||||
- Add broad, regional, local, and shading-only band presets.
|
||||
|
||||
Exit: planet radius does not silently alter geological scale and band limiting
|
||||
does not rearrange broad terrain.
|
||||
|
||||
### Phase 4: GPU surface-page generation
|
||||
|
||||
- Generate displacement, tangent gradient, ridge, gully, and material evidence.
|
||||
- Sample borders through the spherical field across cube adjacencies.
|
||||
- Reduce displacement bounds and error/slope summaries.
|
||||
- Key pages by planet, field version, parameters, tile, and frequency band.
|
||||
|
||||
Exit: independently generated overlapping pages produce identical samples.
|
||||
|
||||
### Phase 5: page-backed terrain intersection
|
||||
|
||||
- Add page lookup to the Zyphos intersection path.
|
||||
- Use min/max summaries for broad-phase culling and conservative stepping.
|
||||
- Add bracketed near-surface root refinement.
|
||||
- Preserve direct compute evaluation as the oracle. Use the nearest resident
|
||||
ancestor, or the base field before root residency, as the visible fallback.
|
||||
- Remove repeated full-spectrum erosion from page-backed sphere tracing.
|
||||
|
||||
Exit: direct and page-backed hits agree within declared page-filter error.
|
||||
|
||||
### Phase 6: stable quadtree transitions
|
||||
|
||||
- Select the cut by projected geometric error and residency.
|
||||
- Generate child residual bands relative to parent low frequencies.
|
||||
- Fade residuals without double counting.
|
||||
- Exercise orbital descent, lateral flight, teleport, and reload.
|
||||
|
||||
Exit: no visible popping, crawling, or terrain rearrangement.
|
||||
|
||||
### Phase 7: differential shading and materials
|
||||
|
||||
- Derive normals from composed displacement differentials.
|
||||
- Incorporate authored residual gradients.
|
||||
- Feed terrain evidence into client material policy.
|
||||
- Lower unresolved bands into matching normal/material detail where useful.
|
||||
|
||||
Exit: geometry, lighting, materials, and CPU queries agree on one surface.
|
||||
|
||||
### Phase 8: performance and backend decision
|
||||
|
||||
- Measure per-ray calls, octave cost, page generation, occupancy, residency,
|
||||
cache hit rate, and frame cost from orbit to ground.
|
||||
- Render coarse tessellated cube-sphere patches and perform a fixed, bounded
|
||||
page-backed radial refinement in the pixel shader.
|
||||
- Use the SDF backend only as a correctness oracle and diagnostic comparison.
|
||||
- Prove candidate-buffer, depth, front/back-face, and reservoir ownership with
|
||||
an actual visible capture before tuning appearance.
|
||||
- Keep one terrain contract regardless of backend.
|
||||
|
||||
Exit: the chosen lowering fits named frame and memory budgets.
|
||||
|
||||
Initial budgets at 640 by 360 are: no more than 1.0 ms GPU for terrain patch
|
||||
raster plus pixel refinement, no more than 1.0 ms amortized GPU for page
|
||||
generation, no more than 64 MiB resident terrain pages, and 16.67 ms total GPU
|
||||
for a 60 Hz frame. The first six-root measurement is 2,646 page samples,
|
||||
approximately 83.3 KiB resident, and about 0.04--0.05 ms for the patch draw.
|
||||
The visible six-root capture measures approximately 0.071 ms for patch raster
|
||||
and refinement, 0.932 ms for the complete candidate pass, and 1.37 ms recorded
|
||||
GPU work, with 2,646 samples occupying about 83.3 KiB. At the exterior ground
|
||||
camera, eleven pages occupy 152.6 KiB, repeated page generation measures 0.624
|
||||
ms, the 64 by 64 patch measures 0.799 ms, and recorded GPU work is 1.649 ms.
|
||||
The earlier 24--26 ms
|
||||
frame and black capture came from the obsolete fractal-splat planet writer,
|
||||
which repainted the patch and is no longer published by Zyphos. Page generation
|
||||
and the selected raster lowering therefore fit the named budgets.
|
||||
|
||||
## Verification matrix
|
||||
|
||||
Numerical probes cover repeatability, CPU/GPU parity, scale invariance,
|
||||
band-limited identity, direct-versus-page error, and finite behavior near weak
|
||||
gradients and basis switches.
|
||||
|
||||
Topology probes cover all twelve cube edges, all eight corners, sibling and
|
||||
parent/child neighbors, independent request order, eviction, and page-border
|
||||
overlap at several levels.
|
||||
|
||||
The root-page GPU topology probe groups canonical spherical boundary samples
|
||||
and verifies all `12 * (9 - 2) = 84` non-corner edge groups plus all eight
|
||||
three-face corners within `1e-5`.
|
||||
|
||||
Timeline probes cover direct load, orbital descent, cube-edge crossing,
|
||||
transition midpoint and arrival, missing-page fallback, page arrival, eviction,
|
||||
regeneration, reload, and field-version change.
|
||||
|
||||
Visible debug views expose face/tile/level, residency, field version, active
|
||||
wavelengths, height bounds, tangent gradient, final normal, ridge/gully masks,
|
||||
parent residual blend, direct-minus-page error, and cross-edge discontinuity.
|
||||
|
||||
Patch-native runtime modes currently provide:
|
||||
|
||||
- mode 22: selected face, deepest level, and residual blend;
|
||||
- mode 23: sampled height, maximum slope, and unresolved/error bound;
|
||||
- mode 24: refined raster normal;
|
||||
- mode 25: ridge, gully, and residual blend;
|
||||
- mode 26: cube-edge proximity, cross-edge height delta, and missing-side
|
||||
residency;
|
||||
- mode 27: direct filtered erosion minus summed page height, normalized by the
|
||||
declared error bound, with bound violations in blue;
|
||||
- mode 28: content-version fingerprint, active spacing ratio, and deepest
|
||||
resident level.
|
||||
|
||||
## Performance questions
|
||||
|
||||
- How many terrain evaluations occur per ray and resolved pixel?
|
||||
- What is the cost per octave on target GPUs?
|
||||
- What page size and border minimize generation and filtering waste?
|
||||
- Can page generation overlap rendering without starving the D3D12 queue?
|
||||
- What page budget preserves orbital and ground traversal locality?
|
||||
- Does conservative SDF refinement remain competitive with opaque patches?
|
||||
|
||||
No performance claim follows from a headless boot or successful shader compile.
|
||||
Measure the actual visible path.
|
||||
|
||||
## Completion audit
|
||||
|
||||
1. The four-step correction primitive has matching CultMath C# and HLSL bodies;
|
||||
CPU convergence probes, direct/page GPU readback, cube topology probes, and
|
||||
transition debug captures cover interiors, edges, and lifecycle states.
|
||||
2. The complete audit passes: 106 Engine tests, 170 Fractal tests, and 36
|
||||
CultMath tests. The current solution builds without warnings or errors.
|
||||
3. The patch stages compile in the focused shader smoke and in visible runtime
|
||||
captures. Runtime shader-build latency is tracked separately from frame cost.
|
||||
4. Page generation, patch refinement, candidate/reservoir work, total GPU time,
|
||||
and residency have separate measurements and fit the named budgets.
|
||||
|
||||
## Sources
|
||||
|
||||
- Rune Skovbo Johansen, *Fast and Gorgeous Erosion Filter*:
|
||||
<https://blog.runevision.com/2026/03/fast-and-gorgeous-erosion-filter.html>
|
||||
- Alexander Goslin, *Terrain Diffusion / InfiniteDiffusion*:
|
||||
<https://xandergos.github.io/terrain-diffusion/>
|
||||
- Existing Fensalir cube-sphere and field architecture:
|
||||
`research/rendering/cube-sphere-fractal-brushes.md` and
|
||||
`docs/perfect-machine-architecture.md`.
|
||||
|
|
@ -91,9 +91,9 @@ residency, tiled/bin dispatch, GPU accumulation, and clustered visibility.
|
|||
The active GPU boundary is deliberately narrow:
|
||||
|
||||
```text
|
||||
Mimir calibration/device metadata
|
||||
-> AquariumGpuSensorFrame { calibrated cameras + shared GPU textures }
|
||||
-> D3D12 sensor metadata buffers + imported texture SRVs
|
||||
Mimir calibration/device metadata or FieldEvidence camera Feature claims
|
||||
-> AquariumGpuSensorFrame or planned GpuSensorFusion packets
|
||||
-> D3D12 sensor metadata buffers + imported/leased texture SRVs
|
||||
-> D3D12 GPU sensor fusion compute shader
|
||||
-> RWStructuredBuffer<TemporalGaussian>
|
||||
-> instanced SDF Gaussian draw
|
||||
|
|
@ -103,7 +103,11 @@ Mimir calibration/device metadata
|
|||
Ownership:
|
||||
|
||||
- `AquariumGpuSensorFrame` is the live renderer contract for GPU-owned fusion
|
||||
input.
|
||||
input when a client publishes the legacy direct sensor frame.
|
||||
- FieldEvidence camera-sensor `Feature` claims with `SensorObservation`
|
||||
proposal policy are the current Mimir sensor-fusion contract. They plan to
|
||||
the `GpuSensorFusion` backend, and `D3D12Renderer` derives sensor metadata
|
||||
plus SRV bindings from resolved Texture2D resources.
|
||||
- `AquariumGpuFusionField` remains a temporary fallback/debug contract for
|
||||
already-derived point claims.
|
||||
- Mimir-owned adapters may convert app-specific point claims into compact seeds
|
||||
|
|
@ -131,6 +135,16 @@ raises confidence when stochastic samples appear to correspond, and shrinks the
|
|||
kernel toward the matched surface. Acoustic constraints from the ultrasonic
|
||||
chirplet loop bias confidence and velocity near measured room/position returns.
|
||||
|
||||
The current FieldEvidence cut also consumes Mimir camera texture leases without
|
||||
restoring the retired direct sensor DTO path. LeapStereoIr, Bayer8/R8-style,
|
||||
and YUY2 camera textures are sampled directly by the compute shader. YUY2 uses
|
||||
the documented D3D12/DXGI SRV view path for `DXGI_FORMAT_YUY2`: Fensalir binds
|
||||
an `R8G8B8A8_UNORM` SRV, reads `Y0/U/Y1/V`, and converts to RGB in
|
||||
`D3D12GpuSensorFusion.hlsl` before feature scoring. Provenance:
|
||||
<https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format>.
|
||||
Packed video formats must not pretend to be ordinary RGBA; the declared format
|
||||
selects the decode path.
|
||||
|
||||
Next cut: replace the first-pass descriptor comparison with calibrated
|
||||
epipolar/flow search, Leap packed-map channel extraction, and a persistent GPU
|
||||
correspondence/refinement buffer that can update camera pose and surface tracks
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ Lower mean absolute relative area error is better for terrain density.
|
|||
| normalize | NegativeY | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| normalize | PositiveZ | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| normalize | NegativeZ | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| tangent | PositiveX | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
|
||||
| tangent | NegativeX | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
|
||||
| tangent | PositiveY | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
|
||||
| tangent | NegativeY | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
|
||||
| tangent | PositiveZ | 16 | 0.523891 | 0.873521 | 1.175189 | 0.075993 | 0.088592 |
|
||||
| tangent | NegativeZ | 16 | 0.523891 | 0.873521 | 1.175189 | 0.075993 | 0.088592 |
|
||||
| tangent | PositiveX | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
|
||||
| tangent | NegativeX | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
|
||||
| tangent | PositiveY | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
|
||||
| tangent | NegativeY | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
|
||||
| tangent | PositiveZ | 16 | 0.523866 | 0.873549 | 1.175245 | 0.075944 | 0.088557 |
|
||||
| tangent | NegativeZ | 16 | 0.523866 | 0.873549 | 1.175245 | 0.075944 | 0.088557 |
|
||||
|
||||
Default for the first terrain slice: `tangent`.
|
||||
It is still cheap, remains seam-compatible with the cube face mapping, and the sampler gives it lower area spread than direct normalization on this harness.
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ foreach ($mode in $modes) {
|
|||
RenderDebugMode = $debugMode
|
||||
ReservoirScale = $captureScale
|
||||
SpatialReuseBudget = $captureSpatialReuseBudget
|
||||
CaptureSeconds = 0.0
|
||||
Suffix = $suffix
|
||||
Path = $outputPath
|
||||
})
|
||||
|
|
@ -128,6 +129,7 @@ foreach ($mode in $modes) {
|
|||
|
||||
Write-Host ("Capturing {0} ready={1} debug={2} -> {3}" -f $mode, $readyFrame, $debugMode, $outputPath)
|
||||
$env:AQUARIUM_HEADLESS_READY_FRAMES = [string][Math]::Max(1, $readyFrame)
|
||||
$captureTimer = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$process = Start-Process -FilePath $exePath -ArgumentList $arguments -NoNewWindow -PassThru -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog
|
||||
if (-not $process.WaitForExit([Math]::Max(1, $TimeoutSeconds) * 1000)) {
|
||||
$process.Kill($true)
|
||||
|
|
@ -136,6 +138,7 @@ foreach ($mode in $modes) {
|
|||
}
|
||||
|
||||
$process.WaitForExit()
|
||||
$captureTimer.Stop()
|
||||
$process.Refresh()
|
||||
$exitCode = $process.ExitCode
|
||||
if ($null -eq $exitCode -and (Test-Path $outputPath)) {
|
||||
|
|
@ -155,6 +158,7 @@ foreach ($mode in $modes) {
|
|||
RenderDebugMode = $debugMode
|
||||
ReservoirScale = $captureScale
|
||||
SpatialReuseBudget = $captureSpatialReuseBudget
|
||||
CaptureSeconds = $captureTimer.Elapsed.TotalSeconds
|
||||
Suffix = $suffix
|
||||
Path = $outputPath
|
||||
})
|
||||
|
|
|
|||
255
scripts/measure-reservoir-loss-curve.ps1
Normal file
255
scripts/measure-reservoir-loss-curve.ps1
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
param(
|
||||
[string]$CaptureDirectory = "",
|
||||
[string]$Stamp,
|
||||
[string]$ManifestPath = "",
|
||||
[string]$CandidateMode = "native",
|
||||
[string]$OutputPath = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
if ([string]::IsNullOrWhiteSpace($CaptureDirectory)) {
|
||||
$CaptureDirectory = Join-Path $repoRoot "artifacts\fensalir-captures"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Stamp)) {
|
||||
throw "Stamp is required, for example: -Stamp 20260530-003006"
|
||||
}
|
||||
|
||||
$CaptureDirectory = [System.IO.Path]::GetFullPath($CaptureDirectory)
|
||||
if ([string]::IsNullOrWhiteSpace($ManifestPath)) {
|
||||
$ManifestPath = Join-Path $CaptureDirectory "reservoir-sequence-$Stamp-manifest.csv"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
$OutputPath = Join-Path $CaptureDirectory "reservoir-loss-curve-$Stamp.md"
|
||||
}
|
||||
$ManifestPath = [System.IO.Path]::GetFullPath($ManifestPath)
|
||||
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
|
||||
$csvPath = [System.IO.Path]::ChangeExtension($OutputPath, ".csv")
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
throw "Missing sequence manifest: $ManifestPath"
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$captures = Import-Csv -Path $ManifestPath
|
||||
$byKey = @{}
|
||||
foreach ($capture in $captures) {
|
||||
$key = "{0}|{1}|{2}" -f $capture.Mode, $capture.ReadyFrames, $capture.Suffix
|
||||
$byKey[$key] = $capture
|
||||
}
|
||||
|
||||
function Find-Capture {
|
||||
param(
|
||||
[string]$Mode,
|
||||
[int]$ReadyFrames,
|
||||
[string]$Suffix
|
||||
)
|
||||
|
||||
$key = "{0}|{1}|{2}" -f $Mode, $ReadyFrames, $Suffix
|
||||
if ($byKey.ContainsKey($key)) {
|
||||
return $byKey[$key]
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-PreferredMaskPath {
|
||||
param(
|
||||
[string]$Mode,
|
||||
[int]$ReadyFrames
|
||||
)
|
||||
|
||||
$disocclusion = Find-Capture -Mode $Mode -ReadyFrames $ReadyFrames -Suffix "disocclusion"
|
||||
if ($null -ne $disocclusion) {
|
||||
return $disocclusion.Path
|
||||
}
|
||||
|
||||
$rejection = Find-Capture -Mode $Mode -ReadyFrames $ReadyFrames -Suffix "rejection"
|
||||
if ($null -ne $rejection) {
|
||||
return $rejection.Path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function Measure-Loss {
|
||||
param(
|
||||
[string]$CandidatePath,
|
||||
[string]$ReferencePath,
|
||||
[string]$Label,
|
||||
[int]$ReadyFrames,
|
||||
[string]$CandidateMaskPath = "",
|
||||
[string]$ReferenceMaskPath = ""
|
||||
)
|
||||
|
||||
$candidate = [System.Drawing.Bitmap]::new($CandidatePath)
|
||||
$reference = [System.Drawing.Bitmap]::new($ReferencePath)
|
||||
$candidateMask = if ([string]::IsNullOrWhiteSpace($CandidateMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($CandidateMaskPath) }
|
||||
$referenceMask = if ([string]::IsNullOrWhiteSpace($ReferenceMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($ReferenceMaskPath) }
|
||||
try {
|
||||
if ($candidate.Width -ne $reference.Width -or $candidate.Height -ne $reference.Height) {
|
||||
throw "Capture dimensions differ for $Label."
|
||||
}
|
||||
if ($candidateMask -ne $null -and ($candidateMask.Width -ne $candidate.Width -or $candidateMask.Height -ne $candidate.Height)) {
|
||||
throw "Candidate mask dimensions differ for $Label."
|
||||
}
|
||||
if ($referenceMask -ne $null -and ($referenceMask.Width -ne $candidate.Width -or $referenceMask.Height -ne $candidate.Height)) {
|
||||
throw "Reference mask dimensions differ for $Label."
|
||||
}
|
||||
|
||||
$measuredPixels = 0
|
||||
$changedPixels = 0
|
||||
$absoluteErrorSum = 0.0
|
||||
$squaredErrorSum = 0.0
|
||||
$maxChannelDelta = 0
|
||||
for ($y = 0; $y -lt $candidate.Height; $y++) {
|
||||
for ($x = 0; $x -lt $candidate.Width; $x++) {
|
||||
$maskActive = $true
|
||||
if ($candidateMask -ne $null -or $referenceMask -ne $null) {
|
||||
$maskActive = $false
|
||||
if ($candidateMask -ne $null) {
|
||||
$maskColor = $candidateMask.GetPixel($x, $y)
|
||||
$maskActive = $maskActive -or ([Math]::Max([int]$maskColor.R, [Math]::Max([int]$maskColor.G, [int]$maskColor.B)) -gt 12)
|
||||
}
|
||||
if ($referenceMask -ne $null) {
|
||||
$maskColor = $referenceMask.GetPixel($x, $y)
|
||||
$maskActive = $maskActive -or ([Math]::Max([int]$maskColor.R, [Math]::Max([int]$maskColor.G, [int]$maskColor.B)) -gt 12)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $maskActive) {
|
||||
continue
|
||||
}
|
||||
|
||||
$a = $candidate.GetPixel($x, $y)
|
||||
$b = $reference.GetPixel($x, $y)
|
||||
$dr = [int]$a.R - [int]$b.R
|
||||
$dg = [int]$a.G - [int]$b.G
|
||||
$db = [int]$a.B - [int]$b.B
|
||||
$maxDelta = [Math]::Max([Math]::Abs($dr), [Math]::Max([Math]::Abs($dg), [Math]::Abs($db)))
|
||||
if ($maxDelta -gt 0) {
|
||||
$changedPixels++
|
||||
}
|
||||
|
||||
$absoluteErrorSum += [Math]::Abs($dr) + [Math]::Abs($dg) + [Math]::Abs($db)
|
||||
$squaredErrorSum += ($dr * $dr) + ($dg * $dg) + ($db * $db)
|
||||
if ($maxDelta -gt $maxChannelDelta) {
|
||||
$maxChannelDelta = $maxDelta
|
||||
}
|
||||
$measuredPixels++
|
||||
}
|
||||
}
|
||||
|
||||
$totalPixels = $candidate.Width * $candidate.Height
|
||||
$pixelDenominator = if ($measuredPixels -gt 0) { $measuredPixels } else { 1 }
|
||||
$channelDenominator = $pixelDenominator * 3.0
|
||||
$mse = $squaredErrorSum / $channelDenominator
|
||||
$rmse = [Math]::Sqrt($mse)
|
||||
$psnr = if ($mse -le 0.0) { [double]::PositiveInfinity } else { 10.0 * [Math]::Log10((255.0 * 255.0) / $mse) }
|
||||
|
||||
[pscustomobject]@{
|
||||
Label = $Label
|
||||
ReadyFrames = $ReadyFrames
|
||||
MeasuredPixels = $measuredPixels
|
||||
TotalPixels = $totalPixels
|
||||
FramePercent = $measuredPixels / $totalPixels
|
||||
ChangedPixels = $changedPixels
|
||||
ChangedPercent = $changedPixels / $pixelDenominator
|
||||
MAE = $absoluteErrorSum / $channelDenominator
|
||||
MSE = $mse
|
||||
RMSE = $rmse
|
||||
PSNR = $psnr
|
||||
MaxChannelDelta = $maxChannelDelta
|
||||
CandidatePath = $CandidatePath
|
||||
ReferencePath = $ReferencePath
|
||||
CandidateMaskPath = $CandidateMaskPath
|
||||
ReferenceMaskPath = $ReferenceMaskPath
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($candidateMask -ne $null) {
|
||||
$candidateMask.Dispose()
|
||||
}
|
||||
if ($referenceMask -ne $null) {
|
||||
$referenceMask.Dispose()
|
||||
}
|
||||
$candidate.Dispose()
|
||||
$reference.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$readyFrames = @($captures |
|
||||
Where-Object { $_.Mode -eq $CandidateMode -and $_.Suffix -eq "final" } |
|
||||
ForEach-Object { [int]$_.ReadyFrames } |
|
||||
Sort-Object -Unique)
|
||||
if ($readyFrames.Count -eq 0) {
|
||||
throw "No final candidate captures found for mode '$CandidateMode'."
|
||||
}
|
||||
|
||||
$results = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($readyFrame in $readyFrames) {
|
||||
$candidate = Find-Capture -Mode $CandidateMode -ReadyFrames $readyFrame -Suffix "final"
|
||||
$reference = Find-Capture -Mode "reference" -ReadyFrames $readyFrame -Suffix "final"
|
||||
if ($null -eq $candidate -or $null -eq $reference) {
|
||||
continue
|
||||
}
|
||||
|
||||
$results.Add((Measure-Loss -CandidatePath $candidate.Path -ReferencePath $reference.Path -Label "final" -ReadyFrames $readyFrame))
|
||||
|
||||
$candidateMask = Find-PreferredMaskPath -Mode $CandidateMode -ReadyFrames $readyFrame
|
||||
$referenceMask = Find-PreferredMaskPath -Mode "reference" -ReadyFrames $readyFrame
|
||||
if (-not [string]::IsNullOrWhiteSpace($candidateMask) -or -not [string]::IsNullOrWhiteSpace($referenceMask)) {
|
||||
$results.Add((Measure-Loss -CandidatePath $candidate.Path -ReferencePath $reference.Path -Label "masked" -ReadyFrames $readyFrame -CandidateMaskPath $candidateMask -ReferenceMaskPath $referenceMask))
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.Count -eq 0) {
|
||||
throw "No candidate/reference final pairs found for stamp '$Stamp'."
|
||||
}
|
||||
|
||||
$results | Export-Csv -NoTypeInformation -Path $csvPath
|
||||
|
||||
$lines = [System.Collections.Generic.List[string]]::new()
|
||||
$lines.Add("# Reservoir Reconstruction Loss Curve")
|
||||
$lines.Add("")
|
||||
$lines.Add(("Stamp: ``{0}``" -f $Stamp))
|
||||
$lines.Add(("Candidate mode: ``{0}``" -f $CandidateMode))
|
||||
$lines.Add("")
|
||||
$lines.Add("| Scope | Ready frames | Measured/frame % | Changed % | MAE | MSE | RMSE | PSNR dB | Max delta |")
|
||||
$lines.Add("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
|
||||
foreach ($result in ($results | Sort-Object ReadyFrames, Label)) {
|
||||
$psnrText = if ([double]::IsInfinity($result.PSNR)) { "inf" } else { $result.PSNR.ToString("N3") }
|
||||
$lines.Add(("| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} |" -f
|
||||
$result.Label,
|
||||
$result.ReadyFrames,
|
||||
$result.FramePercent.ToString("P4"),
|
||||
$result.ChangedPercent.ToString("P4"),
|
||||
$result.MAE.ToString("N4"),
|
||||
$result.MSE.ToString("N4"),
|
||||
$result.RMSE.ToString("N4"),
|
||||
$psnrText,
|
||||
$result.MaxChannelDelta))
|
||||
}
|
||||
$lines.Add("")
|
||||
$lines.Add("Loss is RGB reconstruction error against the same-time native-domain reference capture. The reference is higher work-grid scale and higher spatial reuse budget when the manifest was captured that way; it is not an offline path-traced ground truth.")
|
||||
Set-Content -LiteralPath $OutputPath -Value $lines -Encoding UTF8
|
||||
|
||||
Write-Host "Reservoir reconstruction loss curve:"
|
||||
foreach ($result in ($results | Sort-Object ReadyFrames, Label)) {
|
||||
$psnrText = if ([double]::IsInfinity($result.PSNR)) { "inf" } else { $result.PSNR.ToString("N3") }
|
||||
Write-Host (" {0} f{1}: measured {2} changed {3} MAE {4} RMSE {5} PSNR {6} dB max {7}" -f
|
||||
$result.Label,
|
||||
$result.ReadyFrames,
|
||||
$result.FramePercent.ToString("P4"),
|
||||
$result.ChangedPercent.ToString("P4"),
|
||||
$result.MAE.ToString("N4"),
|
||||
$result.RMSE.ToString("N4"),
|
||||
$psnrText,
|
||||
$result.MaxChannelDelta)
|
||||
}
|
||||
Write-Host "Wrote:"
|
||||
Write-Host " $OutputPath"
|
||||
Write-Host " $csvPath"
|
||||
|
|
@ -6,4 +6,8 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\CultMath\src\CultMath\CultMath.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public sealed class AquariumAudioDocument
|
|||
private readonly ConcurrentQueue<AquariumAudioControlFrame> controlFrames = new();
|
||||
private readonly ConcurrentQueue<AquariumStreamingDspProgram> streamingDspPrograms = new();
|
||||
private readonly ConcurrentQueue<AquariumStreamingAudioBlock> streamingAudioBlocks = new();
|
||||
private readonly ConcurrentQueue<AquariumAudioCaptureRequest> captureRequests = new();
|
||||
|
||||
public static AquariumAudioDocument Empty { get; } = new();
|
||||
|
||||
|
|
@ -131,6 +132,38 @@ public sealed class AquariumAudioDocument
|
|||
|
||||
return drained;
|
||||
}
|
||||
|
||||
public void EnqueueSystemLoopbackCapture(
|
||||
string profileId,
|
||||
string sourceId,
|
||||
string displayName,
|
||||
bool enabled,
|
||||
long sequence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profileId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
captureRequests.Enqueue(new AquariumAudioCaptureRequest(
|
||||
profileId,
|
||||
string.IsNullOrWhiteSpace(sourceId) ? "system-loopback" : sourceId,
|
||||
string.IsNullOrWhiteSpace(displayName) ? "System audio" : displayName,
|
||||
AquariumAudioCaptureKind.SystemLoopback,
|
||||
enabled,
|
||||
sequence));
|
||||
}
|
||||
|
||||
public IReadOnlyList<AquariumAudioCaptureRequest> DrainCaptureRequests(int maxRequests = 8)
|
||||
{
|
||||
var drained = new List<AquariumAudioCaptureRequest>();
|
||||
while (drained.Count < maxRequests && captureRequests.TryDequeue(out var request))
|
||||
{
|
||||
drained.Add(request);
|
||||
}
|
||||
|
||||
return drained;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record AquariumPcmAudioChunk(float[] MonoSamples, int SampleRate, float LeftGain = 1.0f, float RightGain = 1.0f);
|
||||
|
|
@ -178,6 +211,19 @@ public sealed record AquariumStreamingAudioChannel(
|
|||
string SourceId,
|
||||
float[] Samples);
|
||||
|
||||
public enum AquariumAudioCaptureKind
|
||||
{
|
||||
SystemLoopback
|
||||
}
|
||||
|
||||
public sealed record AquariumAudioCaptureRequest(
|
||||
string ProfileId,
|
||||
string SourceId,
|
||||
string DisplayName,
|
||||
AquariumAudioCaptureKind Kind,
|
||||
bool Enabled,
|
||||
long Sequence);
|
||||
|
||||
public sealed record AquariumAudioStemFrame(
|
||||
string ProfileId,
|
||||
IReadOnlyList<AquariumAudioStemChannel> Channels,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public readonly record struct GraphicsSettings(
|
|||
float FieldReservoirSpatialReuseBudget)
|
||||
{
|
||||
public const int MinRenderDebugMode = 0;
|
||||
public const int MaxRenderDebugMode = 20;
|
||||
public const int MaxRenderDebugMode = 28;
|
||||
public const int FieldReservoirModeNativeDomain = 0;
|
||||
public const int FieldReservoirModeTexelBaseline = 1;
|
||||
public const float MinFieldReservoirScale = 0.25f;
|
||||
|
|
|
|||
|
|
@ -128,8 +128,13 @@ public enum KeyCode
|
|||
Delete,
|
||||
Enter,
|
||||
LeftArrow,
|
||||
UpArrow,
|
||||
RightArrow,
|
||||
DownArrow,
|
||||
Home,
|
||||
End,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Shift,
|
||||
Control,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,24 @@ public sealed class AquariumShaderBuilder(AquariumShaderManifest manifest)
|
|||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder PlanetarySurfacePage(string path)
|
||||
{
|
||||
manifest.PlanetarySurfacePage(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder PlanetarySurfacePageSummary(string path)
|
||||
{
|
||||
manifest.PlanetarySurfacePageSummary(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder PlanetarySurfacePatch(string path)
|
||||
{
|
||||
manifest.PlanetarySurfacePatch(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder Include(string path)
|
||||
{
|
||||
manifest.Include(path);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ public enum AquariumFieldBackendKind
|
|||
SurfacePage = 5,
|
||||
TubeField = 6,
|
||||
DebugOverlay = 7,
|
||||
GpuSensorFusion = 8,
|
||||
}
|
||||
|
||||
public enum AquariumFieldResourceKind
|
||||
|
|
@ -1124,6 +1125,9 @@ public static class AquariumFieldLoweringPlanner
|
|||
AquariumFieldEncoding.Feature
|
||||
when domainKind == AquariumFieldDomainKind.CameraSensor &&
|
||||
request.ProposalKind == AquariumFieldProposalKind.DeterministicStructural => AquariumFieldBackendKind.DebugOverlay,
|
||||
AquariumFieldEncoding.Feature
|
||||
when domainKind == AquariumFieldDomainKind.CameraSensor &&
|
||||
request.ProposalKind == AquariumFieldProposalKind.SensorObservation => AquariumFieldBackendKind.GpuSensorFusion,
|
||||
_ => AquariumFieldBackendKind.Unknown,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public sealed class AquariumShaderManifest
|
|||
|
||||
public string FractalSplatRenderShader { get; private set; } = "D3D12FractalSplatRender.hlsl";
|
||||
|
||||
public string PlanetarySurfacePageShader { get; private set; } = "D3D12PlanetarySurfacePage.hlsl";
|
||||
|
||||
public string PlanetarySurfacePageSummaryShader { get; private set; } = "D3D12PlanetarySurfacePageSummary.hlsl";
|
||||
|
||||
public string PlanetarySurfacePatchShader { get; private set; } = "D3D12PlanetarySurfacePatch.hlsl";
|
||||
|
||||
public string PostShader { get; private set; } = "D3D12Post.hlsl";
|
||||
|
||||
public string SdfCommonInclude { get; private set; } = "D3D12SdfCommon.hlsli";
|
||||
|
|
@ -92,6 +98,24 @@ public sealed class AquariumShaderManifest
|
|||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest PlanetarySurfacePage(string path)
|
||||
{
|
||||
PlanetarySurfacePageShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest PlanetarySurfacePageSummary(string path)
|
||||
{
|
||||
PlanetarySurfacePageSummaryShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest PlanetarySurfacePatch(string path)
|
||||
{
|
||||
PlanetarySurfacePatchShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest Post(string path)
|
||||
{
|
||||
PostShader = path;
|
||||
|
|
@ -312,6 +336,7 @@ public enum AquariumGpuSensorPixelFormat
|
|||
R16Float,
|
||||
Rg8Unorm,
|
||||
LeapPackedMap,
|
||||
Yuy2,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumExternalGpuTexture(
|
||||
|
|
@ -336,7 +361,8 @@ public readonly record struct AquariumGpuSensorCamera(
|
|||
int Height,
|
||||
int FirstTextureIndex,
|
||||
int TextureCount,
|
||||
long TimestampNs);
|
||||
long TimestampNs,
|
||||
AquariumGpuSensorPixelFormat PixelFormat = AquariumGpuSensorPixelFormat.Unknown);
|
||||
|
||||
public sealed class AquariumGpuSensorFrame
|
||||
{
|
||||
|
|
@ -497,6 +523,8 @@ public sealed class AquariumSceneState
|
|||
|
||||
public AquariumFractalReservoirField FractalReservoirField { get; init; } = AquariumFractalReservoirField.Empty;
|
||||
|
||||
public AquariumPlanetarySurfacePageSet PlanetarySurfacePages { get; init; } = AquariumPlanetarySurfacePageSet.Empty;
|
||||
|
||||
public AquariumGpuSensorFrame GpuSensorFrame { get; init; } = AquariumGpuSensorFrame.Empty;
|
||||
|
||||
public AquariumAcousticFieldFrame AcousticFieldFrame { get; init; } = AquariumAcousticFieldFrame.Empty;
|
||||
|
|
@ -509,9 +537,189 @@ public sealed class AquariumSceneState
|
|||
|
||||
public AquariumBufferFieldFrame BufferFieldFrame { get; init; } = AquariumBufferFieldFrame.Empty;
|
||||
|
||||
public AquariumBokushoBrushFrame BokushoBrushFrame { get; init; } = AquariumBokushoBrushFrame.Empty;
|
||||
|
||||
public AquariumSplineFrame SplineFrame { get; init; } = AquariumSplineFrame.Empty;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPlanetarySurfacePageInput(Vector4 DirectionRadius, Vector4 Sampling);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPlanetarySurfacePageMetadata(Vector4 Address, Vector4 Layout, Vector4 Bounds, Vector4 State);
|
||||
|
||||
public sealed class AquariumPlanetarySurfacePage
|
||||
{
|
||||
public long ContentKey { get; init; }
|
||||
public IReadOnlyList<AquariumPlanetarySurfacePageInput> Samples { get; init; } = [];
|
||||
public AquariumPlanetarySurfacePageMetadata Metadata { get; init; }
|
||||
public bool HasInput => ContentKey != 0 && Samples.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class AquariumPlanetarySurfacePageSet
|
||||
{
|
||||
public static AquariumPlanetarySurfacePageSet Empty { get; } = new();
|
||||
public string GeneratorEntryPoint { get; init; } = "D3D12PlanetaryTerrainPageCS";
|
||||
public long ContentVersion { get; init; }
|
||||
public long PresentationVersion { get; init; }
|
||||
public IReadOnlyList<AquariumPlanetarySurfacePage> Pages { get; init; } = [];
|
||||
public bool RenderCoarsePatches { get; init; }
|
||||
public int PatchCells { get; init; } = 16;
|
||||
public int SampleCount => Pages.Sum(page => page.Samples.Count);
|
||||
public bool HasInput => ContentVersion != 0 && Pages.Count > 0 && Pages.All(page => page.HasInput);
|
||||
}
|
||||
|
||||
public sealed class AquariumBokushoBrushFrame
|
||||
{
|
||||
public static AquariumBokushoBrushFrame Empty { get; } = new();
|
||||
|
||||
public int TuftCount { get; init; }
|
||||
|
||||
public int SampleCount { get; init; }
|
||||
|
||||
public float PhysicsHz { get; init; } = 500.0f;
|
||||
|
||||
public float BrushRadius { get; init; } = 2.2f;
|
||||
|
||||
public float Pressure { get; init; } = 0.72f;
|
||||
|
||||
public float InkLoad { get; init; } = 0.90f;
|
||||
|
||||
public float Wetness { get; init; } = 0.86f;
|
||||
|
||||
public float Splay { get; init; } = 0.72f;
|
||||
|
||||
public float Bend { get; init; } = 0.58f;
|
||||
|
||||
public float Friction { get; init; } = 0.62f;
|
||||
|
||||
public float RadiusScale { get; init; } = 1.0f;
|
||||
|
||||
public float PressureScale { get; init; } = 1.0f;
|
||||
|
||||
public float NormalScale { get; init; } = 1.0f;
|
||||
|
||||
public float TangentScale { get; init; } = 1.0f;
|
||||
|
||||
public Vector4 StrokeP0 { get; init; } = new(-7.2f, 1.0f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP1 { get; init; } = new(-2.4f, -1.8f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP2 { get; init; } = new(2.4f, -1.6f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP3 { get; init; } = new(7.2f, 0.8f, 0.0f, 0.0f);
|
||||
|
||||
public IReadOnlyList<AquariumBokushoBrushStroke> Strokes { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<AquariumBokushoSourcePoint> SourcePoints { get; init; } = [];
|
||||
|
||||
public bool HasInput => TuftCount > 0 && SampleCount > 1 && PhysicsHz > 0.0f;
|
||||
|
||||
public AquariumBokushoBrushFrame Normalized() => new()
|
||||
{
|
||||
TuftCount = Math.Clamp(TuftCount, 1, 4096),
|
||||
SampleCount = Math.Clamp(SampleCount, 2, 4096),
|
||||
PhysicsHz = Math.Clamp(PhysicsHz, 60.0f, 2000.0f),
|
||||
BrushRadius = MathF.Max(0.0001f, BrushRadius),
|
||||
Pressure = Math.Clamp(Pressure, 0.0f, 2.0f),
|
||||
InkLoad = Math.Clamp(InkLoad, 0.0f, 3.0f),
|
||||
Wetness = Math.Clamp(Wetness, 0.0f, 1.6f),
|
||||
Splay = Math.Clamp(Splay, 0.0f, 2.0f),
|
||||
Bend = Math.Clamp(Bend, 0.0f, 2.4f),
|
||||
Friction = Math.Clamp(Friction, 0.0f, 1.0f),
|
||||
RadiusScale = Math.Clamp(RadiusScale, 0.1f, 4.0f),
|
||||
PressureScale = Math.Clamp(PressureScale, 0.1f, 4.0f),
|
||||
NormalScale = Math.Clamp(NormalScale, 0.1f, 4.0f),
|
||||
TangentScale = Math.Clamp(TangentScale, 0.1f, 4.0f),
|
||||
StrokeP0 = StrokeP0,
|
||||
StrokeP1 = StrokeP1,
|
||||
StrokeP2 = StrokeP2,
|
||||
StrokeP3 = StrokeP3,
|
||||
Strokes = Strokes.Select(stroke => stroke.Normalized()).ToArray(),
|
||||
SourcePoints = SourcePoints.Select(point => point.Normalized()).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class AquariumBokushoSourcePoint
|
||||
{
|
||||
public int SourceStrokeId { get; init; } = -1;
|
||||
|
||||
public float T { get; init; }
|
||||
|
||||
public Vector2 Position { get; init; }
|
||||
|
||||
public AquariumBokushoSourcePoint Normalized() => new()
|
||||
{
|
||||
SourceStrokeId = SourceStrokeId,
|
||||
T = Math.Clamp(T, 0.0f, 1.0f),
|
||||
Position = Position,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class AquariumBokushoBrushStroke
|
||||
{
|
||||
public Vector4 StrokeP0 { get; init; } = new(-7.2f, 1.0f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP1 { get; init; } = new(-2.4f, -1.8f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP2 { get; init; } = new(2.4f, -1.6f, 0.0f, 0.0f);
|
||||
|
||||
public Vector4 StrokeP3 { get; init; } = new(7.2f, 0.8f, 0.0f, 0.0f);
|
||||
|
||||
public float RadiusScale { get; init; } = 1.0f;
|
||||
|
||||
public float PressureScale { get; init; } = 1.0f;
|
||||
|
||||
public float NormalScale { get; init; } = 1.0f;
|
||||
|
||||
public float TangentScale { get; init; } = 1.0f;
|
||||
|
||||
public float ShaftTilt { get; init; }
|
||||
|
||||
public float ShaftRotation { get; init; }
|
||||
|
||||
public float GripHeight { get; init; } = 1.0f;
|
||||
|
||||
public float Compliance { get; init; } = 1.0f;
|
||||
|
||||
public float EntryTaper { get; init; } = 0.10f;
|
||||
|
||||
public float ExitTaper { get; init; } = 0.14f;
|
||||
|
||||
public float PigmentScale { get; init; } = 1.0f;
|
||||
|
||||
public float SplitScale { get; init; } = 1.0f;
|
||||
|
||||
public float SegmentStart { get; init; }
|
||||
|
||||
public float SegmentEnd { get; init; } = 1.0f;
|
||||
|
||||
public int SourceStrokeId { get; init; } = -1;
|
||||
|
||||
public AquariumBokushoBrushStroke Normalized() => new()
|
||||
{
|
||||
StrokeP0 = StrokeP0,
|
||||
StrokeP1 = StrokeP1,
|
||||
StrokeP2 = StrokeP2,
|
||||
StrokeP3 = StrokeP3,
|
||||
RadiusScale = Math.Clamp(RadiusScale, 0.1f, 4.0f),
|
||||
PressureScale = Math.Clamp(PressureScale, 0.1f, 4.0f),
|
||||
NormalScale = Math.Clamp(NormalScale, 0.1f, 4.0f),
|
||||
TangentScale = Math.Clamp(TangentScale, 0.1f, 4.0f),
|
||||
ShaftTilt = Math.Clamp(ShaftTilt, -1.5f, 1.5f),
|
||||
ShaftRotation = Math.Clamp(ShaftRotation, -2.0f, 2.0f),
|
||||
GripHeight = Math.Clamp(GripHeight, 0.2f, 2.4f),
|
||||
Compliance = Math.Clamp(Compliance, 0.1f, 2.5f),
|
||||
EntryTaper = Math.Clamp(EntryTaper, 0.01f, 0.50f),
|
||||
ExitTaper = Math.Clamp(ExitTaper, 0.01f, 0.50f),
|
||||
PigmentScale = Math.Clamp(PigmentScale, 0.05f, 4.0f),
|
||||
SplitScale = Math.Clamp(SplitScale, 0.0f, 4.0f),
|
||||
SegmentStart = Math.Clamp(SegmentStart, 0.0f, 1.0f),
|
||||
SegmentEnd = Math.Clamp(SegmentEnd, 0.0f, 1.0f),
|
||||
SourceStrokeId = SourceStrokeId,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class AquariumBufferFieldFrame
|
||||
{
|
||||
public static AquariumBufferFieldFrame Empty { get; } = new();
|
||||
|
|
|
|||
1076
src/Aquarium.Engine.Contracts/Render/BokushoBrushSimulation.cs
Normal file
1076
src/Aquarium.Engine.Contracts/Render/BokushoBrushSimulation.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,12 +3,15 @@ namespace Aquarium.Engine.Ui;
|
|||
public sealed class AquariumUiDocument
|
||||
{
|
||||
private readonly List<AquariumUiPanel> panels = [];
|
||||
private readonly List<AquariumUiSurface> surfaces = [];
|
||||
private readonly List<AquariumConsoleCommand> commands = [];
|
||||
|
||||
public static AquariumUiDocument Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumUiPanel> Panels => panels;
|
||||
|
||||
public IReadOnlyList<AquariumUiSurface> Surfaces => surfaces;
|
||||
|
||||
public IReadOnlyList<AquariumConsoleCommand> Commands => commands;
|
||||
|
||||
public AquariumUiDocument Panel(string title, Action<AquariumUiPanelBuilder> compose)
|
||||
|
|
@ -29,6 +32,35 @@ public sealed class AquariumUiDocument
|
|||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiDocument Surface(string id, string title, float left, float top, float width, float height, Action<AquariumUiSurfaceBuilder> compose)
|
||||
{
|
||||
return Surface(id, title, left, top, width, height, contentPadding: 8.0f, rootGap: 8.0f, rootPadding: 8.0f, compose);
|
||||
}
|
||||
|
||||
public AquariumUiDocument Surface(
|
||||
string id,
|
||||
string title,
|
||||
float left,
|
||||
float top,
|
||||
float width,
|
||||
float height,
|
||||
float contentPadding,
|
||||
float rootGap,
|
||||
float rootPadding,
|
||||
Action<AquariumUiSurfaceBuilder> compose)
|
||||
{
|
||||
var children = new List<AquariumUiElement>();
|
||||
compose(new AquariumUiSurfaceBuilder(children));
|
||||
surfaces.Add(new AquariumUiSurface(
|
||||
"cultmesh.eve_surface.v0",
|
||||
id,
|
||||
title,
|
||||
new AquariumUiRect(left, top, width, height),
|
||||
new AquariumUiElement(id + ".root", "dashboard", title, Role: "root", Layout: AquariumUiLayout.Vertical(rootGap, rootPadding), Children: children),
|
||||
Math.Max(0.0f, contentPadding)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiDocument Command(string name, Func<IReadOnlyList<string>, string> execute, string description = "")
|
||||
{
|
||||
commands.Add(new AquariumConsoleCommand(name, execute, description));
|
||||
|
|
@ -44,6 +76,218 @@ public sealed record AquariumUiPanel(
|
|||
IReadOnlyList<AquariumUiControl> Controls,
|
||||
bool FadeWhenMouseDistant = false);
|
||||
|
||||
public sealed record AquariumUiSurface(
|
||||
string Schema,
|
||||
string Id,
|
||||
string Title,
|
||||
AquariumUiRect Bounds,
|
||||
AquariumUiElement Root,
|
||||
float ContentPadding = 8.0f);
|
||||
|
||||
public sealed record AquariumUiRect(float Left, float Top, float Width, float Height);
|
||||
|
||||
public sealed record AquariumUiLayout(
|
||||
string Direction = "vertical",
|
||||
float Gap = 8.0f,
|
||||
float Padding = 8.0f)
|
||||
{
|
||||
public static AquariumUiLayout Vertical(float gap = 8.0f, float padding = 8.0f) => new("vertical", gap, padding);
|
||||
|
||||
public static AquariumUiLayout Horizontal(float gap = 8.0f, float padding = 8.0f) => new("horizontal", gap, padding);
|
||||
}
|
||||
|
||||
public sealed record AquariumUiStyle(string Variant = "default", string Tone = "neutral");
|
||||
|
||||
public sealed record AquariumUiBinding(
|
||||
string DocumentSchema,
|
||||
string DocumentId,
|
||||
string Path,
|
||||
string ValueKind,
|
||||
string Access = "read",
|
||||
string Authority = "",
|
||||
string? CommandId = null)
|
||||
{
|
||||
public static AquariumUiBinding Read(string documentSchema, string documentId, string path, string valueKind, string authority = "")
|
||||
=> new(documentSchema, documentId, path, valueKind, "read", authority);
|
||||
|
||||
public static AquariumUiBinding ReadWrite(string documentSchema, string documentId, string path, string valueKind, string authority = "")
|
||||
=> new(documentSchema, documentId, path, valueKind, "read-write", authority);
|
||||
|
||||
public static AquariumUiBinding Command(string documentSchema, string documentId, string path, string authority, string commandId)
|
||||
=> new(documentSchema, documentId, path, "command", "command", authority, commandId);
|
||||
}
|
||||
|
||||
public sealed record AquariumUiPreviewItem(
|
||||
string Id,
|
||||
string Label,
|
||||
float X,
|
||||
float Y,
|
||||
float Width,
|
||||
float Height,
|
||||
bool Selected = false,
|
||||
string Tone = "neutral");
|
||||
|
||||
public readonly record struct AquariumUiPreviewInteraction(
|
||||
string ItemId,
|
||||
string Handle,
|
||||
string Phase,
|
||||
float X,
|
||||
float Y,
|
||||
float DeltaX,
|
||||
float DeltaY);
|
||||
|
||||
public readonly record struct AquariumUiPreviewState(
|
||||
string HoverItemId = "",
|
||||
string HoverHandle = "",
|
||||
string ActiveItemId = "",
|
||||
string ActiveHandle = "");
|
||||
|
||||
public readonly record struct AquariumUiPreviewGuide(
|
||||
string Axis,
|
||||
float Position,
|
||||
string Tone = "neutral");
|
||||
|
||||
public sealed record AquariumUiElement(
|
||||
string Id,
|
||||
string Kind,
|
||||
string? Text = null,
|
||||
string? Role = null,
|
||||
AquariumUiLayout? Layout = null,
|
||||
AquariumUiStyle? Style = null,
|
||||
AquariumUiBinding? Binding = null,
|
||||
Func<string>? ReadText = null,
|
||||
Func<double>? ReadMetric = null,
|
||||
Func<bool>? ReadToggle = null,
|
||||
Action<bool>? WriteToggle = null,
|
||||
Func<float>? ReadFloat = null,
|
||||
Action<float>? WriteFloat = null,
|
||||
float Min = 0.0f,
|
||||
float Max = 1.0f,
|
||||
string Format = "0.###",
|
||||
Func<int>? ReadOption = null,
|
||||
Action<int>? WriteOption = null,
|
||||
IReadOnlyList<AquariumUiOption>? Options = null,
|
||||
Func<IReadOnlyList<AquariumUiPreviewItem>>? ReadPreviewItems = null,
|
||||
Func<AquariumUiPreviewState>? ReadPreviewState = null,
|
||||
Func<IReadOnlyList<AquariumUiPreviewGuide>>? ReadPreviewGuides = null,
|
||||
bool PreviewBlitsOutputBuffer = false,
|
||||
Action<AquariumUiPreviewInteraction>? HandlePreviewInteraction = null,
|
||||
Action? Invoke = null,
|
||||
Func<bool>? IsVisible = null,
|
||||
float Weight = 1.0f,
|
||||
float? FixedExtent = null,
|
||||
IReadOnlyList<AquariumUiElement>? Children = null)
|
||||
{
|
||||
public bool Visible => IsVisible?.Invoke() ?? true;
|
||||
}
|
||||
|
||||
public sealed class AquariumUiSurfaceBuilder(List<AquariumUiElement> children)
|
||||
{
|
||||
public AquariumUiSurfaceBuilder Vertical(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
|
||||
{
|
||||
var scopeChildren = new List<AquariumUiElement>();
|
||||
compose(new AquariumUiSurfaceBuilder(scopeChildren));
|
||||
children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Vertical(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Horizontal(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
|
||||
{
|
||||
var scopeChildren = new List<AquariumUiElement>();
|
||||
compose(new AquariumUiSurfaceBuilder(scopeChildren));
|
||||
children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Horizontal(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Row(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, Func<bool>? isVisible = null)
|
||||
{
|
||||
return Horizontal(id, compose, weight, isVisible: isVisible);
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Pane(string id, string title, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f, float? fixedExtent = null)
|
||||
{
|
||||
var paneChildren = new List<AquariumUiElement>();
|
||||
compose(new AquariumUiSurfaceBuilder(paneChildren));
|
||||
children.Add(new AquariumUiElement(id, "pane", title, Style: new AquariumUiStyle("panel", tone), Layout: AquariumUiLayout.Vertical(8.0f, 10.0f), Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: paneChildren));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Card(string id, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f)
|
||||
{
|
||||
var cardChildren = new List<AquariumUiElement>();
|
||||
compose(new AquariumUiSurfaceBuilder(cardChildren));
|
||||
children.Add(new AquariumUiElement(id, "card", Style: new AquariumUiStyle("compact", tone), Layout: AquariumUiLayout.Vertical(4.0f, 8.0f), Weight: Math.Max(0.001f, weight), Children: cardChildren));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Text(string id, string text, string role = "body", float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "text", text, role, Binding: binding, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Text(string id, Func<string> read, string role = "body", float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "text", Role: role, Binding: binding, ReadText: read, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Metric(string id, string label, Func<double> read, string tone = "neutral", float weight = 1.0f, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "metric", label, Style: new AquariumUiStyle("compact", tone), Binding: binding, ReadMetric: read, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Toggle(string id, string label, Func<bool> read, Action<bool> write, float weight = 1.0f, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "toggle", label, Binding: binding, ReadToggle: read, WriteToggle: write, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Slider(string id, string label, Func<float> read, Action<float> write, float min, float max, string format = "0.###", float weight = 1.0f, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "slider", label, Binding: binding, ReadFloat: read, WriteFloat: write, Min: min, Max: max, Format: format, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Options(string id, string label, Func<int> read, Action<int> write, IReadOnlyList<AquariumUiOption> options, float weight = 1.0f, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "select", label, Binding: binding, ReadOption: read, WriteOption: write, Options: options, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Button(string id, string label, Action invoke, float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(id, "button", label, Binding: binding, Invoke: invoke, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiSurfaceBuilder Preview(
|
||||
string id,
|
||||
string label,
|
||||
Func<IReadOnlyList<AquariumUiPreviewItem>> readItems,
|
||||
float weight = 1.0f,
|
||||
bool blitOutputBuffer = false,
|
||||
Action<AquariumUiPreviewInteraction>? handleInteraction = null,
|
||||
Func<AquariumUiPreviewState>? readState = null,
|
||||
Func<IReadOnlyList<AquariumUiPreviewGuide>>? readGuides = null,
|
||||
AquariumUiBinding? binding = null)
|
||||
{
|
||||
children.Add(new AquariumUiElement(
|
||||
id,
|
||||
"preview",
|
||||
label,
|
||||
ReadPreviewItems: readItems,
|
||||
ReadPreviewState: readState,
|
||||
ReadPreviewGuides: readGuides,
|
||||
Binding: binding,
|
||||
PreviewBlitsOutputBuffer: blitOutputBuffer,
|
||||
HandlePreviewInteraction: handleInteraction,
|
||||
Weight: Math.Max(0.001f, weight)));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumUiPanelBuilder(List<AquariumUiControl> controls)
|
||||
{
|
||||
public AquariumUiPanelBuilder Section(string title, Func<bool>? isVisible = null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public enum CubeFace
|
||||
{
|
||||
PositiveX,
|
||||
NegativeX,
|
||||
PositiveY,
|
||||
NegativeY,
|
||||
PositiveZ,
|
||||
NegativeZ,
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public readonly record struct CubeFacePosition
|
||||
{
|
||||
public CubeFacePosition(CubeFace face, double u, double v)
|
||||
{
|
||||
if (!double.IsFinite(u) || u is < -1.0 or > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(u), u, "Cube face U must be finite and in [-1, 1].");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(v) || v is < -1.0 or > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(v), v, "Cube face V must be finite and in [-1, 1].");
|
||||
}
|
||||
|
||||
Face = face;
|
||||
U = u;
|
||||
V = v;
|
||||
}
|
||||
|
||||
public CubeFace Face { get; }
|
||||
|
||||
public double U { get; }
|
||||
|
||||
public double V { get; }
|
||||
|
||||
public Vector3 ToCubeVector()
|
||||
{
|
||||
return Face switch
|
||||
{
|
||||
CubeFace.PositiveX => new Vector3(1.0f, (float)V, (float)-U),
|
||||
CubeFace.NegativeX => new Vector3(-1.0f, (float)V, (float)U),
|
||||
CubeFace.PositiveY => new Vector3((float)U, 1.0f, (float)-V),
|
||||
CubeFace.NegativeY => new Vector3((float)U, -1.0f, (float)V),
|
||||
CubeFace.PositiveZ => new Vector3((float)U, (float)V, 1.0f),
|
||||
CubeFace.NegativeZ => new Vector3((float)-U, (float)V, -1.0f),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Face), Face, "Unknown cube face."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public readonly record struct CubeTileKey
|
||||
{
|
||||
public const int MaxLevel = 30;
|
||||
|
||||
public CubeTileKey(CubeFace face, int level, int x, int y)
|
||||
{
|
||||
if (level is < 0 or > MaxLevel)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(level), level, $"Cube tile level must be in [0, {MaxLevel}].");
|
||||
}
|
||||
|
||||
var axisTileCount = 1 << level;
|
||||
if (x < 0 || x >= axisTileCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(x), x, $"Cube tile X must be in [0, {axisTileCount - 1}] for level {level}.");
|
||||
}
|
||||
|
||||
if (y < 0 || y >= axisTileCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(y), y, $"Cube tile Y must be in [0, {axisTileCount - 1}] for level {level}.");
|
||||
}
|
||||
|
||||
Face = face;
|
||||
Level = level;
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public CubeFace Face { get; }
|
||||
|
||||
public int Level { get; }
|
||||
|
||||
public int X { get; }
|
||||
|
||||
public int Y { get; }
|
||||
|
||||
public int AxisTileCount => 1 << Level;
|
||||
|
||||
public CubeTileKey Parent()
|
||||
{
|
||||
if (Level == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Root cube tile has no parent.");
|
||||
}
|
||||
|
||||
return new CubeTileKey(Face, Level - 1, X >> 1, Y >> 1);
|
||||
}
|
||||
|
||||
public CubeTileKey Child(int childX, int childY)
|
||||
{
|
||||
if (childX is < 0 or > 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(childX), childX, "Child X must be 0 or 1.");
|
||||
}
|
||||
|
||||
if (childY is < 0 or > 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(childY), childY, "Child Y must be 0 or 1.");
|
||||
}
|
||||
|
||||
if (Level == MaxLevel)
|
||||
{
|
||||
throw new InvalidOperationException($"Cube tile level {MaxLevel} cannot be subdivided.");
|
||||
}
|
||||
|
||||
return new CubeTileKey(Face, Level + 1, (X << 1) | childX, (Y << 1) | childY);
|
||||
}
|
||||
|
||||
public CubeFacePosition PositionAt(double localU, double localV)
|
||||
{
|
||||
if (!double.IsFinite(localU) || localU is < 0.0 or > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(localU), localU, "Local U must be finite and in [0, 1].");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(localV) || localV is < 0.0 or > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(localV), localV, "Local V must be finite and in [0, 1].");
|
||||
}
|
||||
|
||||
var axisTileCount = AxisTileCount;
|
||||
var faceU = -1.0 + (2.0 * (X + localU) / axisTileCount);
|
||||
var faceV = -1.0 + (2.0 * (Y + localV) / axisTileCount);
|
||||
return new CubeFacePosition(Face, faceU, faceV);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Face}/L{Level:D2}/{X}/{Y}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ public static class FractalDslCompiler
|
|||
break;
|
||||
case "tile":
|
||||
EnsureTokenCount(tokens, 6, lineIndex);
|
||||
var tile = new CubeTileKey(ParseFace(tokens[1], lineIndex), ParseInt(tokens[2], lineIndex), ParseInt(tokens[3], lineIndex), ParseInt(tokens[4], lineIndex));
|
||||
var tile = new PlanetaryTileAddress(ParseFace(tokens[1], lineIndex), ParseInt(tokens[2], lineIndex), ParseInt(tokens[3], lineIndex), ParseInt(tokens[4], lineIndex));
|
||||
var domainKey = FractalStableKeyBuilder.ForCubeTile(tile, tokens[5]);
|
||||
var parentKey = tokens.Length >= 7 ? new AquariumFractalKey(tokens[6]) : default;
|
||||
rootKey = FractalStableKeyBuilder.Child(domainKey, "root");
|
||||
|
|
@ -500,9 +502,9 @@ public static class FractalDslCompiler
|
|||
return new Vector4(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
private static CubeFace ParseFace(string value, int lineIndex)
|
||||
private static PlanetaryCubeFace ParseFace(string value, int lineIndex)
|
||||
{
|
||||
if (Enum.TryParse<CubeFace>(value, ignoreCase: false, out var face))
|
||||
if (Enum.TryParse<PlanetaryCubeFace>(value, ignoreCase: false, out var face))
|
||||
{
|
||||
return face;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalStableKeyBuilder
|
||||
{
|
||||
public static AquariumFractalKey ForCubeTile(CubeTileKey tile, string grammarPath)
|
||||
public static AquariumFractalKey ForCubeTile(PlanetaryTileAddress tile, string grammarPath)
|
||||
{
|
||||
return new AquariumFractalKey($"cube/{tile.Face}/L{tile.Level:D2}/{tile.X}/{tile.Y}:{NormalizePath(grammarPath)}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
|
@ -79,7 +81,7 @@ public static class FractalGpuProgramCompiler
|
|||
var tileAddress = domains.TryGetValue(claim.DomainKey, out var domainRow)
|
||||
&& domainRow.Kind == AquariumFractalDomainKind.CubeSphereTile
|
||||
? domainRow.Parameters0
|
||||
: new Vector4((float)CubeFace.PositiveZ, 0.0f, 0.0f, 0.0f);
|
||||
: new Vector4((float)PlanetaryCubeFace.PositiveZ, 0.0f, 0.0f, 0.0f);
|
||||
var rotationCos = MathF.Cos(claim.RotationRadians);
|
||||
var rotationSin = MathF.Sin(claim.RotationRadians);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
|
|
@ -6,5 +8,5 @@ public interface ICubeSphereProjection
|
|||
{
|
||||
string Name { get; }
|
||||
|
||||
Vector3 Project(CubeFacePosition position);
|
||||
Vector3 Project(PlanetaryFaceCoordinate position);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
|
|
@ -6,8 +8,8 @@ public sealed class NormalizeCubeSphereProjection : ICubeSphereProjection
|
|||
{
|
||||
public string Name => "normalize";
|
||||
|
||||
public Vector3 Project(CubeFacePosition position)
|
||||
public Vector3 Project(PlanetaryFaceCoordinate position)
|
||||
{
|
||||
return Vector3.Normalize(position.ToCubeVector());
|
||||
return (Vector3)PlanetaryTopology.NormalizedCubeDirection(position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
public readonly record struct ProjectionDistortionResult(
|
||||
string ProjectionName,
|
||||
CubeFace Face,
|
||||
PlanetaryCubeFace Face,
|
||||
int SamplesPerAxis,
|
||||
double AverageAreaScale,
|
||||
double MinRelativeArea,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
|
|
@ -9,7 +11,7 @@ public static class ProjectionDistortionSampler
|
|||
|
||||
public static ProjectionDistortionResult SampleFace(
|
||||
ICubeSphereProjection projection,
|
||||
CubeFace face,
|
||||
PlanetaryCubeFace face,
|
||||
int samplesPerAxis)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(projection);
|
||||
|
|
@ -56,14 +58,14 @@ public static class ProjectionDistortionSampler
|
|||
Math.Sqrt(totalSquaredRelativeError / sampleCount));
|
||||
}
|
||||
|
||||
private static double SurfaceAreaScale(ICubeSphereProjection projection, CubeFace face, double u, double v)
|
||||
private static double SurfaceAreaScale(ICubeSphereProjection projection, PlanetaryCubeFace face, double u, double v)
|
||||
{
|
||||
var du = Derivative(projection, face, u, v, DerivativeAxis.U);
|
||||
var dv = Derivative(projection, face, u, v, DerivativeAxis.V);
|
||||
return Vector3.Cross(du, dv).Length();
|
||||
}
|
||||
|
||||
private static Vector3 Derivative(ICubeSphereProjection projection, CubeFace face, double u, double v, DerivativeAxis axis)
|
||||
private static Vector3 Derivative(ICubeSphereProjection projection, PlanetaryCubeFace face, double u, double v, DerivativeAxis axis)
|
||||
{
|
||||
var low = axis == DerivativeAxis.U ? u : v;
|
||||
var high = low;
|
||||
|
|
@ -86,13 +88,13 @@ public static class ProjectionDistortionSampler
|
|||
Vector3 highPoint;
|
||||
if (axis == DerivativeAxis.U)
|
||||
{
|
||||
lowPoint = projection.Project(new CubeFacePosition(face, lowCoordinate, v));
|
||||
highPoint = projection.Project(new CubeFacePosition(face, highCoordinate, v));
|
||||
lowPoint = projection.Project(new PlanetaryFaceCoordinate(face, lowCoordinate, v));
|
||||
highPoint = projection.Project(new PlanetaryFaceCoordinate(face, highCoordinate, v));
|
||||
}
|
||||
else
|
||||
{
|
||||
lowPoint = projection.Project(new CubeFacePosition(face, u, lowCoordinate));
|
||||
highPoint = projection.Project(new CubeFacePosition(face, u, highCoordinate));
|
||||
lowPoint = projection.Project(new PlanetaryFaceCoordinate(face, u, lowCoordinate));
|
||||
highPoint = projection.Project(new PlanetaryFaceCoordinate(face, u, highCoordinate));
|
||||
}
|
||||
|
||||
return (highPoint - lowPoint) / (float)(forward + backward);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
public static class ProjectionReport
|
||||
|
|
@ -25,7 +28,7 @@ public static class ProjectionReport
|
|||
|
||||
foreach (var projection in projections)
|
||||
{
|
||||
foreach (var face in Enum.GetValues<CubeFace>())
|
||||
foreach (var face in Enum.GetValues<PlanetaryCubeFace>())
|
||||
{
|
||||
var result = ProjectionDistortionSampler.SampleFace(projection, face, samplesPerAxis);
|
||||
builder.Append("| ")
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
using System.Numerics;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Projection;
|
||||
|
||||
public sealed class TangentCubeSphereProjection : ICubeSphereProjection
|
||||
{
|
||||
private const double QuarterPi = Math.PI * 0.25;
|
||||
|
||||
public string Name => "tangent";
|
||||
|
||||
public Vector3 Project(CubeFacePosition position)
|
||||
public Vector3 Project(PlanetaryFaceCoordinate position)
|
||||
{
|
||||
var warped = new CubeFacePosition(position.Face, Math.Tan(position.U * QuarterPi), Math.Tan(position.V * QuarterPi));
|
||||
return Vector3.Normalize(warped.ToCubeVector());
|
||||
return (Vector3)PlanetaryTopology.Direction(position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\CultMath\src\CultMath\CultMath.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,11 @@ public static class AquariumHost
|
|||
var readyFrames = 0;
|
||||
var requiredReadyFrames = ParseHeadlessReadyFrames();
|
||||
var captureFramePath = ParseCaptureFramePath(args);
|
||||
var captureBokushoCanvasPath = ParseCaptureBokushoCanvasPath(args);
|
||||
var captureBokushoDensityPath = ParseCaptureBokushoDensityPath(args);
|
||||
var capturedFrame = false;
|
||||
var capturedBokushoCanvas = false;
|
||||
var capturedBokushoDensity = false;
|
||||
IAquariumRuntime? sceneReadyRuntime = null;
|
||||
|
||||
try
|
||||
|
|
@ -134,6 +138,7 @@ public static class AquariumHost
|
|||
activeRuntime.OnSceneReady();
|
||||
sceneReadyRuntime = activeRuntime;
|
||||
synthHost ??= new AquariumSynthHost(audioStemBus);
|
||||
synthHost.Update(AquariumSynthDocument.Combine(activeRuntime.Synth, renderer.DebugSynth), activeRuntime.Audio, 0.0f);
|
||||
lastFrame = frameClock.Elapsed;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +153,35 @@ public static class AquariumHost
|
|||
capturedFrame = true;
|
||||
}
|
||||
|
||||
if (runtime.Options.Headless && frames >= 2 && readyFrames >= requiredReadyFrames)
|
||||
if (runtime.Options.Headless
|
||||
&& !capturedBokushoCanvas
|
||||
&& !string.IsNullOrWhiteSpace(captureBokushoCanvasPath)
|
||||
&& readyFrames >= requiredReadyFrames)
|
||||
{
|
||||
renderer.SaveBokushoBrushCanvasRaw(captureBokushoCanvasPath);
|
||||
Console.WriteLine($"Headless Aquarium Bokusho canvas captured: {Path.GetFullPath(captureBokushoCanvasPath)}");
|
||||
capturedBokushoCanvas = true;
|
||||
}
|
||||
|
||||
if (runtime.Options.Headless
|
||||
&& !capturedBokushoDensity
|
||||
&& !string.IsNullOrWhiteSpace(captureBokushoDensityPath)
|
||||
&& readyFrames >= requiredReadyFrames)
|
||||
{
|
||||
renderer.SaveBokushoPageDensityRaw(captureBokushoDensityPath);
|
||||
Console.WriteLine($"Headless Aquarium Bokusho density captured: {Path.GetFullPath(captureBokushoDensityPath)}");
|
||||
capturedBokushoDensity = true;
|
||||
}
|
||||
|
||||
var needsFrameCapture = !string.IsNullOrWhiteSpace(captureFramePath) && !capturedFrame;
|
||||
var needsBokushoCanvasCapture = !string.IsNullOrWhiteSpace(captureBokushoCanvasPath) && !capturedBokushoCanvas;
|
||||
var needsBokushoDensityCapture = !string.IsNullOrWhiteSpace(captureBokushoDensityPath) && !capturedBokushoDensity;
|
||||
if (runtime.Options.Headless
|
||||
&& frames >= 2
|
||||
&& readyFrames >= requiredReadyFrames
|
||||
&& !needsFrameCapture
|
||||
&& !needsBokushoCanvasCapture
|
||||
&& !needsBokushoDensityCapture)
|
||||
{
|
||||
Console.WriteLine("Headless Aquarium completed requested frames.");
|
||||
break;
|
||||
|
|
@ -214,6 +247,34 @@ public static class AquariumHost
|
|||
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_FRAME");
|
||||
}
|
||||
|
||||
private static string? ParseCaptureBokushoCanvasPath(IReadOnlyCollection<string> args)
|
||||
{
|
||||
var values = args.ToArray();
|
||||
for (var index = 0; index < values.Length - 1; index++)
|
||||
{
|
||||
if (string.Equals(values[index], "--capture-bokusho-canvas", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return values[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_BOKUSHO_CANVAS");
|
||||
}
|
||||
|
||||
private static string? ParseCaptureBokushoDensityPath(IReadOnlyCollection<string> args)
|
||||
{
|
||||
var values = args.ToArray();
|
||||
for (var index = 0; index < values.Length - 1; index++)
|
||||
{
|
||||
if (string.Equals(values[index], "--capture-bokusho-density", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return values[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_BOKUSHO_DENSITY");
|
||||
}
|
||||
|
||||
private static string? ParseCachePath(IReadOnlyCollection<string> args)
|
||||
{
|
||||
var values = args.ToArray();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ internal sealed class AquariumSynthHost : IDisposable
|
|||
private readonly IAquariumAudioStemBus audioStemBus;
|
||||
private readonly WasapiAudioDevice audioDevice = new();
|
||||
private readonly AquaSynthPatchCompiler patchCompiler = new(new AquaSynthNativeOptions(DspSourceDirectory: Path.Combine(AppContext.BaseDirectory, "Synth")));
|
||||
private WasapiLoopbackCaptureDevice? loopbackCapture;
|
||||
private AquariumAudioCaptureRequest? activeCaptureRequest;
|
||||
private float timeSeconds;
|
||||
|
||||
public AquariumSynthHost(IAquariumAudioStemBus? audioStemBus = null)
|
||||
|
|
@ -81,6 +83,11 @@ internal sealed class AquariumSynthHost : IDisposable
|
|||
PlayMonitorOutputs(block, stemFrame);
|
||||
}
|
||||
|
||||
foreach (var request in audio.DrainCaptureRequests())
|
||||
{
|
||||
ApplyCaptureRequest(request);
|
||||
}
|
||||
|
||||
if (!synth.Enabled)
|
||||
{
|
||||
return;
|
||||
|
|
@ -113,6 +120,42 @@ internal sealed class AquariumSynthHost : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ApplyCaptureRequest(AquariumAudioCaptureRequest request)
|
||||
{
|
||||
if (request.Kind != AquariumAudioCaptureKind.SystemLoopback)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request.Enabled)
|
||||
{
|
||||
if (activeCaptureRequest?.ProfileId == request.ProfileId)
|
||||
{
|
||||
loopbackCapture?.Dispose();
|
||||
loopbackCapture = null;
|
||||
activeCaptureRequest = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeCaptureRequest is not null &&
|
||||
activeCaptureRequest.ProfileId == request.ProfileId &&
|
||||
activeCaptureRequest.SourceId == request.SourceId &&
|
||||
activeCaptureRequest.DisplayName == request.DisplayName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loopbackCapture?.Dispose();
|
||||
loopbackCapture = new WasapiLoopbackCaptureDevice(
|
||||
audioStemBus,
|
||||
request.ProfileId,
|
||||
request.SourceId,
|
||||
request.DisplayName);
|
||||
activeCaptureRequest = request;
|
||||
}
|
||||
|
||||
private void PlayMonitorOutputs(AquariumStreamingAudioBlock block, AquariumAudioStemFrame stemFrame)
|
||||
{
|
||||
if (block.MonitorLeftChannel < 0 && block.MonitorRightChannel < 0)
|
||||
|
|
@ -341,6 +384,7 @@ internal sealed class AquariumSynthHost : IDisposable
|
|||
}
|
||||
|
||||
streamingDsp.Dispose();
|
||||
loopbackCapture?.Dispose();
|
||||
audioDevice.Dispose();
|
||||
patchCompiler.Dispose();
|
||||
}
|
||||
|
|
|
|||
331
src/Aquarium.Engine/Audio/WasapiLoopbackCaptureDevice.cs
Normal file
331
src/Aquarium.Engine/Audio/WasapiLoopbackCaptureDevice.cs
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Aquarium.Engine.Audio;
|
||||
|
||||
internal sealed class WasapiLoopbackCaptureDevice : IDisposable
|
||||
{
|
||||
private static readonly bool TraceAudio = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AQUARIUM_AUDIO_TRACE"));
|
||||
|
||||
private readonly IAquariumAudioStemBus stemBus;
|
||||
private readonly string profileId;
|
||||
private readonly string sourceId;
|
||||
private readonly string displayName;
|
||||
private readonly Thread thread;
|
||||
private volatile bool running = true;
|
||||
private volatile bool started;
|
||||
private long sequence;
|
||||
|
||||
public WasapiLoopbackCaptureDevice(
|
||||
IAquariumAudioStemBus stemBus,
|
||||
string profileId,
|
||||
string sourceId,
|
||||
string displayName)
|
||||
{
|
||||
this.stemBus = stemBus;
|
||||
this.profileId = profileId;
|
||||
this.sourceId = sourceId;
|
||||
this.displayName = displayName;
|
||||
thread = new Thread(CaptureThread)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Aquarium WASAPI Loopback"
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
running = false;
|
||||
if (started)
|
||||
{
|
||||
thread.Join(TimeSpan.FromSeconds(2.0));
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureThread()
|
||||
{
|
||||
started = true;
|
||||
var comInitialized = false;
|
||||
IAudioClient? audioClient = null;
|
||||
try
|
||||
{
|
||||
var hr = CoInitializeEx(IntPtr.Zero, CoInit.Multithreaded);
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
comInitialized = true;
|
||||
#pragma warning disable CA1416
|
||||
var enumeratorType = Type.GetTypeFromCLSID(MMDeviceEnumeratorClsid, throwOnError: true)
|
||||
?? throw new InvalidOperationException("MMDeviceEnumerator COM type is not available.");
|
||||
#pragma warning restore CA1416
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)(Activator.CreateInstance(enumeratorType)
|
||||
?? throw new InvalidOperationException("failed to create MMDeviceEnumerator"));
|
||||
Marshal.ThrowExceptionForHR(deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.Render, ERole.Console, out var device));
|
||||
var audioClientId = typeof(IAudioClient).GUID;
|
||||
Marshal.ThrowExceptionForHR(device.Activate(ref audioClientId, ClsCtx.All, IntPtr.Zero, out var audioClientObject));
|
||||
audioClient = (IAudioClient)audioClientObject;
|
||||
Marshal.ThrowExceptionForHR(audioClient.GetMixFormat(out var mixFormatPointer));
|
||||
try
|
||||
{
|
||||
var format = WasapiFormat.FromPointer(mixFormatPointer);
|
||||
Marshal.ThrowExceptionForHR(audioClient.Initialize(
|
||||
AudioClientShareMode.Shared,
|
||||
AudioClientStreamFlags.Loopback | AudioClientStreamFlags.NoPersist,
|
||||
0,
|
||||
0,
|
||||
mixFormatPointer,
|
||||
IntPtr.Zero));
|
||||
var captureClientId = typeof(IAudioCaptureClient).GUID;
|
||||
Marshal.ThrowExceptionForHR(audioClient.GetService(ref captureClientId, out var captureClientObject));
|
||||
var captureClient = (IAudioCaptureClient)captureClientObject;
|
||||
Marshal.ThrowExceptionForHR(audioClient.Start());
|
||||
Console.WriteLine($"Aquarium WASAPI loopback started: profile={profileId} {format.SampleRate} Hz, {format.Channels} channels, {format.BitsPerSample}-bit {format.SampleKind}.");
|
||||
|
||||
while (running)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(captureClient.GetNextPacketSize(out var packetFrames));
|
||||
while (packetFrames > 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(captureClient.GetBuffer(
|
||||
out var buffer,
|
||||
out var frames,
|
||||
out var flags,
|
||||
out _,
|
||||
out _));
|
||||
try
|
||||
{
|
||||
PublishPacket(buffer, frames, flags, format);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(captureClient.ReleaseBuffer(frames));
|
||||
}
|
||||
|
||||
Marshal.ThrowExceptionForHR(captureClient.GetNextPacketSize(out packetFrames));
|
||||
}
|
||||
|
||||
Thread.Sleep(4);
|
||||
}
|
||||
|
||||
audioClient.Stop();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(mixFormatPointer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Aquarium WASAPI loopback unavailable: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
audioClient?.Stop();
|
||||
if (comInitialized)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishPacket(IntPtr buffer, int frameCount, uint flags, WasapiFormat format)
|
||||
{
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mono = (flags & AudioCaptureBufferFlagsSilent) != 0
|
||||
? new float[frameCount]
|
||||
: format.SampleKind == WasapiSampleKind.Float32
|
||||
? ReadFloat32(buffer, frameCount, format.Channels)
|
||||
: ReadPcm16(buffer, frameCount, format.Channels);
|
||||
stemBus.Publish(new AquariumAudioStemFrame(
|
||||
profileId,
|
||||
[new AquariumAudioStemChannel(0, "system_mix", displayName, sourceId, mono)],
|
||||
frameCount,
|
||||
format.SampleRate,
|
||||
Interlocked.Increment(ref sequence)));
|
||||
|
||||
if (TraceAudio)
|
||||
{
|
||||
Console.WriteLine($"Aquarium loopback packet: profile={profileId} frames={frameCount} seq={sequence}");
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe float[] ReadFloat32(IntPtr buffer, int frameCount, int channels)
|
||||
{
|
||||
var output = new float[frameCount];
|
||||
var input = (float*)buffer;
|
||||
for (var frame = 0; frame < frameCount; frame++)
|
||||
{
|
||||
var sum = 0.0f;
|
||||
for (var channel = 0; channel < channels; channel++)
|
||||
{
|
||||
sum += *input++;
|
||||
}
|
||||
|
||||
output[frame] = Math.Clamp(sum / Math.Max(1, channels), -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static unsafe float[] ReadPcm16(IntPtr buffer, int frameCount, int channels)
|
||||
{
|
||||
var output = new float[frameCount];
|
||||
var input = (short*)buffer;
|
||||
for (var frame = 0; frame < frameCount; frame++)
|
||||
{
|
||||
var sum = 0.0f;
|
||||
for (var channel = 0; channel < channels; channel++)
|
||||
{
|
||||
sum += *input++ / 32768.0f;
|
||||
}
|
||||
|
||||
output[frame] = Math.Clamp(sum / Math.Max(1, channels), -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private readonly record struct WasapiFormat(int Channels, int SampleRate, int BitsPerSample, int BlockAlign, WasapiSampleKind SampleKind)
|
||||
{
|
||||
public static WasapiFormat FromPointer(IntPtr pointer)
|
||||
{
|
||||
var tag = Marshal.ReadInt16(pointer, 0);
|
||||
var channels = Marshal.ReadInt16(pointer, 2);
|
||||
var sampleRate = Marshal.ReadInt32(pointer, 4);
|
||||
var blockAlign = Marshal.ReadInt16(pointer, 12);
|
||||
var bitsPerSample = Marshal.ReadInt16(pointer, 14);
|
||||
var kind = tag switch
|
||||
{
|
||||
WaveFormatIeeeFloat => WasapiSampleKind.Float32,
|
||||
WaveFormatPcm => WasapiSampleKind.Pcm16,
|
||||
WaveFormatExtensible when bitsPerSample == 32 => WasapiSampleKind.Float32,
|
||||
_ => WasapiSampleKind.Pcm16
|
||||
};
|
||||
return new WasapiFormat(channels, sampleRate, bitsPerSample, blockAlign, kind);
|
||||
}
|
||||
}
|
||||
|
||||
private enum WasapiSampleKind
|
||||
{
|
||||
Float32,
|
||||
Pcm16
|
||||
}
|
||||
|
||||
private const uint AudioCaptureBufferFlagsSilent = 0x2;
|
||||
private const short WaveFormatPcm = 1;
|
||||
private const short WaveFormatIeeeFloat = 3;
|
||||
private const short WaveFormatExtensible = -2;
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CoInitializeEx(IntPtr pvReserved, CoInit dwCoInit);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern void CoUninitialize();
|
||||
|
||||
[Flags]
|
||||
private enum CoInit : uint
|
||||
{
|
||||
Multithreaded = 0x0
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum ClsCtx : uint
|
||||
{
|
||||
All = 23
|
||||
}
|
||||
|
||||
private enum EDataFlow
|
||||
{
|
||||
Render,
|
||||
Capture,
|
||||
All
|
||||
}
|
||||
|
||||
private enum ERole
|
||||
{
|
||||
Console,
|
||||
Multimedia,
|
||||
Communications
|
||||
}
|
||||
|
||||
private enum AudioClientShareMode
|
||||
{
|
||||
Shared,
|
||||
Exclusive
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum AudioClientStreamFlags : uint
|
||||
{
|
||||
Loopback = 0x00020000,
|
||||
NoPersist = 0x00080000
|
||||
}
|
||||
|
||||
private static readonly Guid MMDeviceEnumeratorClsid = new("BCDE0395-E52F-467C-8E3D-C4579291692E");
|
||||
|
||||
[ComImport]
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMMDeviceEnumerator
|
||||
{
|
||||
int EnumAudioEndpoints(EDataFlow dataFlow, uint stateMask, out IntPtr devices);
|
||||
|
||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMMDevice
|
||||
{
|
||||
int Activate(ref Guid iid, ClsCtx clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfaceObject);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IAudioClient
|
||||
{
|
||||
int Initialize(AudioClientShareMode shareMode, AudioClientStreamFlags streamFlags, long bufferDuration, long periodicity, IntPtr format, IntPtr audioSessionGuid);
|
||||
|
||||
int GetBufferSize(out int bufferFrameCount);
|
||||
|
||||
int GetStreamLatency(out long latency);
|
||||
|
||||
int GetCurrentPadding(out int paddingFrameCount);
|
||||
|
||||
int IsFormatSupported(AudioClientShareMode shareMode, IntPtr format, out IntPtr closestMatch);
|
||||
|
||||
int GetMixFormat(out IntPtr deviceFormat);
|
||||
|
||||
int GetDevicePeriod(out long defaultDevicePeriod, out long minimumDevicePeriod);
|
||||
|
||||
int Start();
|
||||
|
||||
int Stop();
|
||||
|
||||
int Reset();
|
||||
|
||||
int SetEventHandle(IntPtr eventHandle);
|
||||
|
||||
int GetService(ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object serviceInterface);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IAudioCaptureClient
|
||||
{
|
||||
int GetBuffer(out IntPtr dataBuffer, out int numFramesToRead, out uint bufferFlags, out long devicePosition, out long qpcPosition);
|
||||
|
||||
int ReleaseBuffer(int numFramesRead);
|
||||
|
||||
int GetNextPacketSize(out int numFramesInNextPacket);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,10 +69,15 @@ public sealed class Win32Window : IDisposable
|
|||
private const int VK_DELETE = 0x2E;
|
||||
private const int VK_RETURN = 0x0D;
|
||||
private const int VK_LEFT = 0x25;
|
||||
private const int VK_UP = 0x26;
|
||||
private const int VK_RIGHT = 0x27;
|
||||
private const int VK_DOWN = 0x28;
|
||||
private const int VK_HOME = 0x24;
|
||||
private const int VK_END = 0x23;
|
||||
private const int VK_PRIOR = 0x21;
|
||||
private const int VK_NEXT = 0x22;
|
||||
private const int VK_SHIFT = 0x10;
|
||||
private const int VK_CONTROL = 0x11;
|
||||
private const int WHEEL_DELTA = 120;
|
||||
|
||||
private readonly WndProc windowProcedure;
|
||||
|
|
@ -824,18 +829,33 @@ public sealed class Win32Window : IDisposable
|
|||
case VK_LEFT:
|
||||
input.SetKey(KeyCode.LeftArrow, isDown);
|
||||
break;
|
||||
case VK_UP:
|
||||
input.SetKey(KeyCode.UpArrow, isDown);
|
||||
break;
|
||||
case VK_RIGHT:
|
||||
input.SetKey(KeyCode.RightArrow, isDown);
|
||||
break;
|
||||
case VK_DOWN:
|
||||
input.SetKey(KeyCode.DownArrow, isDown);
|
||||
break;
|
||||
case VK_HOME:
|
||||
input.SetKey(KeyCode.Home, isDown);
|
||||
break;
|
||||
case VK_END:
|
||||
input.SetKey(KeyCode.End, isDown);
|
||||
break;
|
||||
case VK_PRIOR:
|
||||
input.SetKey(KeyCode.PageUp, isDown);
|
||||
break;
|
||||
case VK_NEXT:
|
||||
input.SetKey(KeyCode.PageDown, isDown);
|
||||
break;
|
||||
case VK_SHIFT:
|
||||
input.SetKey(KeyCode.Shift, isDown);
|
||||
break;
|
||||
case VK_CONTROL:
|
||||
input.SetKey(KeyCode.Control, isDown);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
64
src/Aquarium.Engine/Render/D3D12ComputeProbe.cs
Normal file
64
src/Aquarium.Engine/Render/D3D12ComputeProbe.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Vortice.Direct3D;
|
||||
using Vortice.Direct3D12;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
||||
internal static class D3D12ComputeProbe
|
||||
{
|
||||
public static unsafe TOutput[] Run<TInput, TOutput>(string shaderPath, string entryPoint, ReadOnlySpan<TInput> inputs)
|
||||
where TInput : unmanaged where TOutput : unmanaged
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) throw new PlatformNotSupportedException("D3D12 probes require Windows.");
|
||||
var inputBytes = checked(inputs.Length * Unsafe.SizeOf<TInput>());
|
||||
var outputBytes = checked(inputs.Length * Unsafe.SizeOf<TOutput>());
|
||||
using var device = D3D12.D3D12CreateDevice<ID3D12Device>(IntPtr.Zero, FeatureLevel.Level_11_0);
|
||||
using var queue = device.CreateCommandQueue(new CommandQueueDescription(CommandListType.Direct));
|
||||
using var allocator = device.CreateCommandAllocator(CommandListType.Direct);
|
||||
using var list = device.CreateCommandList<ID3D12GraphicsCommandList>(0, CommandListType.Direct, allocator, null);
|
||||
using var fence = device.CreateFence(0);
|
||||
using var fenceEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
using var input = device.CreateCommittedResource(HeapType.Upload, ResourceDescription.Buffer((ulong)inputBytes), ResourceStates.GenericRead);
|
||||
using var output = device.CreateCommittedResource(HeapType.Default, ResourceDescription.Buffer((ulong)outputBytes, ResourceFlags.AllowUnorderedAccess), ResourceStates.UnorderedAccess);
|
||||
using var readback = device.CreateCommittedResource(HeapType.Readback, ResourceDescription.Buffer((ulong)outputBytes), ResourceStates.CopyDest);
|
||||
|
||||
var mappedInput = input.Map<byte>(0);
|
||||
MemoryMarshal.AsBytes(inputs).CopyTo(new Span<byte>(mappedInput, inputBytes));
|
||||
input.Unmap(0);
|
||||
|
||||
var parameters = new[]
|
||||
{
|
||||
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(0, 0), ShaderVisibility.All),
|
||||
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(0, 0), ShaderVisibility.All),
|
||||
new RootParameter(new RootConstants(0, 0, 3), ShaderVisibility.All),
|
||||
};
|
||||
var signatureDescription = new RootSignatureDescription(RootSignatureFlags.None, parameters, []);
|
||||
using var signature = device.CreateRootSignature(0, in signatureDescription, RootSignatureVersion.Version1);
|
||||
using var pipeline = device.CreateComputePipelineState(new ComputePipelineStateDescription
|
||||
{
|
||||
RootSignature = signature,
|
||||
ComputeShader = D3D12ShaderCompiler.Compile(shaderPath, entryPoint, "cs_5_0"),
|
||||
});
|
||||
list.SetComputeRootSignature(signature);
|
||||
list.SetPipelineState(pipeline);
|
||||
list.SetComputeRootShaderResourceView(0, input.GPUVirtualAddress);
|
||||
list.SetComputeRootUnorderedAccessView(1, output.GPUVirtualAddress);
|
||||
list.SetComputeRoot32BitConstant(2, (uint)inputs.Length, 0);
|
||||
list.Dispatch((uint)((inputs.Length + 63) / 64), 1, 1);
|
||||
list.ResourceBarrier(ResourceBarrier.BarrierTransition(output, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
|
||||
list.CopyBufferRegion(readback, 0, output, 0, (ulong)outputBytes);
|
||||
list.Close();
|
||||
queue.ExecuteCommandList(list);
|
||||
queue.Signal(fence, 1).CheckError();
|
||||
fence.SetEventOnCompletion(1, fenceEvent.SafeWaitHandle.DangerousGetHandle()).CheckError();
|
||||
fenceEvent.WaitOne();
|
||||
|
||||
var results = new TOutput[inputs.Length];
|
||||
var mappedOutput = readback.Map<byte>(0);
|
||||
new ReadOnlySpan<byte>(mappedOutput, outputBytes).CopyTo(MemoryMarshal.AsBytes(results.AsSpan()));
|
||||
readback.Unmap(0);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed class D3D12ExternalSensorTexture : IDisposable
|
|||
Resource,
|
||||
new ShaderResourceViewDescription
|
||||
{
|
||||
Format = Format,
|
||||
Format = ToShaderResourceViewFormat(Format),
|
||||
ViewDimension = ShaderResourceViewDimension.Texture2D,
|
||||
Shader4ComponentMapping = ShaderComponentMapping.Default,
|
||||
Texture2D = new Texture2DShaderResourceView { MipLevels = 1 },
|
||||
|
|
@ -131,10 +131,18 @@ internal sealed class D3D12ExternalSensorTexture : IDisposable
|
|||
AquariumGpuSensorPixelFormat.R16Float => Format.R16_Float,
|
||||
AquariumGpuSensorPixelFormat.Rg8Unorm => Format.R8G8_UNorm,
|
||||
AquariumGpuSensorPixelFormat.LeapPackedMap => Format.R8G8B8A8_UNorm,
|
||||
AquariumGpuSensorPixelFormat.Yuy2 => Format.YUY2,
|
||||
_ => Format.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
private static Format ToShaderResourceViewFormat(Format format)
|
||||
{
|
||||
return format == Format.YUY2
|
||||
? Format.R8G8B8A8_UNorm
|
||||
: format;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Resource.Dispose();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ internal sealed class D3D12FieldMesh : IDisposable
|
|||
AquariumFieldMeshIndexFormat indexFormat,
|
||||
AquariumFieldMeshLayout layout,
|
||||
int submeshCount,
|
||||
ulong version)
|
||||
ulong version,
|
||||
string sourceUri)
|
||||
{
|
||||
Vertices = vertices;
|
||||
Indices = indices;
|
||||
|
|
@ -22,6 +23,7 @@ internal sealed class D3D12FieldMesh : IDisposable
|
|||
Layout = layout;
|
||||
SubmeshCount = submeshCount;
|
||||
Version = version;
|
||||
SourceUri = sourceUri;
|
||||
}
|
||||
|
||||
public D3D12StructuredBuffer Vertices { get; }
|
||||
|
|
@ -44,6 +46,11 @@ internal sealed class D3D12FieldMesh : IDisposable
|
|||
|
||||
public ulong Version { get; }
|
||||
|
||||
public string SourceUri { get; }
|
||||
|
||||
public bool IsGeneratedFromResource =>
|
||||
SourceUri.StartsWith("derived-from:", StringComparison.Ordinal);
|
||||
|
||||
public PrimitiveTopology D3DTopology => Topology switch
|
||||
{
|
||||
AquariumFieldMeshTopology.TriangleStrip => PrimitiveTopology.TriangleStrip,
|
||||
|
|
|
|||
|
|
@ -668,7 +668,8 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
|
|||
existing.Indices.StrideBytes == mesh.Indices.StrideBytes &&
|
||||
existing.Topology == mesh.Topology &&
|
||||
existing.IndexFormat == mesh.IndexFormat &&
|
||||
existing.Layout == mesh.Layout)
|
||||
existing.Layout == mesh.Layout &&
|
||||
string.Equals(existing.SourceUri, declaration.SourceUri, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -683,12 +684,14 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
|
|||
device,
|
||||
mesh.Vertices.Count,
|
||||
mesh.Vertices.StrideBytes,
|
||||
$"Aquarium D3D12 Field Mesh Vertices {declaration.ResourceKey}");
|
||||
$"Aquarium D3D12 Field Mesh Vertices {declaration.ResourceKey}",
|
||||
allowUnorderedAccess: IsGeneratedMeshSource(declaration.SourceUri));
|
||||
var indices = new D3D12StructuredBuffer(
|
||||
device,
|
||||
mesh.Indices.Count,
|
||||
mesh.Indices.StrideBytes,
|
||||
$"Aquarium D3D12 Field Mesh Indices {declaration.ResourceKey}");
|
||||
$"Aquarium D3D12 Field Mesh Indices {declaration.ResourceKey}",
|
||||
allowUnorderedAccess: IsGeneratedMeshSource(declaration.SourceUri));
|
||||
meshes[declaration.ResourceKey] = new D3D12FieldMesh(
|
||||
vertices,
|
||||
indices,
|
||||
|
|
@ -696,10 +699,14 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
|
|||
mesh.IndexFormat,
|
||||
mesh.Layout,
|
||||
mesh.SubmeshCount,
|
||||
declaration.Version);
|
||||
declaration.Version,
|
||||
declaration.SourceUri);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsGeneratedMeshSource(string sourceUri) =>
|
||||
sourceUri.StartsWith("derived-from:", StringComparison.Ordinal);
|
||||
|
||||
private int RemoveStaleStructuredBuffers(D3D12ResourceRegistry resourceRegistry, HashSet<string> currentKeys)
|
||||
{
|
||||
var removed = 0;
|
||||
|
|
|
|||
|
|
@ -165,13 +165,34 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public static D3D12FieldTexture2D WrapOwned(
|
||||
ID3D12Resource resource,
|
||||
int width,
|
||||
int height,
|
||||
Format format,
|
||||
bool allowsUnorderedAccess,
|
||||
ResourceStates state,
|
||||
string name)
|
||||
{
|
||||
var texture = new D3D12FieldTexture2D(
|
||||
resource,
|
||||
uploadResource: null,
|
||||
Math.Max(1, width),
|
||||
Math.Max(1, height),
|
||||
format,
|
||||
allowsUnorderedAccess,
|
||||
name);
|
||||
texture.State = state;
|
||||
return texture;
|
||||
}
|
||||
|
||||
public void CreateShaderResourceView(ID3D12Device device, D3D12DescriptorSlot descriptor)
|
||||
{
|
||||
device.CreateShaderResourceView(
|
||||
Resource,
|
||||
new ShaderResourceViewDescription
|
||||
{
|
||||
Format = Format,
|
||||
Format = ToShaderResourceViewFormat(Format),
|
||||
ViewDimension = ShaderResourceViewDimension.Texture2D,
|
||||
Shader4ComponentMapping = ShaderComponentMapping.Default,
|
||||
Texture2D = new Texture2DShaderResourceView { MipLevels = 1 },
|
||||
|
|
@ -216,6 +237,13 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable
|
|||
Resource.Dispose();
|
||||
}
|
||||
|
||||
private static Format ToShaderResourceViewFormat(Format format)
|
||||
{
|
||||
return format == Format.YUY2
|
||||
? Format.R8G8B8A8_UNorm
|
||||
: format;
|
||||
}
|
||||
|
||||
private static D3D12FieldTexture2D CreateRgba8(
|
||||
ID3D12Device device,
|
||||
ID3D12GraphicsCommandList commandList,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ internal static class D3D12FieldTextureFormat
|
|||
{
|
||||
format = formatName switch
|
||||
{
|
||||
"Gray8" or "R8" or "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm,
|
||||
"Bayer8" or "Gray8" or "R8" or "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm,
|
||||
"R16Float" or "R16_Float" or "R16_FLOAT" => Format.R16_Float,
|
||||
"R32Float" or "R32_Float" or "R32_FLOAT" or "Float32" => Format.R32_Float,
|
||||
"Rg8" or "Rg8Unorm" or "R8G8_UNorm" or "R8G8_UNORM" or "LeapStereoIr" or "LeapPackedMap" => Format.R8G8_UNorm,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
105
src/Aquarium.Engine/Render/D3D12ShaderCompiler.cs
Normal file
105
src/Aquarium.Engine/Render/D3D12ShaderCompiler.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using Vortice.D3DCompiler;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
||||
internal static class D3D12ShaderCompiler
|
||||
{
|
||||
public static ReadOnlyMemory<byte> Compile(string path, string entryPoint, string profile, bool skipOptimizationInDebug = true)
|
||||
{
|
||||
var flags = ShaderFlags.EnableStrictness;
|
||||
#if DEBUG
|
||||
flags |= ShaderFlags.Debug;
|
||||
if (skipOptimizationInDebug) flags |= ShaderFlags.SkipOptimization;
|
||||
#endif
|
||||
var source = ExpandIncludes(path, []);
|
||||
return Compiler.Compile(source, entryPoint, path, profile, flags, EffectFlags.None);
|
||||
}
|
||||
|
||||
internal static string ExpandIncludes(string path, HashSet<string> stack)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
if (!stack.Add(fullPath)) throw new InvalidOperationException($"Circular shader include detected at {fullPath}");
|
||||
var lines = File.ReadAllLines(fullPath);
|
||||
var expanded = new List<string>(lines.Length);
|
||||
var directory = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory;
|
||||
for (var index = 0; index < lines.Length; index++)
|
||||
{
|
||||
var trimmed = lines[index].TrimStart();
|
||||
if (trimmed.StartsWith("#include \"", StringComparison.Ordinal))
|
||||
{
|
||||
var first = trimmed.IndexOf('"');
|
||||
var second = trimmed.IndexOf('"', first + 1);
|
||||
if (second > first)
|
||||
{
|
||||
var include = ResolveInclude(directory, trimmed.Substring(first + 1, second - first - 1));
|
||||
expanded.Add($"#line 1 \"{include.Replace("\\", "\\\\")}\"");
|
||||
expanded.Add(ExpandIncludes(include, stack));
|
||||
expanded.Add($"#line {index + 2} \"{fullPath.Replace("\\", "\\\\")}\"");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
expanded.Add(lines[index]);
|
||||
}
|
||||
stack.Remove(fullPath);
|
||||
return string.Join(Environment.NewLine, expanded);
|
||||
}
|
||||
|
||||
private static string ResolveInclude(string directory, string includeName)
|
||||
{
|
||||
var local = Path.GetFullPath(Path.Combine(directory, includeName));
|
||||
if (File.Exists(local)) return local;
|
||||
var normalized = includeName.Replace('\\', '/');
|
||||
if (normalized.StartsWith("CultMath/", StringComparison.Ordinal))
|
||||
{
|
||||
var relative = normalized["CultMath/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
||||
foreach (var start in new[] { directory, AppContext.BaseDirectory })
|
||||
{
|
||||
var current = Path.GetFullPath(start);
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
var candidate = Path.Combine(current, "CultMath", "shaders", relative);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
var parent = Path.GetDirectoryName(current);
|
||||
if (string.IsNullOrEmpty(parent) || parent == current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
const string cultMathUnityPrefix = "Packages/org.gamecult.cultmath/shaders/";
|
||||
if (normalized.StartsWith(cultMathUnityPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
var relative = normalized[cultMathUnityPrefix.Length..].Replace('/', Path.DirectorySeparatorChar);
|
||||
foreach (var start in new[] { directory, AppContext.BaseDirectory })
|
||||
{
|
||||
var current = Path.GetFullPath(start);
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
var localPackage = Path.Combine(current, "CultMath", relative);
|
||||
if (File.Exists(localPackage)) return localPackage;
|
||||
var siblingRepository = Path.Combine(current, "CultMath", "shaders", relative);
|
||||
if (File.Exists(siblingRepository)) return siblingRepository;
|
||||
var parent = Path.GetDirectoryName(current);
|
||||
if (string.IsNullOrEmpty(parent) || parent == current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (normalized.StartsWith("GameCult.Geometry/", StringComparison.Ordinal))
|
||||
{
|
||||
var relative = normalized["GameCult.Geometry/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
||||
foreach (var start in new[] { directory, AppContext.BaseDirectory })
|
||||
{
|
||||
var current = Path.GetFullPath(start);
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
var candidate = Path.Combine(current, "CultLib", "src", "GameCult.Geometry", "Shaders", relative);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
var parent = Path.GetDirectoryName(current);
|
||||
if (string.IsNullOrEmpty(parent) || parent == current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return local;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Numerics;
|
||||
using Vortice.DCommon;
|
||||
using Vortice.Direct2D1;
|
||||
using Vortice.DirectWrite;
|
||||
using Vortice.DXGI;
|
||||
using Vortice.Mathematics;
|
||||
using Aquarium.Engine.Render.Ui;
|
||||
using Aquarium.Engine.Ui;
|
||||
using D2DFactoryType = Vortice.Direct2D1.FactoryType;
|
||||
using DWriteFactoryType = Vortice.DirectWrite.FactoryType;
|
||||
|
||||
|
|
@ -16,6 +18,7 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
private readonly IDWriteFontCollection fontCollection;
|
||||
private readonly IDWriteTypography smallCapsTypography;
|
||||
private readonly ID2D1RenderTarget renderTarget;
|
||||
private readonly ID2D1Bitmap outputSnapshot;
|
||||
private readonly ID2D1SolidColorBrush primaryTextBrush;
|
||||
private readonly ID2D1SolidColorBrush quietTextBrush;
|
||||
private readonly ID2D1SolidColorBrush panelBrush;
|
||||
|
|
@ -65,6 +68,14 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
renderTarget = direct2DFactory.CreateDxgiSurfaceRenderTarget(backBufferSurface, renderTargetProperties);
|
||||
renderTarget.AntialiasMode = AntialiasMode.PerPrimitive;
|
||||
renderTarget.TextAntialiasMode = Vortice.Direct2D1.TextAntialiasMode.Grayscale;
|
||||
outputSnapshot = renderTarget.CreateBitmap(
|
||||
new SizeI(width, height),
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
new BitmapProperties(
|
||||
new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Ignore),
|
||||
96.0f,
|
||||
96.0f));
|
||||
|
||||
primaryTextBrush = renderTarget.CreateSolidColorBrush(new Color4(0.88f, 0.96f, 1.0f, 0.92f));
|
||||
quietTextBrush = renderTarget.CreateSolidColorBrush(new Color4(0.54f, 0.68f, 0.72f, 0.72f));
|
||||
|
|
@ -84,8 +95,10 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
monospaceFormat = CreateTextFormat("Ubuntu Sans Mono", 11.0f, FontWeight.Regular, ParagraphAlignment.Near);
|
||||
}
|
||||
|
||||
public void Render(AquariumFrame frame, int renderDebugMode, DebugUi? debugUi, IReadOnlyList<DebugUi> clientUiPanels, string performanceText)
|
||||
public void Render(AquariumFrame frame, int renderDebugMode, DebugUi? debugUi, IReadOnlyList<DebugUi> clientUiPanels, IReadOnlyList<AquariumUiSurface> clientUiSurfaces, string performanceText)
|
||||
{
|
||||
CaptureOutputSnapshot();
|
||||
|
||||
renderTarget.BeginDraw();
|
||||
DrawPerformanceCounter(performanceText);
|
||||
if (debugUi is not null)
|
||||
|
|
@ -98,6 +111,11 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
DrawPanel(panel, panel.DrawOpacity());
|
||||
}
|
||||
|
||||
foreach (var surface in clientUiSurfaces)
|
||||
{
|
||||
DrawSurface(surface);
|
||||
}
|
||||
|
||||
renderTarget.EndDraw();
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +192,387 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawSurface(AquariumUiSurface surface)
|
||||
{
|
||||
var edgePadding = surface.ContentPadding <= 0.0f ? 0.0f : 8.0f;
|
||||
var bounds = RectFromEdges(
|
||||
Math.Clamp(surface.Bounds.Left, edgePadding, Math.Max(edgePadding, width - 80.0f)),
|
||||
Math.Clamp(surface.Bounds.Top, edgePadding, Math.Max(edgePadding, height - 48.0f)),
|
||||
Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 80.0f + edgePadding, width - edgePadding),
|
||||
Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 48.0f + edgePadding, height - edgePadding));
|
||||
renderTarget.FillRectangle(bounds, panelBrush);
|
||||
renderTarget.DrawRectangle(bounds, outlineBrush, 1.0f);
|
||||
var hasTitle = !string.IsNullOrWhiteSpace(surface.Title);
|
||||
if (hasTitle)
|
||||
{
|
||||
DrawHeader(surface.Title, titleFormat, RectFromEdges(bounds.Left + 12.0f, bounds.Top + 8.0f, bounds.Right - 12.0f, bounds.Top + 34.0f), primaryTextBrush);
|
||||
}
|
||||
|
||||
var contentTop = hasTitle ? bounds.Top + 42.0f : bounds.Top + surface.ContentPadding;
|
||||
var content = RectFromEdges(bounds.Left + surface.ContentPadding, contentTop, bounds.Right - surface.ContentPadding, bounds.Bottom - surface.ContentPadding);
|
||||
DrawSurfaceChildren(surface.Root.Children ?? [], content, surface.Root.Layout ?? AquariumUiLayout.Vertical());
|
||||
}
|
||||
|
||||
private void DrawSurfaceChildren(IReadOnlyList<AquariumUiElement> elements, Rect bounds, AquariumUiLayout layout)
|
||||
{
|
||||
var visible = elements.Where(static element => element.Visible).ToArray();
|
||||
if (visible.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var content = RectFromEdges(
|
||||
bounds.Left + layout.Padding,
|
||||
bounds.Top + layout.Padding,
|
||||
bounds.Right - layout.Padding,
|
||||
bounds.Bottom - layout.Padding);
|
||||
var totalGap = layout.Gap * Math.Max(0, visible.Length - 1);
|
||||
var horizontal = string.Equals(layout.Direction, "horizontal", StringComparison.Ordinal);
|
||||
var cursor = horizontal ? content.Left : content.Top;
|
||||
var available = Math.Max(0.0f, (horizontal ? content.Width : content.Height) - totalGap);
|
||||
var preferred = visible.Select(element => PreferredExtent(element, horizontal)).ToArray();
|
||||
var preferredTotal = preferred.Sum(static value => value ?? 0.0f);
|
||||
var preferredScale = preferredTotal > available && preferredTotal > 0.0f ? available / preferredTotal : 1.0f;
|
||||
var flexibleWeight = Math.Max(0.001f, visible.Where((_, index) => preferred[index] is null).Sum(static element => Math.Max(0.001f, element.Weight)));
|
||||
var flexibleAvailable = Math.Max(0.0f, available - preferredTotal * preferredScale);
|
||||
for (var index = 0; index < visible.Length; index++)
|
||||
{
|
||||
var element = visible[index];
|
||||
var extent = preferred[index] is { } fixedExtent
|
||||
? fixedExtent * preferredScale
|
||||
: flexibleAvailable * Math.Max(0.001f, element.Weight) / flexibleWeight;
|
||||
var childBounds = horizontal
|
||||
? RectFromEdges(cursor, content.Top, Math.Min(content.Right, cursor + extent), content.Bottom)
|
||||
: RectFromEdges(content.Left, cursor, content.Right, Math.Min(content.Bottom, cursor + extent));
|
||||
DrawSurfaceElement(element, childBounds);
|
||||
cursor += extent + layout.Gap;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSurfaceElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
switch (element.Kind)
|
||||
{
|
||||
case "group":
|
||||
DrawSurfaceChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical());
|
||||
break;
|
||||
case "pane":
|
||||
var hasPaneTitle = !string.IsNullOrWhiteSpace(element.Text);
|
||||
DrawSurfaceFrame(bounds, element.Text, element.Style?.Tone ?? "neutral", drawTitle: hasPaneTitle);
|
||||
var paneContent = hasPaneTitle
|
||||
? RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f)
|
||||
: bounds;
|
||||
DrawSurfaceChildren(element.Children ?? [], paneContent, element.Layout ?? AquariumUiLayout.Vertical());
|
||||
break;
|
||||
case "card":
|
||||
DrawSurfaceFrame(bounds, null, element.Style?.Tone ?? "neutral", drawTitle: false);
|
||||
DrawSurfaceChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical(4.0f, 8.0f));
|
||||
break;
|
||||
case "metric":
|
||||
DrawMetricElement(element, bounds);
|
||||
break;
|
||||
case "toggle":
|
||||
DrawToggleElement(element, bounds);
|
||||
break;
|
||||
case "slider":
|
||||
DrawSliderElement(element, bounds);
|
||||
break;
|
||||
case "select":
|
||||
DrawSelectElement(element, bounds);
|
||||
break;
|
||||
case "button":
|
||||
DrawButtonElement(element, bounds);
|
||||
break;
|
||||
case "preview":
|
||||
DrawPreviewElement(element, bounds);
|
||||
break;
|
||||
default:
|
||||
DrawTextElement(element, bounds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSurfaceFrame(Rect bounds, string? titleText, string tone, bool drawTitle)
|
||||
{
|
||||
renderTarget.FillRectangle(bounds, tone == "danger" ? activeRowBrush : tone == "warm" ? hoverRowBrush : rowBrush);
|
||||
renderTarget.DrawRectangle(bounds, ToneBrush(tone), 1.0f);
|
||||
if (drawTitle && !string.IsNullOrWhiteSpace(titleText))
|
||||
{
|
||||
DrawHeader(titleText, smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 6.0f, bounds.Right - 8.0f, bounds.Top + 28.0f), accentBrush);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTextElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var text = element.ReadText?.Invoke() ?? element.Text ?? "";
|
||||
var format = element.Role is "mono" ? monospaceFormat : smallFormat;
|
||||
var brush = element.Role is "caption" ? quietTextBrush : element.Role is "strong" or "title" ? primaryTextBrush : primaryTextBrush;
|
||||
renderTarget.DrawText(text, format, bounds, brush, DrawTextOptions.Clip);
|
||||
}
|
||||
|
||||
private void DrawMetricElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var value = element.ReadMetric?.Invoke() ?? 0.0;
|
||||
var normalized = Math.Clamp(value, 0.0, 1.0);
|
||||
DrawSurfaceFrame(bounds, null, element.Style?.Tone ?? "neutral", drawTitle: false);
|
||||
renderTarget.DrawText($"{element.Text}: {value:0.000}", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 2.0f, bounds.Right - 8.0f, bounds.Top + 18.0f), primaryTextBrush, DrawTextOptions.Clip);
|
||||
var track = RectFromEdges(bounds.Left + 8.0f, bounds.Bottom - 10.0f, bounds.Right - 8.0f, bounds.Bottom - 5.0f);
|
||||
renderTarget.FillRectangle(track, dimAccentBrush);
|
||||
renderTarget.FillRectangle(RectFromEdges(track.Left, track.Top, track.Left + track.Width * (float)normalized, track.Bottom), ToneBrush(element.Style?.Tone ?? "neutral"));
|
||||
}
|
||||
|
||||
private void DrawToggleElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var isOn = element.ReadToggle?.Invoke() ?? false;
|
||||
DrawSurfaceFrame(bounds, null, isOn ? "cool" : "neutral", drawTitle: false);
|
||||
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Right - 36.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
|
||||
var box = RectFromEdges(bounds.Right - 26.0f, bounds.Top + 7.0f, bounds.Right - 10.0f, bounds.Top + 23.0f);
|
||||
renderTarget.DrawRectangle(box, isOn ? accentBrush : outlineBrush, 1.0f);
|
||||
if (isOn)
|
||||
{
|
||||
renderTarget.FillRectangle(RectFromEdges(box.Left + 3.0f, box.Top + 3.0f, box.Right - 3.0f, box.Bottom - 3.0f), accentBrush);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSliderElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var raw = element.ReadFloat?.Invoke() ?? element.Min;
|
||||
var span = Math.Max(0.0001f, element.Max - element.Min);
|
||||
var normalized = Math.Clamp((raw - element.Min) / span, 0.0f, 1.0f);
|
||||
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
|
||||
renderTarget.DrawText($"{element.Text}: {raw.ToString(element.Format, System.Globalization.CultureInfo.InvariantCulture)}", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Left + 170.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
|
||||
var track = RectFromEdges(bounds.Left + 178.0f, bounds.Top + 13.0f, bounds.Right - 12.0f, bounds.Top + 18.0f);
|
||||
renderTarget.FillRectangle(track, dimAccentBrush);
|
||||
renderTarget.FillRectangle(RectFromEdges(track.Left, track.Top, track.Left + track.Width * normalized, track.Bottom), accentBrush);
|
||||
}
|
||||
|
||||
private void DrawSelectElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var selected = element.ReadOption?.Invoke() ?? 0;
|
||||
var label = element.Options?.FirstOrDefault(option => option.Value == selected).Label ?? selected.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
|
||||
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Left + 130.0f, bounds.Bottom), accentBrush, DrawTextOptions.Clip);
|
||||
renderTarget.DrawText(label, smallFormat, RectFromEdges(bounds.Left + 138.0f, bounds.Top, bounds.Right - 8.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
|
||||
}
|
||||
|
||||
private void DrawButtonElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
DrawSurfaceFrame(bounds, null, "warm", drawTitle: false);
|
||||
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Right - 8.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
|
||||
}
|
||||
|
||||
private void DrawPreviewElement(AquariumUiElement element, Rect bounds)
|
||||
{
|
||||
var hasLabel = !string.IsNullOrWhiteSpace(element.Text);
|
||||
if (hasLabel)
|
||||
{
|
||||
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
|
||||
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 6.0f, bounds.Right - 8.0f, bounds.Top + 24.0f), quietTextBrush, DrawTextOptions.Clip);
|
||||
}
|
||||
|
||||
var padded = hasLabel
|
||||
? RectFromEdges(bounds.Left + 10.0f, bounds.Top + 30.0f, bounds.Right - 10.0f, bounds.Bottom - 10.0f)
|
||||
: RectFromEdges(bounds.Left + 2.0f, bounds.Top + 2.0f, bounds.Right - 2.0f, bounds.Bottom - 2.0f);
|
||||
if (padded.Width <= 1.0f || padded.Height <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetAspect = 16.0f / 9.0f;
|
||||
var canvasWidth = padded.Width;
|
||||
var canvasHeight = canvasWidth / targetAspect;
|
||||
if (canvasHeight > padded.Height)
|
||||
{
|
||||
canvasHeight = padded.Height;
|
||||
canvasWidth = canvasHeight * targetAspect;
|
||||
}
|
||||
|
||||
var canvas = RectFromEdges(
|
||||
padded.Left + (padded.Width - canvasWidth) * 0.5f,
|
||||
padded.Top + (padded.Height - canvasHeight) * 0.5f,
|
||||
padded.Left + (padded.Width + canvasWidth) * 0.5f,
|
||||
padded.Top + (padded.Height + canvasHeight) * 0.5f);
|
||||
if (element.PreviewBlitsOutputBuffer)
|
||||
{
|
||||
DrawOutputSnapshot(canvas);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderTarget.FillRectangle(canvas, panelBrush);
|
||||
}
|
||||
|
||||
renderTarget.DrawRectangle(canvas, outlineBrush, 1.0f);
|
||||
DrawPreviewGuides(element, canvas);
|
||||
|
||||
var items = element.ReadPreviewItems?.Invoke() ?? [];
|
||||
var state = element.ReadPreviewState?.Invoke() ?? default;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var left = canvas.Left + Math.Clamp(item.X, 0.0f, 1.0f) * canvas.Width;
|
||||
var top = canvas.Top + Math.Clamp(item.Y, 0.0f, 1.0f) * canvas.Height;
|
||||
var right = canvas.Left + Math.Clamp(item.X + item.Width, 0.0f, 1.0f) * canvas.Width;
|
||||
var bottom = canvas.Top + Math.Clamp(item.Y + item.Height, 0.0f, 1.0f) * canvas.Height;
|
||||
var itemBounds = RectFromEdges(left, top, right, bottom);
|
||||
if (itemBounds.Width <= 1.0f || itemBounds.Height <= 1.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var hovered = string.Equals(state.HoverItemId, item.Id, StringComparison.Ordinal);
|
||||
var active = string.Equals(state.ActiveItemId, item.Id, StringComparison.Ordinal);
|
||||
var brush = active || item.Selected ? accentBrush : ToneBrush(item.Tone);
|
||||
renderTarget.FillRectangle(itemBounds, active ? activeRowBrush : hovered ? hoverRowBrush : rowBrush);
|
||||
renderTarget.DrawRectangle(itemBounds, brush, active || item.Selected ? 2.0f : hovered ? 1.5f : 1.0f);
|
||||
renderTarget.DrawText(item.Label, smallFormat, RectFromEdges(itemBounds.Left + 6.0f, itemBounds.Top + 3.0f, itemBounds.Right - 6.0f, itemBounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
|
||||
if (item.Selected && element.HandlePreviewInteraction is not null)
|
||||
{
|
||||
DrawPreviewHandle(itemBounds.Left, itemBounds.Top, state, item.Id, "nw");
|
||||
DrawPreviewHandle(itemBounds.Right, itemBounds.Top, state, item.Id, "ne");
|
||||
DrawPreviewHandle(itemBounds.Left, itemBounds.Bottom, state, item.Id, "sw");
|
||||
DrawPreviewHandle(itemBounds.Right, itemBounds.Bottom, state, item.Id, "se");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPreviewGuides(AquariumUiElement element, Rect canvas)
|
||||
{
|
||||
var guides = element.ReadPreviewGuides?.Invoke() ?? [];
|
||||
foreach (var guide in guides)
|
||||
{
|
||||
var position = Math.Clamp(guide.Position, 0.0f, 1.0f);
|
||||
var brush = ToneBrush(guide.Tone);
|
||||
if (string.Equals(guide.Axis, "x", StringComparison.Ordinal))
|
||||
{
|
||||
var x = canvas.Left + position * canvas.Width;
|
||||
renderTarget.DrawLine(new Vector2(x, canvas.Top), new Vector2(x, canvas.Bottom), brush, 1.25f);
|
||||
}
|
||||
else if (string.Equals(guide.Axis, "y", StringComparison.Ordinal))
|
||||
{
|
||||
var y = canvas.Top + position * canvas.Height;
|
||||
renderTarget.DrawLine(new Vector2(canvas.Left, y), new Vector2(canvas.Right, y), brush, 1.25f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPreviewHandle(float x, float y, AquariumUiPreviewState state, string itemId, string handleId)
|
||||
{
|
||||
var active = string.Equals(state.ActiveItemId, itemId, StringComparison.Ordinal) &&
|
||||
string.Equals(state.ActiveHandle, handleId, StringComparison.Ordinal);
|
||||
var hovered = string.Equals(state.HoverItemId, itemId, StringComparison.Ordinal) &&
|
||||
string.Equals(state.HoverHandle, handleId, StringComparison.Ordinal);
|
||||
var radius = active ? 6.0f : hovered ? 5.0f : 4.0f;
|
||||
var handle = RectFromEdges(x - radius, y - radius, x + radius, y + radius);
|
||||
renderTarget.FillRectangle(handle, active ? accentActiveBrush : hovered ? accentHoverBrush : accentBrush);
|
||||
renderTarget.DrawRectangle(handle, panelBrush, 1.0f);
|
||||
}
|
||||
|
||||
private void CaptureOutputSnapshot()
|
||||
{
|
||||
try
|
||||
{
|
||||
outputSnapshot.CopyFromRenderTarget(renderTarget);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The overlay must never kill the frame if the swapchain surface is not copyable.
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOutputSnapshot(Rect canvas)
|
||||
{
|
||||
if (canvas.Width <= 1.0f || canvas.Height <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceWidth = (float)Math.Max(1, width);
|
||||
var sourceHeight = (float)Math.Max(1, height);
|
||||
var sourceAspect = sourceWidth / sourceHeight;
|
||||
var targetAspect = canvas.Width / Math.Max(1.0f, canvas.Height);
|
||||
if (sourceAspect > targetAspect)
|
||||
{
|
||||
sourceWidth = sourceHeight * targetAspect;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceHeight = sourceWidth / targetAspect;
|
||||
}
|
||||
|
||||
var source = new Rect(
|
||||
(width - sourceWidth) * 0.5f,
|
||||
(height - sourceHeight) * 0.5f,
|
||||
sourceWidth,
|
||||
sourceHeight);
|
||||
renderTarget.DrawBitmap(outputSnapshot, canvas, 1.0f, BitmapInterpolationMode.Linear, source);
|
||||
}
|
||||
|
||||
private ID2D1SolidColorBrush ToneBrush(string tone) =>
|
||||
tone switch
|
||||
{
|
||||
"cool" => primaryTextBrush,
|
||||
"warm" => accentBrush,
|
||||
"danger" => accentActiveBrush,
|
||||
_ => outlineBrush,
|
||||
};
|
||||
|
||||
private static float? PreferredExtent(AquariumUiElement element, bool horizontal)
|
||||
{
|
||||
if (element.FixedExtent is { } fixedExtent)
|
||||
{
|
||||
return Math.Max(0.0f, fixedExtent);
|
||||
}
|
||||
|
||||
if (horizontal)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var weight = Math.Max(0.001f, element.Weight);
|
||||
return element.Kind switch
|
||||
{
|
||||
"group" or "pane" or "card" when HasFlexibleChild(element) => null,
|
||||
"group" or "pane" or "card" => PreferredContainerExtent(element) * weight,
|
||||
"text" when element.Role is "mono" => 18.0f * weight,
|
||||
"text" when element.Role is "strong" or "title" => 22.0f * weight,
|
||||
"text" => 18.0f * weight,
|
||||
"toggle" or "select" or "slider" => 24.0f * weight,
|
||||
"button" => 30.0f * weight,
|
||||
"metric" => 36.0f * weight,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasFlexibleChild(AquariumUiElement element)
|
||||
{
|
||||
var children = element.Children?.Where(static child => child.Visible).ToArray() ?? [];
|
||||
return children.Any(static child => PreferredExtent(child, horizontal: false) is null);
|
||||
}
|
||||
|
||||
private static float PreferredContainerExtent(AquariumUiElement element)
|
||||
{
|
||||
var layout = element.Layout ?? AquariumUiLayout.Vertical();
|
||||
var children = element.Children?.Where(static child => child.Visible).ToArray() ?? [];
|
||||
if (children.Length == 0)
|
||||
{
|
||||
return element.Kind == "pane" ? 72.0f : 24.0f;
|
||||
}
|
||||
|
||||
var horizontal = string.Equals(layout.Direction, "horizontal", StringComparison.Ordinal);
|
||||
var childExtents = children.Select(static child => PreferredExtent(child, horizontal: false) ?? 48.0f * Math.Max(0.001f, child.Weight)).ToArray();
|
||||
var contentExtent = horizontal
|
||||
? childExtents.Max()
|
||||
: childExtents.Sum() + layout.Gap * Math.Max(0, childExtents.Length - 1);
|
||||
contentExtent += layout.Padding * 2.0f;
|
||||
|
||||
return element.Kind switch
|
||||
{
|
||||
"pane" when !string.IsNullOrWhiteSpace(element.Text) => contentExtent + 38.0f,
|
||||
"pane" => contentExtent,
|
||||
"card" => contentExtent,
|
||||
_ => contentExtent,
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
smallFormat.Dispose();
|
||||
|
|
@ -194,6 +593,7 @@ internal sealed class DirectWriteOverlay : IDisposable
|
|||
panelBrush.Dispose();
|
||||
quietTextBrush.Dispose();
|
||||
primaryTextBrush.Dispose();
|
||||
outputSnapshot.Dispose();
|
||||
renderTarget.Dispose();
|
||||
directWriteFactory.Dispose();
|
||||
direct2DFactory.Dispose();
|
||||
|
|
|
|||
|
|
@ -27,4 +27,8 @@ public interface IAquariumRenderer : IAquariumFieldResourceBroker, IDisposable
|
|||
void Render(AquariumFrame frame, int width, int height);
|
||||
|
||||
void SaveFramePng(string path);
|
||||
|
||||
void SaveBokushoBrushCanvasRaw(string path);
|
||||
|
||||
void SaveBokushoPageDensityRaw(string path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
|
||||
struct ParityInput { float4 position_base; float4 slope_fade; float4 scalar0; float4 rounding; float4 onset; float4 scalar1; float4 scalar2; float4 scalar3; };
|
||||
struct ParityOutput { float4 delta_magnitude; float4 ridge_fade; };
|
||||
StructuredBuffer<ParityInput> inputs : register(t0);
|
||||
RWStructuredBuffer<ParityOutput> outputs : register(u0);
|
||||
cbuffer ParityConstants : register(b0) { uint input_count; }
|
||||
|
||||
[numthreads(64, 1, 1)]
|
||||
void D3D12AdvancedErosionParityCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if (id.x >= input_count) return;
|
||||
ParityInput input = inputs[id.x];
|
||||
GameCultGeometryAdvancedErosionParameters p;
|
||||
p.scale=input.scalar0.x; p.strength=input.scalar0.y; p.gully_weight=input.scalar0.z; p.detail=input.scalar0.w;
|
||||
p.rounding=input.rounding; p.onset=input.onset; p.assumed_slope=input.scalar1.xy;
|
||||
p.cell_scale=input.scalar1.z; p.normalization=input.scalar1.w; p.octaves=(int)input.scalar2.x;
|
||||
p.lacunarity=input.scalar2.y; p.gain=input.scalar2.z;
|
||||
GameCultGeometryErosionBandSelection band; band.active_octaves=(int)input.scalar3.x; band.final_octave_weight=input.scalar3.y; band.finest_included_wavelength=input.scalar3.z; band.unresolved_height_bound=input.scalar3.w;
|
||||
GameCultGeometryAdvancedErosionResult r = gamecult_geometry_advanced_erosion_filter_banded(input.position_base.xy, float3(input.position_base.z, input.slope_fade.xy), input.slope_fade.z, p, band);
|
||||
outputs[id.x].delta_magnitude=float4(r.delta, r.magnitude);
|
||||
outputs[id.x].ridge_fade=float4(r.ridge_map, r.fade_target, 0, 0);
|
||||
}
|
||||
699
src/Aquarium.Engine/Render/Shaders/D3D12BokushoBrush.hlsl
Normal file
699
src/Aquarium.Engine/Render/Shaders/D3D12BokushoBrush.hlsl
Normal file
|
|
@ -0,0 +1,699 @@
|
|||
#include "CultMath/CultMath.hlsl"
|
||||
|
||||
static const float BOKUSHO_PAGE_DENSITY_SCALE = 65535.0;
|
||||
static const float BOKUSHO_PAGE_PATCH_TRANSFER_GAIN = 0.0044;
|
||||
static const float BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE = 0.32;
|
||||
static const float BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE = 0.32;
|
||||
static const float BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS = 0.001;
|
||||
static const float BOKUSHO_PAGE_PATCH_WORLD_QUANTUM = 1.0 / 1024.0;
|
||||
static const float BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM = 1.0 / 4096.0;
|
||||
static const float BOKUSHO_STROKE_RHYTHM_QUANTUM = 1.0 / 4096.0;
|
||||
static const uint BOKUSHO_PAGE_PATCH_INK_UNITS = 2048u;
|
||||
static const uint BOKUSHO_PAGE_PATCH_COVERAGE_UNITS = 1024u;
|
||||
static const uint BOKUSHO_PAGE_PATCH_TRANSFER_ENCODED_GAIN = 288u;
|
||||
static const uint BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM = 8u;
|
||||
|
||||
cbuffer BokushoBrushConstants : register(b4)
|
||||
{
|
||||
float4 brushShape; // sampleCount, tuftCount, physicsHz, strokeCount
|
||||
float4 brushMaterial; // radius, pressure, inkLoad, wetness
|
||||
float4 brushDynamics; // splay, bend, friction, reserved
|
||||
float4 brushProfile; // radiusScale, pressureScale, normalScale, tangentScale
|
||||
float4 strokeP0;
|
||||
float4 strokeP1;
|
||||
float4 strokeP2;
|
||||
float4 strokeP3;
|
||||
float4 pageView; // center.xy, radius, pageSize
|
||||
};
|
||||
|
||||
struct BokushoBrushStroke
|
||||
{
|
||||
float4 profile;
|
||||
float4 dynamics;
|
||||
float4 pose;
|
||||
float4 p0;
|
||||
float4 p1;
|
||||
float4 p2;
|
||||
float4 p3;
|
||||
};
|
||||
|
||||
RWStructuredBuffer<float> BokushoTraceField : register(u20);
|
||||
RWStructuredBuffer<float> BokushoCanvasField : register(u21);
|
||||
RWStructuredBuffer<float4> BokushoTipField : register(u22);
|
||||
RWStructuredBuffer<uint> BokushoPageDensityField : register(u23);
|
||||
StructuredBuffer<BokushoBrushStroke> BokushoBrushStrokes : register(t78);
|
||||
StructuredBuffer<float4> BokushoSourcePoints : register(t81);
|
||||
|
||||
float QuantizeScalar(float value, float quantum)
|
||||
{
|
||||
return quantum > 0.0 ? floor((value / quantum) + 0.5) * quantum : value;
|
||||
}
|
||||
|
||||
float2 QuantizeFloat2(float2 value, float quantum)
|
||||
{
|
||||
return float2(QuantizeScalar(value.x, quantum), QuantizeScalar(value.y, quantum));
|
||||
}
|
||||
|
||||
float QuantizePositive(float value, float quantum)
|
||||
{
|
||||
return QuantizeScalar(max(value, 0.0), quantum);
|
||||
}
|
||||
|
||||
uint QuantizePositiveUnits(float value, uint units)
|
||||
{
|
||||
return (uint)floor(saturate(value) * (float)units + 0.5);
|
||||
}
|
||||
|
||||
float2 PacketStrokePoint(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
return cultmath_catmullrom(stroke.p0.xy, stroke.p1.xy, stroke.p2.xy, stroke.p3.xy, t);
|
||||
}
|
||||
|
||||
bool TrySampleSourcePoint(BokushoBrushStroke stroke, float sourceT, out float2 sampledPoint)
|
||||
{
|
||||
sampledPoint = float2(0.0, 0.0);
|
||||
uint sourcePointCount = (uint)round(max(brushDynamics.w, 0.0));
|
||||
uint start = (uint)round(max(stroke.p1.z, 0.0));
|
||||
uint count = (uint)round(max(stroke.p1.w, 0.0));
|
||||
if (count < 2u || sourcePointCount == 0u || start >= sourcePointCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
count = min(count, sourcePointCount - start);
|
||||
if (count < 2u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float target = saturate(sourceT);
|
||||
float4 first = BokushoSourcePoints[start];
|
||||
if (target <= first.z)
|
||||
{
|
||||
sampledPoint = first.xy;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint lastIndex = start + count - 1u;
|
||||
float4 last = BokushoSourcePoints[lastIndex];
|
||||
if (target >= last.z)
|
||||
{
|
||||
sampledPoint = last.xy;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint low = start;
|
||||
uint high = lastIndex;
|
||||
[loop]
|
||||
while (low + 1u < high)
|
||||
{
|
||||
uint mid = (low + high) / 2u;
|
||||
if (BokushoSourcePoints[mid].z <= target)
|
||||
{
|
||||
low = mid;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
float4 a = BokushoSourcePoints[low];
|
||||
float4 b = BokushoSourcePoints[high];
|
||||
float span = max(b.z - a.z, 0.000001);
|
||||
float local = saturate((target - a.z) / span);
|
||||
sampledPoint = lerp(a.xy, b.xy, local);
|
||||
return true;
|
||||
}
|
||||
|
||||
float2 StrokePoint(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
float2 sampledPoint;
|
||||
float sourceT = saturate(lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
|
||||
if (TrySampleSourcePoint(stroke, sourceT, sampledPoint))
|
||||
{
|
||||
return QuantizeFloat2(sampledPoint, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
|
||||
}
|
||||
|
||||
return PacketStrokePoint(stroke, t);
|
||||
}
|
||||
|
||||
float2 StrokeTangent(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
float dt = 1.0 / max(brushShape.x - 1.0, 1.0);
|
||||
float2 before = StrokePoint(stroke, saturate(t - dt));
|
||||
float2 after = StrokePoint(stroke, saturate(t + dt));
|
||||
return cultmath_normalize(after - before);
|
||||
}
|
||||
|
||||
float2 StrokeTangent(BokushoBrushStroke stroke, float t, uint stepCount)
|
||||
{
|
||||
float dt = 1.0 / max((float)stepCount - 1.0, 1.0);
|
||||
float2 before = StrokePoint(stroke, saturate(t - dt));
|
||||
float2 after = StrokePoint(stroke, saturate(t + dt));
|
||||
return cultmath_normalize(after - before);
|
||||
}
|
||||
|
||||
bool SameSourceBoundary(BokushoBrushStroke previous, BokushoBrushStroke next)
|
||||
{
|
||||
return previous.p3.w >= 0.0 &&
|
||||
abs(previous.p3.w - next.p3.w) <= 0.5 &&
|
||||
abs(saturate(previous.p0.w) - saturate(next.p0.z)) <= 0.001;
|
||||
}
|
||||
|
||||
float2 StrokeTangent(BokushoBrushStroke stroke, uint strokeIndex, float t, uint stepCount)
|
||||
{
|
||||
float dt = 1.0 / max((float)stepCount - 1.0, 1.0);
|
||||
uint sourcePointCount = (uint)round(max(brushDynamics.w, 0.0));
|
||||
uint sourceRangeCount = (uint)round(max(stroke.p1.w, 0.0));
|
||||
if (sourcePointCount > 0u && sourceRangeCount >= 2u)
|
||||
{
|
||||
float sourceStart = saturate(stroke.p0.z);
|
||||
float sourceEnd = saturate(max(stroke.p0.z, stroke.p0.w));
|
||||
float sourceSpan = max(sourceEnd - sourceStart, 1.0 / max((float)(stepCount - 1u), 1.0));
|
||||
float sourceT = saturate(lerp(sourceStart, sourceEnd, t));
|
||||
float sourceDt = sourceSpan * dt;
|
||||
float2 sourceBefore;
|
||||
float2 sourceAfter;
|
||||
if (TrySampleSourcePoint(stroke, saturate(sourceT - sourceDt), sourceBefore) &&
|
||||
TrySampleSourcePoint(stroke, saturate(sourceT + sourceDt), sourceAfter))
|
||||
{
|
||||
sourceBefore = QuantizeFloat2(sourceBefore, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
|
||||
sourceAfter = QuantizeFloat2(sourceAfter, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
|
||||
return cultmath_normalize(sourceAfter - sourceBefore);
|
||||
}
|
||||
}
|
||||
|
||||
float beforeT = t - dt;
|
||||
float afterT = t + dt;
|
||||
float2 before = StrokePoint(stroke, saturate(beforeT));
|
||||
float2 after = StrokePoint(stroke, saturate(afterT));
|
||||
uint strokeCount = max((uint)round(brushShape.w), 1u);
|
||||
if (beforeT < 0.0 && strokeIndex > 0u)
|
||||
{
|
||||
BokushoBrushStroke previous = BokushoBrushStrokes[strokeIndex - 1u];
|
||||
if (SameSourceBoundary(previous, stroke))
|
||||
{
|
||||
before = StrokePoint(previous, saturate(1.0 + beforeT));
|
||||
}
|
||||
}
|
||||
|
||||
if (afterT > 1.0 && strokeIndex + 1u < strokeCount)
|
||||
{
|
||||
BokushoBrushStroke next = BokushoBrushStrokes[strokeIndex + 1u];
|
||||
if (SameSourceBoundary(stroke, next))
|
||||
{
|
||||
after = StrokePoint(next, saturate(afterT - 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
before = QuantizeFloat2(before, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
|
||||
after = QuantizeFloat2(after, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
|
||||
return cultmath_normalize(after - before);
|
||||
}
|
||||
|
||||
float StrokeTaper(float t)
|
||||
{
|
||||
float entry = cultmath_smoothstep(0.0, 0.10, t);
|
||||
float exit = 1.0 - cultmath_smoothstep(0.86, 1.0, t);
|
||||
return 0.04 + entry * exit * 0.96;
|
||||
}
|
||||
|
||||
float StrokeProgress(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
return saturate(lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
|
||||
}
|
||||
|
||||
float StrokeTaper(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
float strokeT = StrokeProgress(stroke, t);
|
||||
float entryTaper = clamp(stroke.dynamics.x, 0.01, 0.50);
|
||||
float exitTaper = clamp(stroke.dynamics.y, 0.01, 0.50);
|
||||
float entry = cultmath_smoothstep(0.0, entryTaper, strokeT);
|
||||
float exit = 1.0 - cultmath_smoothstep(1.0 - exitTaper, 1.0, strokeT);
|
||||
float x = saturate(entry * exit);
|
||||
float contact = x * (0.76 + x * 0.24);
|
||||
return 0.018 + contact * 0.982;
|
||||
}
|
||||
|
||||
uint StrokeNoiseStep(float strokeT)
|
||||
{
|
||||
return (uint)floor(saturate(strokeT) * 4096.0 + 0.5);
|
||||
}
|
||||
|
||||
float LaneCohesion(float edge, float split, float wetness, float pressure)
|
||||
{
|
||||
return saturate(0.50 + (1.0 - edge) * 0.38 + wetness * 0.22 + pressure * 0.12 - split * 0.18);
|
||||
}
|
||||
|
||||
float StrokeSegmentSpan(BokushoBrushStroke stroke, uint sampleCount)
|
||||
{
|
||||
float start = saturate(stroke.p0.z);
|
||||
float endValue = saturate(max(stroke.p0.z, stroke.p0.w));
|
||||
return max(endValue - start, 1.0 / max((float)(sampleCount - 1u), 1.0));
|
||||
}
|
||||
|
||||
float CanvasPigment(float value)
|
||||
{
|
||||
return 0.96 * (1.0 - exp(-max(value, 0.0) * 0.82));
|
||||
}
|
||||
|
||||
float4 StrokeRhythm(BokushoBrushStroke stroke, float strokeT)
|
||||
{
|
||||
float entry = cultmath_smoothstep(0.0, 0.18, strokeT);
|
||||
float release = cultmath_smoothstep(0.74, 1.0, strokeT);
|
||||
float belly = entry * (1.0 - cultmath_smoothstep(0.62, 0.94, strokeT));
|
||||
float pose = saturate(abs(stroke.pose.x) * 0.34 + abs(stroke.pose.y) * 0.26 + (stroke.pose.w - 1.0) * 0.18);
|
||||
return float4(
|
||||
QuantizeScalar(0.90 + belly * (0.15 + pose * 0.04) + entry * 0.04 - release * (0.12 + pose * 0.03), BOKUSHO_STROKE_RHYTHM_QUANTUM),
|
||||
QuantizeScalar(0.94 + belly * (0.09 + pose * 0.03) - release * 0.08, BOKUSHO_STROKE_RHYTHM_QUANTUM),
|
||||
QuantizeScalar(0.96 + belly * 0.08 + pose * 0.02 - release * 0.04, BOKUSHO_STROKE_RHYTHM_QUANTUM),
|
||||
QuantizeScalar(0.94 + belly * (0.12 + pose * 0.04) - release * 0.10, BOKUSHO_STROKE_RHYTHM_QUANTUM));
|
||||
}
|
||||
|
||||
uint EncodePageDensityContribution(float contribution)
|
||||
{
|
||||
float c = saturate(contribution);
|
||||
float c2 = c * c;
|
||||
float c3 = c2 * c;
|
||||
float c4 = c2 * c2;
|
||||
float density = c + (c2 * 0.5) + (c3 * (1.0 / 3.0)) + (c4 * 0.25);
|
||||
uint encoded = (uint)floor(min(density * BOKUSHO_PAGE_DENSITY_SCALE, BOKUSHO_PAGE_DENSITY_SCALE) + 0.5);
|
||||
return BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM > 1u
|
||||
? ((encoded + (BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM / 2u)) / BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM) * BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM
|
||||
: encoded;
|
||||
}
|
||||
|
||||
uint EncodePageDensityContribution(uint inkUnits, uint coverageUnits)
|
||||
{
|
||||
uint product = inkUnits * coverageUnits;
|
||||
uint denominator = BOKUSHO_PAGE_PATCH_INK_UNITS * BOKUSHO_PAGE_PATCH_COVERAGE_UNITS;
|
||||
uint encoded = (product * BOKUSHO_PAGE_PATCH_TRANSFER_ENCODED_GAIN + denominator / 2u) / denominator;
|
||||
return BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM > 1u
|
||||
? ((encoded + (BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM / 2u)) / BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM) * BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM
|
||||
: encoded;
|
||||
}
|
||||
|
||||
float PageToothHash(uint x, uint y)
|
||||
{
|
||||
uint value = (x + 1u) * 0x9E3779B9u ^ (y + 1u) * 0x85EBCA6Bu ^ 0xC2B2AE35u;
|
||||
value ^= value >> 16;
|
||||
value *= 0x7FEB352Du;
|
||||
value ^= value >> 15;
|
||||
value *= 0x846CA68Bu;
|
||||
value ^= value >> 16;
|
||||
return (float)(value & 0x00FFFFFFu) / 16777215.0;
|
||||
}
|
||||
|
||||
void DepositSweptPatch(float2 previousTip, float2 tip, float previousNormalRadius, float previousTangentRadius, float normalRadius, float tangentRadius, float pigment, float paperTooth)
|
||||
{
|
||||
uint inkUnits = QuantizePositiveUnits(pigment, BOKUSHO_PAGE_PATCH_INK_UNITS);
|
||||
if (inkUnits == 0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
previousTip = QuantizeFloat2(previousTip, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
tip = QuantizeFloat2(tip, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
previousNormalRadius = QuantizePositive(previousNormalRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
previousTangentRadius = QuantizePositive(previousTangentRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
normalRadius = QuantizePositive(normalRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
tangentRadius = QuantizePositive(tangentRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
|
||||
float2 sweep = tip - previousTip;
|
||||
float sweepLength = length(sweep);
|
||||
float2 tangent = sweepLength > 0.000001 ? sweep / sweepLength : float2(1.0, 0.0);
|
||||
float2 normal = float2(-tangent.y, tangent.x);
|
||||
float previousPatchTangentRadius = max(previousTangentRadius * BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
|
||||
float previousPatchNormalRadius = max(previousNormalRadius * BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
|
||||
float currentPatchTangentRadius = max(tangentRadius * BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
|
||||
float currentPatchNormalRadius = max(normalRadius * BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
|
||||
float patchTangentRadius = max(previousPatchTangentRadius, currentPatchTangentRadius);
|
||||
float patchNormalRadius = max(previousPatchNormalRadius, currentPatchNormalRadius);
|
||||
float2 center = (previousTip + tip) * 0.5;
|
||||
float halfSweepLength = sweepLength * 0.5;
|
||||
float2 extents = float2(
|
||||
abs(tangent.x) * (halfSweepLength + patchTangentRadius) + abs(normal.x) * patchNormalRadius,
|
||||
abs(tangent.y) * (halfSweepLength + patchTangentRadius) + abs(normal.y) * patchNormalRadius);
|
||||
float pageSize = max(pageView.w, 1.0);
|
||||
float2 minUv = (center - extents - pageView.xy) / max(pageView.z, 0.001) * 0.5 + 0.5;
|
||||
float2 maxUv = (center + extents - pageView.xy) / max(pageView.z, 0.001) * 0.5 + 0.5;
|
||||
int minX = clamp((int)floor(min(minUv.x, maxUv.x) * (pageSize - 1.0)), 0, (int)pageSize - 1);
|
||||
int maxX = clamp((int)ceil(max(minUv.x, maxUv.x) * (pageSize - 1.0)), 0, (int)pageSize - 1);
|
||||
int minY = clamp((int)floor(min(minUv.y, maxUv.y) * (pageSize - 1.0)), 0, (int)pageSize - 1);
|
||||
int maxY = clamp((int)ceil(max(minUv.y, maxUv.y) * (pageSize - 1.0)), 0, (int)pageSize - 1);
|
||||
|
||||
[loop]
|
||||
for (int y = minY; y <= maxY; y += 1)
|
||||
{
|
||||
float uvY = pageSize <= 1.0 ? 0.0 : (float)y / (pageSize - 1.0);
|
||||
[loop]
|
||||
for (int x = minX; x <= maxX; x += 1)
|
||||
{
|
||||
float uvX = pageSize <= 1.0 ? 0.0 : (float)x / (pageSize - 1.0);
|
||||
float2 world = pageView.xy + (float2(uvX, uvY) * 2.0 - 1.0) * pageView.z;
|
||||
float2 local = world - center;
|
||||
float axialExcess = max(abs(dot(local, tangent)) - halfSweepLength, 0.0);
|
||||
float tangentDistance = axialExcess / patchTangentRadius;
|
||||
float normalDistance = dot(local, normal) / patchNormalRadius;
|
||||
float ellipse = sqrt(tangentDistance * tangentDistance + normalDistance * normalDistance);
|
||||
float coverage = cultmath_smoothstep(1.0, 0.0, ellipse);
|
||||
float tooth = PageToothHash((uint)x, (uint)y);
|
||||
float interiorTooth = (1.0 - cultmath_smoothstep(0.0, 0.34, ellipse)) * saturate(paperTooth);
|
||||
coverage *= 1.0 - interiorTooth * (0.03 + tooth * 0.10);
|
||||
float edgeTooth = cultmath_smoothstep(0.38, 1.0, ellipse) * saturate(paperTooth);
|
||||
if (edgeTooth > 0.0)
|
||||
{
|
||||
coverage *= 1.0 - edgeTooth * (0.18 + tooth * 0.34);
|
||||
}
|
||||
|
||||
uint coverageUnits = QuantizePositiveUnits(coverage, BOKUSHO_PAGE_PATCH_COVERAGE_UNITS);
|
||||
if (coverageUnits == 0u)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
InterlockedAdd(BokushoPageDensityField[(uint)(y * (int)pageSize + x)], EncodePageDensityContribution(inkUnits, coverageUnits));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float LaneHash(uint strokeIndex, uint tuft, uint salt)
|
||||
{
|
||||
uint value = (strokeIndex + 1u) * 0x9E3779B9u ^ (tuft + 1u) * 0x85EBCA6Bu ^ salt;
|
||||
value ^= value >> 16;
|
||||
value *= 0x7FEB352Du;
|
||||
value ^= value >> 15;
|
||||
value *= 0x846CA68Bu;
|
||||
value ^= value >> 16;
|
||||
return (float)(value & 0x00FFFFFFu) / 16777215.0;
|
||||
}
|
||||
|
||||
uint StrokeChainStart(uint strokeIndex)
|
||||
{
|
||||
uint chainStart = strokeIndex;
|
||||
float expectedStart = saturate(BokushoBrushStrokes[strokeIndex].p0.z);
|
||||
float sourceStrokeId = BokushoBrushStrokes[strokeIndex].p3.w;
|
||||
[loop]
|
||||
while (chainStart > 0u && expectedStart > 0.0001)
|
||||
{
|
||||
if (sourceStrokeId >= 0.0 && abs(BokushoBrushStrokes[chainStart - 1u].p3.w - sourceStrokeId) > 0.5)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
float previousEnd = saturate(BokushoBrushStrokes[chainStart - 1u].p0.w);
|
||||
if (abs(previousEnd - expectedStart) > 0.001)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
chainStart = chainStart - 1u;
|
||||
expectedStart = saturate(BokushoBrushStrokes[chainStart].p0.z);
|
||||
}
|
||||
|
||||
return chainStart;
|
||||
}
|
||||
|
||||
void SimulateBokushoTuftSegment(
|
||||
BokushoBrushStroke stroke,
|
||||
uint outputStrokeIndex,
|
||||
uint laneKey,
|
||||
uint tuft,
|
||||
uint sampleCount,
|
||||
uint tuftCount,
|
||||
float restOffset,
|
||||
float edge,
|
||||
inout float offset,
|
||||
inout float2 tip,
|
||||
inout float stateLoad,
|
||||
inout float stateWet,
|
||||
bool writeOutput)
|
||||
{
|
||||
float pressure = saturate(brushMaterial.y * stroke.profile.y * 0.5) * 2.0;
|
||||
float splay = saturate(brushDynamics.x / 2.0);
|
||||
float bend = saturate(brushDynamics.y / 2.4);
|
||||
float friction = saturate(brushDynamics.z);
|
||||
float radius = max(brushMaterial.x * stroke.profile.x, 0.0001);
|
||||
float normalRadius = max(radius * stroke.profile.z, 0.0001);
|
||||
float tangentRadius = max(radius * stroke.profile.w, 0.0001);
|
||||
float segmentSpan = StrokeSegmentSpan(stroke, sampleCount);
|
||||
float integrationSpan = segmentSpan / max((float)sampleCount - 1.0, 1.0);
|
||||
float segmentVelocityScale = 1.0 / segmentSpan;
|
||||
float split = saturate((0.28 - LaneHash(laneKey, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * stroke.dynamics.w;
|
||||
|
||||
[loop]
|
||||
for (uint sample = 0u; sample < sampleCount; sample += 1u)
|
||||
{
|
||||
float t = sampleCount <= 1u ? 0.0 : (float)sample / (float)(sampleCount - 1u);
|
||||
float2 center = StrokePoint(stroke, t);
|
||||
float2 tangent = StrokeTangent(stroke, outputStrokeIndex, t, sampleCount);
|
||||
float2 normal = float2(-tangent.y, tangent.x);
|
||||
float strokeT = StrokeProgress(stroke, t);
|
||||
float taper = StrokeTaper(stroke, t);
|
||||
float4 rhythm = StrokeRhythm(stroke, strokeT);
|
||||
float localPressure = pressure * taper * rhythm.x;
|
||||
float laneCore = cultmath_smoothstep(0.0, 0.78, 1.0 - edge);
|
||||
float cohesion = saturate(0.28 + stateWet * 0.44 + laneCore * 0.24 + localPressure * 0.10 - split * 0.18);
|
||||
float localNormalRadius = max(normalRadius * rhythm.y * (0.18 + taper * 0.82) * (0.72 + splay * 0.34 + localPressure * 0.16 - cohesion * 0.08), 0.0001);
|
||||
float localTangentRadius = max(tangentRadius * rhythm.z * (0.24 + taper * 0.76) * (0.86 + bend * 0.18), 0.0001);
|
||||
float targetOffset = (restOffset + stroke.pose.y * 0.12) * localNormalRadius * (0.58 + splay * 0.34 + localPressure * 0.18 - cohesion * 0.20);
|
||||
float recovery = saturate(0.05 + stroke.pose.w * 0.08 + stateWet * 0.08 + localPressure * 0.10);
|
||||
offset = cultmath_lerp(offset, targetOffset, recovery);
|
||||
|
||||
float poseLead = stroke.pose.x * 0.36 + stroke.pose.y * 0.16;
|
||||
float lag = localTangentRadius * (0.10 + bend * 0.32 + friction * localPressure * 0.16 + edge * 0.06);
|
||||
float2 dragVector = cultmath_normalize(tangent + normal * poseLead);
|
||||
float2 desiredTip = center + normal * offset - dragVector * lag;
|
||||
float2 slip = desiredTip - tip;
|
||||
float velocity = length(slip) * brushShape.z * segmentVelocityScale / max(radius, 0.001);
|
||||
float contact = saturate(localPressure * stateLoad * (0.24 + stateWet * 0.62 + laneCore * 0.18));
|
||||
float drag = contact * friction * (0.28 + stateWet * 0.34);
|
||||
float2 previousTip = tip;
|
||||
tip = tip + slip * (0.18 + recovery * 0.82) * (1.0 - drag * 0.52);
|
||||
float sweptDistance = length(tip - previousTip);
|
||||
float sweptPatch = saturate(sweptDistance / max(localTangentRadius, 0.001) * 0.42);
|
||||
|
||||
float tension = saturate(velocity * 0.014 + abs(targetOffset - offset) / max(localNormalRadius, 0.001) * 0.22 + edge * 0.12);
|
||||
float separation = saturate(split * 0.34 + tension * 0.46 + edge * 0.20 - cohesion * 0.24);
|
||||
float adhesion = saturate(stateWet * (0.36 + cohesion * 0.40) + localPressure * 0.10 - separation * 0.22);
|
||||
float dryMemory = saturate((1.0 - stateWet) * 0.62 + separation * 0.32 + velocity * 0.004);
|
||||
float fiberNoise = LaneHash(laneKey + StrokeNoiseStep(strokeT) * 13u, tuft, 101u);
|
||||
float continuity = 1.0 - cultmath_smoothstep(0.18 + dryMemory * 0.28, 0.96, fiberNoise) * dryMemory * (0.38 + edge * 0.22);
|
||||
float inkFilm = saturate(stateLoad * (0.28 + stateWet * 0.34));
|
||||
float bristleTooth = (0.62 + continuity * (0.30 + inkFilm * 0.08)) * (0.82 + inkFilm * 0.24 - edge * 0.06);
|
||||
float contactTransfer = contact * stateLoad * (0.16 + stateWet * 0.92) * (0.30 + drag * 0.64 + localPressure * 0.22 + sweptPatch * 0.18) * (0.68 + laneCore * 0.50 - separation * 0.14) * bristleTooth;
|
||||
float airborneRelease = (1.0 - contact) * stateLoad * stateWet * saturate(velocity * 0.010 - adhesion * 0.16) * (0.20 + separation * 0.42 + edge * 0.18);
|
||||
float consumedPigment = contactTransfer + airborneRelease;
|
||||
stateLoad = max(0.0, stateLoad - consumedPigment * integrationSpan * (0.014 + localPressure * 0.009 + dryMemory * 0.004));
|
||||
stateWet = max(0.0, stateWet - consumedPigment * integrationSpan * (0.006 + dryMemory * 0.003));
|
||||
|
||||
if (writeOutput)
|
||||
{
|
||||
uint index = ((outputStrokeIndex * tuftCount) + tuft) * sampleCount + sample;
|
||||
float pigment = (contactTransfer * (0.95 + localPressure * 0.34 + sweptPatch * 0.24) + airborneRelease * (1.6 + velocity * 0.002)) * stroke.dynamics.z * rhythm.w;
|
||||
float localPigment = CanvasPigment(pigment);
|
||||
float trace = saturate(contact * 0.70 + airborneRelease * 2.0 + stateLoad * 0.18);
|
||||
BokushoTraceField[index] = trace;
|
||||
BokushoCanvasField[index] = localPigment;
|
||||
BokushoTipField[index] = float4(
|
||||
tip,
|
||||
localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10),
|
||||
localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SimulateBokushoTuftSegmentToPage(
|
||||
BokushoBrushStroke stroke,
|
||||
uint strokeIndex,
|
||||
uint laneKey,
|
||||
uint tuft,
|
||||
uint sampleCount,
|
||||
float restOffset,
|
||||
float edge,
|
||||
inout float offset,
|
||||
inout float2 tip,
|
||||
inout float stateLoad,
|
||||
inout float stateWet,
|
||||
inout float previousPatchNormalRadius,
|
||||
inout float previousPatchTangentRadius,
|
||||
bool writeOutput,
|
||||
bool writeInitialStep)
|
||||
{
|
||||
float pressure = saturate(brushMaterial.y * stroke.profile.y * 0.5) * 2.0;
|
||||
float splay = saturate(brushDynamics.x / 2.0);
|
||||
float bend = saturate(brushDynamics.y / 2.4);
|
||||
float friction = saturate(brushDynamics.z);
|
||||
float radius = max(brushMaterial.x * stroke.profile.x, 0.0001);
|
||||
float normalRadius = max(radius * stroke.profile.z, 0.0001);
|
||||
float tangentRadius = max(radius * stroke.profile.w, 0.0001);
|
||||
float segmentSpan = StrokeSegmentSpan(stroke, sampleCount);
|
||||
uint stepCount = min(max((uint)ceil(segmentSpan * brushShape.z), 2u), 4096u);
|
||||
float integrationSpan = segmentSpan / max((float)stepCount - 1.0, 1.0);
|
||||
float segmentVelocityScale = 1.0 / segmentSpan;
|
||||
float split = saturate((0.28 - LaneHash(laneKey, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * stroke.dynamics.w;
|
||||
|
||||
[loop]
|
||||
for (uint step = 0u; step < stepCount; step += 1u)
|
||||
{
|
||||
float t = stepCount <= 1u ? 0.0 : (float)step / (float)(stepCount - 1u);
|
||||
float2 center = StrokePoint(stroke, t);
|
||||
float2 tangent = StrokeTangent(stroke, strokeIndex, t, stepCount);
|
||||
float2 normal = float2(-tangent.y, tangent.x);
|
||||
float strokeT = StrokeProgress(stroke, t);
|
||||
float taper = StrokeTaper(stroke, t);
|
||||
float4 rhythm = StrokeRhythm(stroke, strokeT);
|
||||
float localPressure = pressure * taper * rhythm.x;
|
||||
float laneCore = cultmath_smoothstep(0.0, 0.78, 1.0 - edge);
|
||||
float cohesion = saturate(0.28 + stateWet * 0.44 + laneCore * 0.24 + localPressure * 0.10 - split * 0.18);
|
||||
float localNormalRadius = max(normalRadius * rhythm.y * (0.18 + taper * 0.82) * (0.72 + splay * 0.34 + localPressure * 0.16 - cohesion * 0.08), 0.0001);
|
||||
float localTangentRadius = max(tangentRadius * rhythm.z * (0.24 + taper * 0.76) * (0.86 + bend * 0.18), 0.0001);
|
||||
float targetOffset = (restOffset + stroke.pose.y * 0.12) * localNormalRadius * (0.58 + splay * 0.34 + localPressure * 0.18 - cohesion * 0.20);
|
||||
float recovery = saturate(0.05 + stroke.pose.w * 0.08 + stateWet * 0.08 + localPressure * 0.10);
|
||||
offset = cultmath_lerp(offset, targetOffset, recovery);
|
||||
|
||||
float poseLead = stroke.pose.x * 0.36 + stroke.pose.y * 0.16;
|
||||
float lag = localTangentRadius * (0.10 + bend * 0.32 + friction * localPressure * 0.16 + edge * 0.06);
|
||||
float2 dragVector = cultmath_normalize(tangent + normal * poseLead);
|
||||
float2 desiredTip = center + normal * offset - dragVector * lag;
|
||||
float2 slip = desiredTip - tip;
|
||||
float velocity = length(slip) * brushShape.z * segmentVelocityScale / max(radius, 0.001);
|
||||
float contact = saturate(localPressure * stateLoad * (0.24 + stateWet * 0.62 + laneCore * 0.18));
|
||||
float drag = contact * friction * (0.28 + stateWet * 0.34);
|
||||
float2 previousTip = tip;
|
||||
tip = tip + slip * (0.18 + recovery * 0.82) * (1.0 - drag * 0.52);
|
||||
float sweptDistance = length(tip - previousTip);
|
||||
float sweptPatch = saturate(sweptDistance / max(localTangentRadius, 0.001) * 0.42);
|
||||
|
||||
float tension = saturate(velocity * 0.014 + abs(targetOffset - offset) / max(localNormalRadius, 0.001) * 0.22 + edge * 0.12);
|
||||
float separation = saturate(split * 0.34 + tension * 0.46 + edge * 0.20 - cohesion * 0.24);
|
||||
float adhesion = saturate(stateWet * (0.36 + cohesion * 0.40) + localPressure * 0.10 - separation * 0.22);
|
||||
float dryMemory = saturate((1.0 - stateWet) * 0.62 + separation * 0.32 + velocity * 0.004);
|
||||
float fiberNoise = LaneHash(laneKey + StrokeNoiseStep(strokeT) * 13u, tuft, 101u);
|
||||
float continuity = 1.0 - cultmath_smoothstep(0.18 + dryMemory * 0.28, 0.96, fiberNoise) * dryMemory * (0.38 + edge * 0.22);
|
||||
float inkFilm = saturate(stateLoad * (0.28 + stateWet * 0.34));
|
||||
float bristleTooth = (0.62 + continuity * (0.30 + inkFilm * 0.08)) * (0.82 + inkFilm * 0.24 - edge * 0.06);
|
||||
float paperTooth = saturate(dryMemory * 0.42 + separation * 0.22 + edge * 0.10 + (1.0 - inkFilm) * 0.18);
|
||||
float contactTransfer = contact * stateLoad * (0.16 + stateWet * 0.92) * (0.30 + drag * 0.64 + localPressure * 0.22 + sweptPatch * 0.18) * (0.68 + laneCore * 0.50 - separation * 0.14) * bristleTooth;
|
||||
float airborneRelease = (1.0 - contact) * stateLoad * stateWet * saturate(velocity * 0.010 - adhesion * 0.16) * (0.20 + separation * 0.42 + edge * 0.18);
|
||||
float consumedPigment = contactTransfer + airborneRelease;
|
||||
stateLoad = max(0.0, stateLoad - consumedPigment * integrationSpan * (0.014 + localPressure * 0.009 + dryMemory * 0.004));
|
||||
stateWet = max(0.0, stateWet - consumedPigment * integrationSpan * (0.006 + dryMemory * 0.003));
|
||||
|
||||
if (writeOutput && (writeInitialStep || step > 0u))
|
||||
{
|
||||
float pigment = (contactTransfer * (0.95 + localPressure * 0.34 + sweptPatch * 0.24) + airborneRelease * (1.6 + velocity * 0.002)) * stroke.dynamics.z * rhythm.w;
|
||||
float patchNormalRadius = localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10);
|
||||
float patchTangentRadius = localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36;
|
||||
float sweepStartNormalRadius = previousPatchNormalRadius > 0.0 ? previousPatchNormalRadius : patchNormalRadius;
|
||||
float sweepStartTangentRadius = previousPatchTangentRadius > 0.0 ? previousPatchTangentRadius : patchTangentRadius;
|
||||
DepositSweptPatch(previousTip, tip, sweepStartNormalRadius, sweepStartTangentRadius, patchNormalRadius, patchTangentRadius, pigment, paperTooth);
|
||||
}
|
||||
|
||||
previousPatchNormalRadius = localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10);
|
||||
previousPatchTangentRadius = localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36;
|
||||
}
|
||||
}
|
||||
|
||||
[numthreads(128, 1, 1)]
|
||||
void D3D12BokushoBrushCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
uint sampleCount = max((uint)round(brushShape.x), 2u);
|
||||
uint tuftCount = max((uint)round(brushShape.y), 1u);
|
||||
uint strokeCount = max((uint)round(brushShape.w), 1u);
|
||||
uint globalIndex = dispatchThreadId.x;
|
||||
uint strokeIndex = globalIndex / tuftCount;
|
||||
uint tuft = globalIndex - strokeIndex * tuftCount;
|
||||
if (strokeIndex >= strokeCount || tuft >= tuftCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BokushoBrushStroke stroke = BokushoBrushStrokes[strokeIndex];
|
||||
uint chainStart = StrokeChainStart(strokeIndex);
|
||||
BokushoBrushStroke seedStroke = BokushoBrushStrokes[chainStart];
|
||||
float laneT = tuftCount <= 1u ? 0.5 : (float)tuft / (float)(tuftCount - 1u);
|
||||
float restOffset = laneT * 2.0 - 1.0;
|
||||
float edge = abs(restOffset);
|
||||
float laneHash = LaneHash(chainStart, tuft, 17u);
|
||||
float laneLoad = 0.76 + laneHash * 0.34;
|
||||
float seedPressure = saturate(brushMaterial.y * seedStroke.profile.y * 0.5) * 2.0;
|
||||
float wetness = saturate(brushMaterial.w / 1.6);
|
||||
float load = clamp(brushMaterial.z / 1.05, 0.0, 2.7);
|
||||
float splay = saturate(brushDynamics.x / 2.0);
|
||||
float seedRadius = max(brushMaterial.x * seedStroke.profile.x, 0.0001);
|
||||
float seedNormalRadius = max(seedRadius * seedStroke.profile.z, 0.0001);
|
||||
float seedSplit = saturate((0.28 - LaneHash(chainStart, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * seedStroke.dynamics.w;
|
||||
float initialCohesion = LaneCohesion(edge, seedSplit, wetness, seedPressure);
|
||||
float2 tip = StrokePoint(seedStroke, 0.0);
|
||||
float poseBias = seedStroke.pose.x * 0.18 + seedStroke.pose.y * 0.08;
|
||||
float offset = (restOffset + poseBias * (1.0 - edge * 0.35)) * seedNormalRadius * (0.42 + splay * 0.38) + (laneHash - 0.5) * seedNormalRadius * 0.05;
|
||||
float stateLoad = load * (0.86 + initialCohesion * 0.48) * (1.0 - edge * 0.08) * laneLoad * (1.0 - seedSplit * 0.18);
|
||||
float stateWet = wetness * (0.74 + initialCohesion * 0.24 - seedSplit * 0.08);
|
||||
|
||||
[loop]
|
||||
for (uint replayStrokeIndex = chainStart; replayStrokeIndex < strokeIndex; replayStrokeIndex += 1u)
|
||||
{
|
||||
SimulateBokushoTuftSegment(BokushoBrushStrokes[replayStrokeIndex], replayStrokeIndex, chainStart, tuft, sampleCount, tuftCount, restOffset, edge, offset, tip, stateLoad, stateWet, false);
|
||||
}
|
||||
|
||||
SimulateBokushoTuftSegment(stroke, strokeIndex, chainStart, tuft, sampleCount, tuftCount, restOffset, edge, offset, tip, stateLoad, stateWet, true);
|
||||
}
|
||||
|
||||
[numthreads(128, 1, 1)]
|
||||
void D3D12BokushoPageClearCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
uint pageValueCount = (uint)(max(pageView.w, 1.0) * max(pageView.w, 1.0));
|
||||
uint index = dispatchThreadId.x;
|
||||
if (index < pageValueCount)
|
||||
{
|
||||
BokushoPageDensityField[index] = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
[numthreads(128, 1, 1)]
|
||||
void D3D12BokushoPageDepositCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
uint sampleCount = max((uint)round(brushShape.x), 2u);
|
||||
uint tuftCount = max((uint)round(brushShape.y), 1u);
|
||||
uint strokeCount = max((uint)round(brushShape.w), 1u);
|
||||
uint globalIndex = dispatchThreadId.x;
|
||||
uint strokeIndex = globalIndex / tuftCount;
|
||||
uint tuft = globalIndex - strokeIndex * tuftCount;
|
||||
if (strokeIndex >= strokeCount || tuft >= tuftCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BokushoBrushStroke stroke = BokushoBrushStrokes[strokeIndex];
|
||||
uint chainStart = StrokeChainStart(strokeIndex);
|
||||
BokushoBrushStroke seedStroke = BokushoBrushStrokes[chainStart];
|
||||
float laneT = tuftCount <= 1u ? 0.5 : (float)tuft / (float)(tuftCount - 1u);
|
||||
float restOffset = laneT * 2.0 - 1.0;
|
||||
float edge = abs(restOffset);
|
||||
float laneHash = LaneHash(chainStart, tuft, 17u);
|
||||
float laneLoad = 0.76 + laneHash * 0.34;
|
||||
float seedPressure = saturate(brushMaterial.y * seedStroke.profile.y * 0.5) * 2.0;
|
||||
float wetness = saturate(brushMaterial.w / 1.6);
|
||||
float load = clamp(brushMaterial.z / 1.05, 0.0, 2.7);
|
||||
float splay = saturate(brushDynamics.x / 2.0);
|
||||
float seedRadius = max(brushMaterial.x * seedStroke.profile.x, 0.0001);
|
||||
float seedNormalRadius = max(seedRadius * seedStroke.profile.z, 0.0001);
|
||||
float seedSplit = saturate((0.28 - LaneHash(chainStart, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * seedStroke.dynamics.w;
|
||||
float initialCohesion = LaneCohesion(edge, seedSplit, wetness, seedPressure);
|
||||
float2 tip = StrokePoint(seedStroke, 0.0);
|
||||
float poseBias = seedStroke.pose.x * 0.18 + seedStroke.pose.y * 0.08;
|
||||
float offset = (restOffset + poseBias * (1.0 - edge * 0.35)) * seedNormalRadius * (0.42 + splay * 0.38) + (laneHash - 0.5) * seedNormalRadius * 0.05;
|
||||
float stateLoad = load * (0.86 + initialCohesion * 0.48) * (1.0 - edge * 0.08) * laneLoad * (1.0 - seedSplit * 0.18);
|
||||
float stateWet = wetness * (0.74 + initialCohesion * 0.24 - seedSplit * 0.08);
|
||||
float previousPatchNormalRadius = 0.0;
|
||||
float previousPatchTangentRadius = 0.0;
|
||||
|
||||
[loop]
|
||||
for (uint replayStrokeIndex = chainStart; replayStrokeIndex < strokeIndex; replayStrokeIndex += 1u)
|
||||
{
|
||||
SimulateBokushoTuftSegmentToPage(BokushoBrushStrokes[replayStrokeIndex], replayStrokeIndex, chainStart, tuft, sampleCount, restOffset, edge, offset, tip, stateLoad, stateWet, previousPatchNormalRadius, previousPatchTangentRadius, false, true);
|
||||
}
|
||||
|
||||
SimulateBokushoTuftSegmentToPage(stroke, strokeIndex, chainStart, tuft, sampleCount, restOffset, edge, offset, tip, stateLoad, stateWet, previousPatchNormalRadius, previousPatchTangentRadius, true, strokeIndex == chainStart);
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ struct SceneOut
|
|||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
|
|
@ -237,6 +238,7 @@ SceneOut ResolveFractalSplat(FractalSplatVertexOut input, bool renderTransparent
|
|||
output.metadata = float4(FIELD_ID_FRACTAL_SPLAT_BASE, normal);
|
||||
output.control = float4(opacity, reservoirConfidence, transparentField ? splat.materialConfidence.z / 10.0 : saturate((sdfResident ? sdf.centerRadius.w : splat.centerRadius.w) * 40.0), 0.0);
|
||||
output.reservoirGuide = float4(reservoirConfidence, reservoirSampleAge, domainValidity, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(input.travel / max(farDistance, 0.0001));
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ StructuredBuffer<AcousticConstraint> acousticConstraints : register(t36);
|
|||
StructuredBuffer<GpuFusionPoint> fusionPoints : register(t37);
|
||||
RWStructuredBuffer<TemporalGaussian> temporalGaussiansOut : register(u0);
|
||||
|
||||
static const uint SensorFormatR8Unorm = 3u;
|
||||
static const uint SensorFormatRg8Unorm = 6u;
|
||||
static const uint SensorFormatLeapPackedMap = 7u;
|
||||
static const uint SensorFormatYuy2 = 8u;
|
||||
|
||||
float Hash01(uint value)
|
||||
{
|
||||
value ^= value >> 16;
|
||||
|
|
@ -128,6 +133,41 @@ float4 LoadSensorTexture(uint slot, int3 pixel)
|
|||
}
|
||||
}
|
||||
|
||||
float3 YuvToRgb(float y, float u, float v)
|
||||
{
|
||||
float cb = u - 0.5;
|
||||
float cr = v - 0.5;
|
||||
return saturate(float3(
|
||||
y + 1.5748 * cr,
|
||||
y - 0.1873 * cb - 0.4681 * cr,
|
||||
y + 1.8556 * cb));
|
||||
}
|
||||
|
||||
float4 DecodeSensorSample(uint slot, GpuSensorCamera camera, uint x, uint y)
|
||||
{
|
||||
uint format = (uint)round(camera.textureRangeTime.w);
|
||||
if (format == SensorFormatYuy2)
|
||||
{
|
||||
float4 yuyv = LoadSensorTexture(slot, int3((int)(x >> 1), (int)y, 0));
|
||||
float luma = (x & 1u) == 0u ? yuyv.r : yuyv.b;
|
||||
return float4(YuvToRgb(luma, yuyv.g, yuyv.a), 1.0);
|
||||
}
|
||||
|
||||
float4 sample = LoadSensorTexture(slot, int3((int)x, (int)y, 0));
|
||||
if (format == SensorFormatR8Unorm)
|
||||
{
|
||||
return float4(sample.rrr, 1.0);
|
||||
}
|
||||
|
||||
if (format == SensorFormatRg8Unorm ||
|
||||
format == SensorFormatLeapPackedMap)
|
||||
{
|
||||
return float4(sample.rg, 0.5 * (sample.r + sample.g), 1.0);
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
float AcousticSupport(float3 center, out float3 velocityBias)
|
||||
{
|
||||
uint count = (uint)gpuFusionInfo.x;
|
||||
|
|
@ -224,9 +264,12 @@ void D3D12GpuSensorFusionCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
|||
uint localIndex = textureSampleIndex + textureSlot * 747796405u;
|
||||
uint x = (localIndex * 73u + (uint)(Hash01(localIndex) * 37.0)) % width;
|
||||
uint y = (localIndex / width * 41u + (uint)(Hash01(localIndex ^ 0x9e3779b9u) * 29.0)) % height;
|
||||
float4 rgba = LoadSensorTexture(textureSlot, int3((int)x, (int)y, 0));
|
||||
uint pairSlot = (textureSlot + 1u) % max(1u, textureCount);
|
||||
float4 pairRgba = LoadSensorTexture(pairSlot, int3((int)x, (int)y, 0));
|
||||
GpuSensorCamera pairCamera = sensorCameras[min(cameraCount - 1, pairSlot % cameraCount)];
|
||||
uint pairWidth = max(1u, (uint)pairCamera.extentsKind.x);
|
||||
uint pairHeight = max(1u, (uint)pairCamera.extentsKind.y);
|
||||
float4 rgba = DecodeSensorSample(textureSlot, camera, x, y);
|
||||
float4 pairRgba = DecodeSensorSample(pairSlot, pairCamera, min(x, pairWidth - 1u), min(y, pairHeight - 1u));
|
||||
float luma = dot(rgba.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
float pairLuma = dot(pairRgba.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
float descriptor = frac(luma * 3.17 + rgba.r * 1.91 + rgba.g * 2.37 + Hash01(localIndex ^ 0x27d4eb2du) * 0.07);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,38 @@ cbuffer HeightFieldBrushes : register(b1)
|
|||
float4 brushDomain[64];
|
||||
};
|
||||
|
||||
cbuffer BokushoPageConstants : register(b5)
|
||||
{
|
||||
float4 bokushoShape; // sampleCount, tuftCount, physicsHz, strokeCount
|
||||
float4 bokushoMaterial; // radius, pressure, inkLoad, wetness
|
||||
float4 bokushoDynamics; // splay, bend, friction, reserved
|
||||
float4 bokushoProfile; // radiusScale, pressureScale, normalScale, tangentScale
|
||||
float4 bokushoStrokeP0;
|
||||
float4 bokushoStrokeP1;
|
||||
float4 bokushoStrokeP2;
|
||||
float4 bokushoStrokeP3;
|
||||
};
|
||||
|
||||
struct BokushoBrushStroke
|
||||
{
|
||||
float4 profile;
|
||||
float4 dynamics;
|
||||
float4 pose;
|
||||
float4 p0;
|
||||
float4 p1;
|
||||
float4 p2;
|
||||
float4 p3;
|
||||
};
|
||||
|
||||
StructuredBuffer<float> BokushoCanvasField : register(t77);
|
||||
StructuredBuffer<BokushoBrushStroke> BokushoBrushStrokes : register(t78);
|
||||
StructuredBuffer<float4> BokushoTipField : register(t79);
|
||||
StructuredBuffer<uint> BokushoPageDensityField : register(t80);
|
||||
|
||||
#include "CultMath/CultMath.hlsl"
|
||||
|
||||
static const float BOKUSHO_PAGE_DENSITY_SCALE = 65535.0;
|
||||
|
||||
struct VertexOut
|
||||
{
|
||||
float4 position : SV_Position;
|
||||
|
|
@ -64,6 +96,196 @@ float2 viewWorld(float2 uv)
|
|||
return viewCenter + (uv * 2.0 - 1.0) * viewRadius;
|
||||
}
|
||||
|
||||
float2 bokushoStrokePoint(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
return cultmath_catmullrom(stroke.p0.xy, stroke.p1.xy, stroke.p2.xy, stroke.p3.xy, t);
|
||||
}
|
||||
|
||||
float2 bokushoStrokeTangent(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
float dt = 1.0 / max(bokushoShape.x - 1.0, 1.0);
|
||||
float2 before = bokushoStrokePoint(stroke, saturate(t - dt));
|
||||
float2 after = bokushoStrokePoint(stroke, saturate(t + dt));
|
||||
return cultmath_normalize(after - before);
|
||||
}
|
||||
|
||||
float bokushoStrokeTaper(BokushoBrushStroke stroke, float t)
|
||||
{
|
||||
float strokeT = saturate(cultmath_lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
|
||||
float entryTaper = clamp(stroke.dynamics.x, 0.01, 0.50);
|
||||
float exitTaper = clamp(stroke.dynamics.y, 0.01, 0.50);
|
||||
float entry = smoothstep(0.0, entryTaper, strokeT);
|
||||
float exit = 1.0 - smoothstep(1.0 - exitTaper, 1.0, strokeT);
|
||||
float contact = pow(entry * exit, 1.12);
|
||||
return 0.018 + contact * 0.982;
|
||||
}
|
||||
|
||||
float bokushoCanvasSample(uint strokeIndex, float sampleIndex, float tuftIndex)
|
||||
{
|
||||
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
|
||||
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
|
||||
float clampedSample = clamp(sampleIndex, 0.0, (float)(sampleCount - 1u));
|
||||
float clampedTuft = clamp(tuftIndex, 0.0, (float)(tuftCount - 1u));
|
||||
uint sample0 = min((uint)floor(clampedSample), sampleCount - 1u);
|
||||
uint sample1 = min(sample0 + 1u, sampleCount - 1u);
|
||||
uint tuft0 = min((uint)floor(clampedTuft), tuftCount - 1u);
|
||||
uint tuft1 = min(tuft0 + 1u, tuftCount - 1u);
|
||||
float sampleBlend = saturate(clampedSample - (float)sample0);
|
||||
float tuftBlend = saturate(clampedTuft - (float)tuft0);
|
||||
uint rowOffset = strokeIndex * tuftCount;
|
||||
float p00 = BokushoCanvasField[(rowOffset + tuft0) * sampleCount + sample0];
|
||||
float p10 = BokushoCanvasField[(rowOffset + tuft0) * sampleCount + sample1];
|
||||
float p01 = BokushoCanvasField[(rowOffset + tuft1) * sampleCount + sample0];
|
||||
float p11 = BokushoCanvasField[(rowOffset + tuft1) * sampleCount + sample1];
|
||||
float lower = cultmath_lerp(p00, p10, sampleBlend);
|
||||
float upper = cultmath_lerp(p01, p11, sampleBlend);
|
||||
return saturate(cultmath_lerp(lower, upper, tuftBlend));
|
||||
}
|
||||
|
||||
float4 bokushoTipSample(uint strokeIndex, uint sampleIndex, uint tuftIndex)
|
||||
{
|
||||
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
|
||||
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
|
||||
uint rowOffset = strokeIndex * tuftCount;
|
||||
return BokushoTipField[(rowOffset + min(tuftIndex, tuftCount - 1u)) * sampleCount + min(sampleIndex, sampleCount - 1u)];
|
||||
}
|
||||
|
||||
float bokushoStrokePageHeight(float2 world, BokushoBrushStroke stroke, uint strokeIndex)
|
||||
{
|
||||
float bestDistance = 1.0e20;
|
||||
float bestT = 0.0;
|
||||
[unroll]
|
||||
for (uint scan = 0u; scan < 16u; scan += 1u)
|
||||
{
|
||||
float t = (float)scan / 15.0;
|
||||
float2 center = bokushoStrokePoint(stroke, t);
|
||||
float distanceSquared = dot(world - center, world - center);
|
||||
if (distanceSquared < bestDistance)
|
||||
{
|
||||
bestDistance = distanceSquared;
|
||||
bestT = t;
|
||||
}
|
||||
}
|
||||
|
||||
[unroll]
|
||||
for (uint refine = 0u; refine < 3u; refine += 1u)
|
||||
{
|
||||
float span = 1.0 / (15.0 * pow(2.0, (float)refine));
|
||||
float leftT = saturate(bestT - span);
|
||||
float rightT = saturate(bestT + span);
|
||||
float2 left = bokushoStrokePoint(stroke, leftT);
|
||||
float2 right = bokushoStrokePoint(stroke, rightT);
|
||||
float leftDistance = dot(world - left, world - left);
|
||||
float rightDistance = dot(world - right, world - right);
|
||||
if (leftDistance < bestDistance)
|
||||
{
|
||||
bestDistance = leftDistance;
|
||||
bestT = leftT;
|
||||
}
|
||||
|
||||
if (rightDistance < bestDistance)
|
||||
{
|
||||
bestDistance = rightDistance;
|
||||
bestT = rightT;
|
||||
}
|
||||
}
|
||||
|
||||
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
|
||||
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
|
||||
float2 center = bokushoStrokePoint(stroke, bestT);
|
||||
float2 tangent = bokushoStrokeTangent(stroke, bestT);
|
||||
float2 normal = float2(-tangent.y, tangent.x);
|
||||
float lateral = dot(world - center, normal);
|
||||
float taper = bokushoStrokeTaper(stroke, bestT);
|
||||
float radius = max(bokushoMaterial.x * stroke.profile.x * stroke.profile.z * (0.18 + taper * 0.82), 0.0001);
|
||||
float splay = saturate(bokushoDynamics.x / 2.0);
|
||||
float pressure = saturate(bokushoMaterial.y * stroke.profile.y * 0.5) * taper;
|
||||
float wetSpread = 0.86 + saturate(bokushoMaterial.w / 1.6) * 0.18;
|
||||
float footprint = radius * wetSpread * (0.72 + splay * 0.44 + pressure * 0.22);
|
||||
float tuftT = saturate(lateral / max(footprint, 0.001) * 0.5 + 0.5);
|
||||
float distance = sqrt(bestDistance);
|
||||
float contact = smoothstep(1.0, 0.0, distance / max(footprint * 0.96, 0.001));
|
||||
float contactCore = contact * contact * (3.0 - 2.0 * contact);
|
||||
uint centerSample = min((uint)round(bestT * (float)(sampleCount - 1u)), sampleCount - 1u);
|
||||
uint centerTuft = min((uint)round(tuftT * (float)max((int)tuftCount - 1, 0)), tuftCount - 1u);
|
||||
float pigmentPeak = 0.0;
|
||||
float pigmentFlow = 0.0;
|
||||
|
||||
[unroll]
|
||||
for (int sampleDelta = -3; sampleDelta <= 3; sampleDelta += 1)
|
||||
{
|
||||
int rawSampleIndex = (int)centerSample + sampleDelta;
|
||||
uint sampleStrokeIndex = strokeIndex;
|
||||
BokushoBrushStroke sampleStroke = stroke;
|
||||
int resolvedSampleIndex = clamp(rawSampleIndex, 0, (int)sampleCount - 1);
|
||||
uint sampleIndex = min((uint)max(resolvedSampleIndex, 0), sampleCount - 1u);
|
||||
float sampleT = (float)sampleIndex / max((float)(sampleCount - 1u), 1.0);
|
||||
float2 sampleTangent = bokushoStrokeTangent(sampleStroke, sampleT);
|
||||
float2 sampleNormal = float2(-sampleTangent.y, sampleTangent.x);
|
||||
|
||||
[unroll]
|
||||
for (int tuftDelta = -2; tuftDelta <= 2; tuftDelta += 1)
|
||||
{
|
||||
uint tuftIndex = min((uint)max((int)centerTuft + tuftDelta, 0), tuftCount - 1u);
|
||||
float4 tip = bokushoTipSample(sampleStrokeIndex, sampleIndex, tuftIndex);
|
||||
uint previousStrokeIndex = sampleStrokeIndex;
|
||||
int previousSampleIndexValue = max((int)sampleIndex - 1, 0);
|
||||
uint previousSampleIndex = min((uint)max(previousSampleIndexValue, 0), sampleCount - 1u);
|
||||
float4 previousTip = bokushoTipSample(previousStrokeIndex, previousSampleIndex, tuftIndex);
|
||||
float2 sweep = tip.xy - previousTip.xy;
|
||||
float sweepLengthSquared = dot(sweep, sweep);
|
||||
float sweepT = sweepLengthSquared <= 0.000001 ? 1.0 : saturate(dot(world - previousTip.xy, sweep) / sweepLengthSquared);
|
||||
float2 contactPoint = previousTip.xy + sweep * sweepT;
|
||||
float sweepDistance = length(world - contactPoint);
|
||||
float sweepContact = smoothstep(1.0, 0.0, sweepDistance / max(max(tip.z, previousTip.z) * 1.04, 0.001));
|
||||
float2 delta = world - contactPoint;
|
||||
float normalDistance = dot(delta, sampleNormal) / max(tip.z, 0.001);
|
||||
float tangentDistance = dot(delta, sampleTangent) / max(tip.w, 0.001);
|
||||
float ellipse = sqrt(normalDistance * normalDistance + tangentDistance * tangentDistance);
|
||||
float tipContact = max(smoothstep(1.0, 0.0, ellipse), sweepContact * 0.86);
|
||||
float longitudinalGate = smoothstep(1.0, 0.0, abs(tangentDistance) * 0.62);
|
||||
float pigment = BokushoCanvasField[((sampleStrokeIndex * tuftCount) + tuftIndex) * sampleCount + sampleIndex];
|
||||
float contribution = pigment
|
||||
* tipContact
|
||||
* (0.006 + contactCore * 0.994)
|
||||
* (0.06 + longitudinalGate * 0.94)
|
||||
* (1.0 - abs((float)sampleDelta) * 0.070)
|
||||
* (0.82 + sweepContact * 0.24);
|
||||
pigmentPeak = max(pigmentPeak, contribution);
|
||||
pigmentFlow += contribution;
|
||||
}
|
||||
}
|
||||
|
||||
float decisiveInk = saturate((pigmentPeak - 0.012) * 2.25 + pigmentFlow * 0.002);
|
||||
return decisiveInk * (0.44 + pressure * 0.36 + saturate(bokushoMaterial.z * 0.5) * 0.20);
|
||||
}
|
||||
|
||||
float bokushoPageHeight(float2 world)
|
||||
{
|
||||
if (bokushoShape.w > 0.5)
|
||||
{
|
||||
float pageSize = 512.0;
|
||||
float2 uv = (world - viewCenter) / max(viewRadius, 0.001) * 0.5 + 0.5;
|
||||
int2 pixel = int2(floor(uv * (pageSize - 1.0) + 0.5));
|
||||
if (all(pixel >= int2(0, 0)) && all(pixel < int2((int)pageSize, (int)pageSize)))
|
||||
{
|
||||
uint densityValue = BokushoPageDensityField[(uint)(pixel.y * (int)pageSize + pixel.x)];
|
||||
float density = (float)densityValue / BOKUSHO_PAGE_DENSITY_SCALE;
|
||||
return saturate(1.0 - exp(-density));
|
||||
}
|
||||
}
|
||||
|
||||
uint strokeCount = min(max((uint)round(bokushoShape.w), 0u), 64u);
|
||||
float height = 0.0;
|
||||
[loop]
|
||||
for (uint strokeIndex = 0u; strokeIndex < strokeCount; strokeIndex += 1u)
|
||||
{
|
||||
height += bokushoStrokePageHeight(world, BokushoBrushStrokes[strokeIndex], strokeIndex);
|
||||
}
|
||||
|
||||
return saturate(height);
|
||||
}
|
||||
|
||||
float powerPulse(float distanceValue, float radius, float power)
|
||||
{
|
||||
float normalized = saturate(distanceValue / max(radius, 0.001));
|
||||
|
|
@ -98,7 +320,7 @@ float D3D12HeightFieldBasePS(VertexOut input) : SV_Target
|
|||
float2 world = viewWorld(saturate(input.uv));
|
||||
float slow = sin((world.x * 0.08 + world.y * 0.06) + timeSeconds * 0.27)
|
||||
* sin((world.x * -0.04 + world.y * 0.07) - timeSeconds * 0.19) * 0.035;
|
||||
return slow;
|
||||
return slow + bokushoPageHeight(world) * 5.0;
|
||||
}
|
||||
|
||||
BrushVertexOut D3D12HeightFieldBrushVS(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
struct PageInput { float4 direction_radius; float4 sampling; };
|
||||
struct PageOutput { float4 height_gradient; float4 masks; };
|
||||
StructuredBuffer<PageInput> page_inputs : register(t0);
|
||||
RWStructuredBuffer<PageOutput> page_outputs : register(u0);
|
||||
cbuffer PageConstants : register(b0) { uint page_sample_count; }
|
||||
[numthreads(64,1,1)]
|
||||
void D3D12ZyphosTerrainPageCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if(id.x>=page_sample_count)return;
|
||||
page_outputs[id.x].height_gradient=0;
|
||||
page_outputs[id.x].masks=float4(0,0,0,1);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
|
||||
struct PageOutput { float4 height_gradient; float4 masks; };
|
||||
struct PageSummary { float4 bounds; float4 metadata; };
|
||||
StructuredBuffer<PageOutput> summary_inputs : register(t0);
|
||||
RWStructuredBuffer<PageSummary> summary_outputs : register(u0);
|
||||
cbuffer PageConstants : register(b0) { uint page_sample_count; uint page_sample_offset; uint page_summary_index; }
|
||||
|
||||
[numthreads(1,1,1)]
|
||||
void D3D12ZyphosTerrainPageSummaryCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if (id.x != 0 || page_sample_count == 0) return;
|
||||
float4 summary = gamecult_geometry_planetary_summary_empty();
|
||||
[loop] for (uint index = 0; index < page_sample_count; index++)
|
||||
{
|
||||
PageOutput sample = summary_inputs[page_sample_offset + index];
|
||||
summary = gamecult_geometry_planetary_summary_accumulate(summary, sample.height_gradient, sample.masks.w);
|
||||
}
|
||||
summary_outputs[page_summary_index].bounds = summary;
|
||||
summary_outputs[page_summary_index].metadata = float4(page_sample_count, page_sample_offset, page_summary_index, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#include "D3D12SdfCommon.hlsli"
|
||||
|
||||
struct PlanetaryPatchVertexOut { float4 position:SV_Position; };
|
||||
PlanetaryPatchVertexOut D3D12ZyphosTerrainPatchVS(uint vertexId:SV_VertexID,uint instanceId:SV_InstanceID)
|
||||
{
|
||||
PlanetaryPatchVertexOut output; output.position=float4(-2,-2,1,1); return output;
|
||||
}
|
||||
SceneOut D3D12ZyphosTerrainPatchPS(PlanetaryPatchVertexOut input)
|
||||
{
|
||||
discard; return (SceneOut)0;
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
|
||||
struct PlanetaryTopologyInput
|
||||
{
|
||||
float4 direction_face;
|
||||
float4 coordinate_mode;
|
||||
float4 projection_parameters;
|
||||
};
|
||||
|
||||
struct PlanetaryTopologyOutput
|
||||
{
|
||||
float4 direction_face;
|
||||
float4 coordinate_valid;
|
||||
};
|
||||
|
||||
StructuredBuffer<PlanetaryTopologyInput> topology_inputs : register(t0);
|
||||
RWStructuredBuffer<PlanetaryTopologyOutput> topology_outputs : register(u0);
|
||||
cbuffer PlanetaryTopologyConstants : register(b0) { uint topology_input_count; }
|
||||
|
||||
[numthreads(64, 1, 1)]
|
||||
void D3D12PlanetaryTopologyParityCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if (id.x >= topology_input_count) return;
|
||||
PlanetaryTopologyInput input = topology_inputs[id.x];
|
||||
PlanetaryTopologyOutput output = (PlanetaryTopologyOutput)0;
|
||||
if (input.coordinate_mode.z < 0.5)
|
||||
{
|
||||
int face = (int)input.direction_face.w;
|
||||
output.direction_face = float4(gamecult_geometry_planetary_face_direction(face, input.coordinate_mode.xy), (float)face);
|
||||
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 1.5)
|
||||
{
|
||||
int face;
|
||||
float2 coordinate = gamecult_geometry_planetary_face_coordinate(input.direction_face.xyz, face);
|
||||
output.direction_face = float4(normalize(input.direction_face.xyz), (float)face);
|
||||
output.coordinate_valid = float4(coordinate, 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 2.5)
|
||||
{
|
||||
output.direction_face = float4(normalize(input.direction_face.xyz), 0.0);
|
||||
output.coordinate_valid = float4(gamecult_geometry_planetary_equirectangular_forward(input.direction_face.xyz), 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 3.5)
|
||||
{
|
||||
output.direction_face = float4(gamecult_geometry_planetary_equirectangular_inverse(input.coordinate_mode.xy), 0.0);
|
||||
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 4.5)
|
||||
{
|
||||
output.direction_face = float4(normalize(input.direction_face.xyz), 0.0);
|
||||
output.coordinate_valid = float4(gamecult_geometry_planetary_equal_earth_forward(input.direction_face.xyz), 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 5.5)
|
||||
{
|
||||
output.direction_face = float4(gamecult_geometry_planetary_equal_earth_inverse(input.coordinate_mode.xy), 0.0);
|
||||
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
|
||||
}
|
||||
else if (input.coordinate_mode.z < 6.5)
|
||||
{
|
||||
float2 coordinate;
|
||||
bool valid=gamecult_geometry_planetary_projection_forward(input.direction_face.xyz,(int)input.coordinate_mode.w,input.projection_parameters.xyz,coordinate);
|
||||
output.direction_face=float4(normalize(input.direction_face.xyz),0);
|
||||
output.coordinate_valid=float4(coordinate,0,valid?1.0:0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
float3 direction;
|
||||
bool valid=gamecult_geometry_planetary_projection_inverse(input.coordinate_mode.xy,(int)input.coordinate_mode.w,input.projection_parameters.xyz,direction);
|
||||
output.direction_face=float4(direction,0);
|
||||
output.coordinate_valid=float4(input.coordinate_mode.xy,0,valid?1.0:0.0);
|
||||
}
|
||||
topology_outputs[id.x] = output;
|
||||
}
|
||||
292
src/Aquarium.Engine/Render/Shaders/D3D12PointCloud.hlsl
Normal file
292
src/Aquarium.Engine/Render/Shaders/D3D12PointCloud.hlsl
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
cbuffer AquariumFrame : register(b0)
|
||||
{
|
||||
float2 resolution;
|
||||
float timeSeconds;
|
||||
float viewRadius;
|
||||
float3 cameraPosition;
|
||||
float farDistance;
|
||||
float3 cameraTarget;
|
||||
float sceneFlags;
|
||||
float2 viewCenter;
|
||||
float frameIndex;
|
||||
float previousTimeSeconds;
|
||||
float3 previousCameraPosition;
|
||||
float previousViewRadius;
|
||||
float3 previousCameraTarget;
|
||||
float previousSceneFlags;
|
||||
float2 previousViewCenter;
|
||||
float2 jitterPixels;
|
||||
float2 previousJitterPixels;
|
||||
float renderDebugMode;
|
||||
float exposure;
|
||||
float bloomIntensity;
|
||||
float bloomVeilIntensity;
|
||||
float4 cursorWorlds;
|
||||
float4 temporalGaussianInfo;
|
||||
float4 cameraFrustumXy;
|
||||
float4 cameraFrustumZ;
|
||||
};
|
||||
|
||||
cbuffer D3D12PointCloudConstants : register(b1)
|
||||
{
|
||||
float4 sourceShape;
|
||||
float4 projection;
|
||||
float4 depthRange;
|
||||
};
|
||||
|
||||
static const float FIELD_ID_POINT_CLOUD = 71.0;
|
||||
|
||||
struct PointCloudVertex
|
||||
{
|
||||
float3 position;
|
||||
float3 normal;
|
||||
float2 uv;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
struct FractalSdfSplat
|
||||
{
|
||||
float4 centerRadius;
|
||||
float4 orientation;
|
||||
float4 radiiFalloff;
|
||||
float4 materialConfidence;
|
||||
float4 key;
|
||||
};
|
||||
|
||||
struct SdfEnvelopeReservoir
|
||||
{
|
||||
float4 centerRadius;
|
||||
float4 radiiFalloff;
|
||||
float4 weightTargetCount;
|
||||
float4 validation;
|
||||
};
|
||||
|
||||
struct PbrMaterialReservoir
|
||||
{
|
||||
float4 baseColorRoughMetal;
|
||||
float4 normalVariance;
|
||||
float4 weightTargetCount;
|
||||
float4 validation;
|
||||
};
|
||||
|
||||
struct RadiosityReservoir
|
||||
{
|
||||
float4 radianceDistance;
|
||||
float4 directionOcclusion;
|
||||
float4 weightTargetCount;
|
||||
float4 validation;
|
||||
};
|
||||
|
||||
Texture2D<float> disparityTexture : register(t0);
|
||||
RWStructuredBuffer<PointCloudVertex> pointVertices : register(u0);
|
||||
RWStructuredBuffer<uint> pointIndices : register(u1);
|
||||
RWStructuredBuffer<FractalSdfSplat> surfaceSplats : register(u2);
|
||||
RWStructuredBuffer<SdfEnvelopeReservoir> surfaceSdfReservoirs : register(u3);
|
||||
RWStructuredBuffer<PbrMaterialReservoir> surfacePbrReservoirs : register(u4);
|
||||
RWStructuredBuffer<RadiosityReservoir> surfaceRadiosityReservoirs : register(u5);
|
||||
|
||||
struct VertexIn
|
||||
{
|
||||
float3 position : POSITION;
|
||||
float3 normal : NORMAL;
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 color : COLOR0;
|
||||
};
|
||||
|
||||
struct VertexOut
|
||||
{
|
||||
float4 position : SV_Position;
|
||||
float3 worldPosition : TEXCOORD0;
|
||||
float3 normal : TEXCOORD1;
|
||||
float2 uv : TEXCOORD2;
|
||||
float4 color : COLOR0;
|
||||
};
|
||||
|
||||
struct SceneOut
|
||||
{
|
||||
float4 colorTravel : SV_Target0;
|
||||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
float DepthFromDisparity(float disparity)
|
||||
{
|
||||
float minDepth = max(0.001, depthRange.x);
|
||||
float maxDepth = max(minDepth + 0.001, depthRange.y);
|
||||
return disparity > 0.0001
|
||||
? saturate(projection.x / max(disparity, 0.0001)) * (maxDepth - minDepth) + minDepth
|
||||
: maxDepth;
|
||||
}
|
||||
|
||||
void cameraBasis(float3 camera, float3 target, out float3 forward, out float3 right, out float3 up)
|
||||
{
|
||||
forward = normalize(target - camera);
|
||||
float3 worldUp = abs(forward.y) > 0.96 ? float3(0.0, 0.0, 1.0) : float3(0.0, 1.0, 0.0);
|
||||
right = normalize(cross(worldUp, forward));
|
||||
up = normalize(cross(forward, right));
|
||||
}
|
||||
|
||||
float3 PositionFromPixel(uint2 pixel, float depth)
|
||||
{
|
||||
float focalPixels = max(1.0, projection.y);
|
||||
float principalX = projection.z;
|
||||
float principalY = projection.w;
|
||||
float minDepth = max(0.001, depthRange.x);
|
||||
float maxDepth = max(minDepth + 0.001, depthRange.y);
|
||||
float lateralDisplayScale = max(1.0, depthRange.z);
|
||||
float forwardDisplayAnchor = depthRange.w > 0.0 ? depthRange.w : (minDepth + maxDepth) * 0.5;
|
||||
float3 cameraLocal = float3(
|
||||
((float)pixel.x - principalX) / focalPixels * depth,
|
||||
-((float)pixel.y - principalY) / focalPixels * depth,
|
||||
depth);
|
||||
float3 forward;
|
||||
float3 right;
|
||||
float3 up;
|
||||
cameraBasis(cameraPosition, cameraTarget, forward, right, up);
|
||||
return cameraTarget +
|
||||
right * cameraLocal.x * lateralDisplayScale +
|
||||
up * cameraLocal.y * lateralDisplayScale +
|
||||
forward * (cameraLocal.z - forwardDisplayAnchor);
|
||||
}
|
||||
|
||||
float DisparityAt(int2 pixel, uint width, uint height)
|
||||
{
|
||||
pixel.x = clamp(pixel.x, 0, (int)width - 1);
|
||||
pixel.y = clamp(pixel.y, 0, (int)height - 1);
|
||||
return disparityTexture.Load(int3(pixel, 0));
|
||||
}
|
||||
|
||||
float Hash01(uint value)
|
||||
{
|
||||
value ^= value >> 16;
|
||||
value *= 0x7feb352du;
|
||||
value ^= value >> 15;
|
||||
value *= 0x846ca68bu;
|
||||
value ^= value >> 16;
|
||||
return (float)(value & 0x00ffffffu) / 16777216.0;
|
||||
}
|
||||
|
||||
float4 projectWorld(float3 worldPosition)
|
||||
{
|
||||
float3 forward;
|
||||
float3 right;
|
||||
float3 up;
|
||||
cameraBasis(cameraPosition, cameraTarget, forward, right, up);
|
||||
float3 relative = worldPosition - cameraPosition;
|
||||
float3 view = float3(dot(relative, right), dot(relative, up), dot(relative, forward));
|
||||
float z = max(view.z, 0.0001);
|
||||
float2 slope = view.xy / z;
|
||||
float2 frustumMin = float2(cameraFrustumXy.x, cameraFrustumXy.z);
|
||||
float2 frustumMax = float2(cameraFrustumXy.y, cameraFrustumXy.w);
|
||||
float2 uv = (slope - frustumMin) / max(frustumMax - frustumMin, float2(0.0001, 0.0001));
|
||||
return float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), saturate(z / max(farDistance, 0.0001)), 1.0);
|
||||
}
|
||||
|
||||
[numthreads(128, 1, 1)]
|
||||
void D3D12PointCloudFromDisparityCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
uint pointIndex = dispatchThreadId.x;
|
||||
uint pointCount = (uint)max(1.0, sourceShape.z);
|
||||
if (pointIndex >= pointCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint width = (uint)max(1.0, sourceShape.x);
|
||||
uint height = (uint)max(1.0, sourceShape.y);
|
||||
uint stride = (uint)max(1.0, sourceShape.w);
|
||||
uint sampledWidth = max(1, (width + stride - 1) / stride);
|
||||
uint x = (pointIndex % sampledWidth) * stride;
|
||||
uint y = (pointIndex / sampledWidth) * stride;
|
||||
x = min(x, width - 1);
|
||||
y = min(y, height - 1);
|
||||
|
||||
float disparity = disparityTexture.Load(int3(x, y, 0));
|
||||
float confidence = disparity > 0.0001 ? 1.0 : 0.0;
|
||||
float depth = DepthFromDisparity(disparity);
|
||||
|
||||
float3 position = PositionFromPixel(uint2(x, y), depth);
|
||||
float minDepth = max(0.001, depthRange.x);
|
||||
float maxDepth = max(minDepth + 0.001, depthRange.y);
|
||||
float normalizedDepth = saturate((depth - minDepth) / max(maxDepth - minDepth, 0.0001));
|
||||
float leftDepth = DepthFromDisparity(DisparityAt(int2((int)x - (int)stride, (int)y), width, height));
|
||||
float rightDepth = DepthFromDisparity(DisparityAt(int2((int)x + (int)stride, (int)y), width, height));
|
||||
float upDepth = DepthFromDisparity(DisparityAt(int2((int)x, (int)y - (int)stride), width, height));
|
||||
float downDepth = DepthFromDisparity(DisparityAt(int2((int)x, (int)y + (int)stride), width, height));
|
||||
float3 leftPosition = PositionFromPixel(uint2(max((int)x - (int)stride, 0), y), leftDepth);
|
||||
float3 rightPosition = PositionFromPixel(uint2(min(x + stride, width - 1), y), rightDepth);
|
||||
float3 upPosition = PositionFromPixel(uint2(x, max((int)y - (int)stride, 0)), upDepth);
|
||||
float3 downPosition = PositionFromPixel(uint2(x, min(y + stride, height - 1)), downDepth);
|
||||
float3 normal = normalize(cross(rightPosition - leftPosition, downPosition - upPosition));
|
||||
normal = dot(normal, normal) > 0.25 ? normal : float3(0.0, 0.0, -1.0);
|
||||
float curvature = saturate((abs(leftDepth + rightDepth - 2.0 * depth) + abs(upDepth + downDepth - 2.0 * depth)) * 4.0);
|
||||
|
||||
PointCloudVertex vertex;
|
||||
vertex.position = position;
|
||||
vertex.normal = normal;
|
||||
vertex.uv = float2((float)x / max(1.0, (float)(width - 1)), (float)y / max(1.0, (float)(height - 1)));
|
||||
vertex.color = confidence > 0.0
|
||||
? float4(lerp(float3(0.2, 0.75, 1.0), float3(1.0, 0.95, 0.35), normalizedDepth), 1.0)
|
||||
: float4(0.08, 0.14, 0.22, 0.18);
|
||||
pointVertices[pointIndex] = vertex;
|
||||
pointIndices[pointIndex] = pointIndex;
|
||||
|
||||
float reservoirConfidence = confidence;
|
||||
float sampleRadius = lerp(0.080, 0.035, saturate(confidence)) * (1.0 + curvature * 2.5);
|
||||
FractalSdfSplat splat;
|
||||
splat.centerRadius = float4(position, sampleRadius);
|
||||
splat.orientation = float4(normal, 1.0);
|
||||
splat.radiiFalloff = float4(sampleRadius, sampleRadius * (1.0 + curvature), sampleRadius * 0.35, 3.0 + curvature * 4.0);
|
||||
splat.materialConfidence = float4(saturate(1.0 - normalizedDepth), 1.0, 1.0, reservoirConfidence);
|
||||
splat.key = float4((float)pointIndex, frameIndex, curvature, Hash01(pointIndex + (uint)frameIndex * 1664525u));
|
||||
surfaceSplats[pointIndex] = splat;
|
||||
|
||||
SdfEnvelopeReservoir sdf;
|
||||
sdf.centerRadius = splat.centerRadius;
|
||||
sdf.radiiFalloff = splat.radiiFalloff;
|
||||
sdf.weightTargetCount = float4(reservoirConfidence, reservoirConfidence, 1.0, reservoirConfidence);
|
||||
sdf.validation = float4(reservoirConfidence, frameIndex, curvature, 1.0);
|
||||
surfaceSdfReservoirs[pointIndex] = sdf;
|
||||
|
||||
PbrMaterialReservoir pbr;
|
||||
pbr.baseColorRoughMetal = float4(vertex.color.rgb, 0.38 + curvature * 0.28);
|
||||
pbr.normalVariance = float4(normal, curvature);
|
||||
pbr.weightTargetCount = sdf.weightTargetCount;
|
||||
pbr.validation = sdf.validation;
|
||||
surfacePbrReservoirs[pointIndex] = pbr;
|
||||
|
||||
RadiosityReservoir radiosity;
|
||||
radiosity.radianceDistance = float4(vertex.color.rgb * 0.18, depth);
|
||||
radiosity.directionOcclusion = float4(normalize(-normal + 0.01), 0.35 + confidence * 0.45);
|
||||
radiosity.weightTargetCount = sdf.weightTargetCount;
|
||||
radiosity.validation = sdf.validation;
|
||||
surfaceRadiosityReservoirs[pointIndex] = radiosity;
|
||||
}
|
||||
|
||||
VertexOut D3D12PointCloudVS(VertexIn input)
|
||||
{
|
||||
VertexOut output;
|
||||
output.position = projectWorld(input.position);
|
||||
output.worldPosition = input.position;
|
||||
output.normal = input.normal;
|
||||
output.uv = input.uv;
|
||||
output.color = input.color;
|
||||
return output;
|
||||
}
|
||||
|
||||
SceneOut D3D12PointCloudPS(VertexOut input)
|
||||
{
|
||||
SceneOut output;
|
||||
float travel = length(input.worldPosition - cameraPosition);
|
||||
float alpha = saturate(input.color.a);
|
||||
output.colorTravel = float4(input.color.rgb * alpha, travel);
|
||||
output.metadata = float4(FIELD_ID_POINT_CLOUD, normalize(input.normal));
|
||||
output.control = float4(alpha, 1.0, 0.35, 0.0);
|
||||
output.reservoirGuide = float4(alpha, 0.0, alpha, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = input.position.z / input.position.w;
|
||||
return output;
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ Texture2D<float4> bloomTexture4 : register(t33);
|
|||
Texture2D<float4> bloomTexture5 : register(t34);
|
||||
Texture2D<float4> bloomTexture6 : register(t35);
|
||||
Texture2D<float4> bloomTexture7 : register(t36);
|
||||
Texture2D<float> sceneOverdrawTexture : register(t76);
|
||||
Texture2D<float4> currentReservoirGuideTexture : register(t26);
|
||||
Texture2D<float> blueNoiseTexture : register(t28);
|
||||
SamplerState sourceSampler : register(s0);
|
||||
|
|
@ -524,6 +525,113 @@ float3 presentColor(float3 scene, float2 uv, float bloomScale)
|
|||
return aces(exposedScene + bloomContribution);
|
||||
}
|
||||
|
||||
float reservoirGuideConfidence(float4 guide, float4 control)
|
||||
{
|
||||
float confidence = guide.x > 0.0 ? saturate(guide.x) : (control.w > 0.0 ? saturate(control.w / MAX_HISTORY_AGE) : 1.0);
|
||||
float validity = guide.z > 0.0 ? saturate(guide.z) : 1.0;
|
||||
return confidence * validity;
|
||||
}
|
||||
|
||||
float reservoirReconstructionWeight(
|
||||
float4 centerColorTravel,
|
||||
float4 centerMetadata,
|
||||
float4 centerControl,
|
||||
float4 centerGuide,
|
||||
float4 sampleColorTravel,
|
||||
float4 sampleMetadata,
|
||||
float4 sampleControl,
|
||||
float4 sampleGuide,
|
||||
float2 offset)
|
||||
{
|
||||
float centerTravel = centerColorTravel.w;
|
||||
float sampleTravel = sampleColorTravel.w;
|
||||
float travelTolerance = max(0.05, max(centerTravel, 0.0) * 0.02);
|
||||
float travelWeight = 1.0 - smoothstep(travelTolerance, travelTolerance * 4.0, abs(sampleTravel - centerTravel));
|
||||
float fieldWeight = abs(sampleMetadata.x - centerMetadata.x) < 0.001 ? 1.0 : 0.0;
|
||||
float normalWeight = 1.0;
|
||||
if (dot(centerMetadata.yzw, centerMetadata.yzw) > 0.01 &&
|
||||
dot(sampleMetadata.yzw, sampleMetadata.yzw) > 0.01)
|
||||
{
|
||||
normalWeight = smoothstep(0.62, 0.97, dot(normalize(centerMetadata.yzw), normalize(sampleMetadata.yzw)));
|
||||
}
|
||||
|
||||
float coverageWeight = 1.0 - smoothstep(0.12, 0.55, abs(saturate(sampleControl.x) - saturate(centerControl.x)));
|
||||
float detailWeight = 1.0 - smoothstep(0.08, 0.38, abs(saturate(sampleControl.z) - saturate(centerControl.z)));
|
||||
float colorWeight = 1.0 - smoothstep(0.20, 1.35, length(sampleColorTravel.rgb - centerColorTravel.rgb));
|
||||
float sampleConfidence = lerp(0.35, 1.0, reservoirGuideConfidence(sampleGuide, sampleControl));
|
||||
float distanceWeight = exp2(-dot(offset, offset) * 0.85);
|
||||
return fieldWeight * travelWeight * normalWeight * coverageWeight * detailWeight * colorWeight * sampleConfidence * distanceWeight;
|
||||
}
|
||||
|
||||
float3 reconstructReservoirColor(float2 uv, out float reconstructionMix)
|
||||
{
|
||||
uint sourceWidth;
|
||||
uint sourceHeight;
|
||||
sourceTexture.GetDimensions(sourceWidth, sourceHeight);
|
||||
uint2 dimensions = max(uint2(sourceWidth, sourceHeight), uint2(1, 1));
|
||||
float2 texel = 1.0 / (float2)dimensions;
|
||||
int2 centerPixel = clamp((int2)floor(uv * (float2)dimensions), int2(0, 0), (int2)dimensions - int2(1, 1));
|
||||
float2 centerUv = ((float2)centerPixel + 0.5) * texel;
|
||||
|
||||
float4 centerColorTravel = sourceTexture.SampleLevel(sourceSampler, uv, 0.0);
|
||||
float4 centerMetadata = currentSceneMetadataTexture.Load(int3(centerPixel, 0));
|
||||
float4 centerControl = currentSceneControlTexture.Load(int3(centerPixel, 0));
|
||||
float4 centerGuide = currentReservoirGuideTexture.Load(int3(centerPixel, 0));
|
||||
float centerConfidence = reservoirGuideConfidence(centerGuide, centerControl);
|
||||
float centerDetail = saturate(centerControl.z);
|
||||
float edgePreservation = 1.0 - smoothstep(0.28, 0.82, centerDetail);
|
||||
|
||||
float centerWeight = lerp(2.25, 1.15, saturate(1.0 - centerConfidence));
|
||||
float3 sumColor = centerColorTravel.rgb * centerWeight;
|
||||
float sumWeight = centerWeight;
|
||||
static const int2 offsets[8] =
|
||||
{
|
||||
int2(-1, 0),
|
||||
int2(1, 0),
|
||||
int2(0, -1),
|
||||
int2(0, 1),
|
||||
int2(-1, -1),
|
||||
int2(1, -1),
|
||||
int2(-1, 1),
|
||||
int2(1, 1)
|
||||
};
|
||||
|
||||
[unroll]
|
||||
for (int index = 0; index < 8; index++)
|
||||
{
|
||||
int2 offset = offsets[index];
|
||||
int2 samplePixel = clamp(centerPixel + offset, int2(0, 0), (int2)dimensions - int2(1, 1));
|
||||
float4 sampleColorTravel = sourceTexture.Load(int3(samplePixel, 0));
|
||||
float4 sampleMetadata = currentSceneMetadataTexture.Load(int3(samplePixel, 0));
|
||||
float4 sampleControl = currentSceneControlTexture.Load(int3(samplePixel, 0));
|
||||
float4 sampleGuide = currentReservoirGuideTexture.Load(int3(samplePixel, 0));
|
||||
float weight = reservoirReconstructionWeight(
|
||||
centerColorTravel,
|
||||
centerMetadata,
|
||||
centerControl,
|
||||
centerGuide,
|
||||
sampleColorTravel,
|
||||
sampleMetadata,
|
||||
sampleControl,
|
||||
sampleGuide,
|
||||
(float2)offset);
|
||||
sumColor += sampleColorTravel.rgb * weight;
|
||||
sumWeight += weight;
|
||||
}
|
||||
|
||||
float3 filtered = sumColor / max(sumWeight, 0.0001);
|
||||
float neighborAgreement = saturate((sumWeight - centerWeight) / 4.0);
|
||||
float blendNeed = saturate((1.0 - centerConfidence) * 0.35 + neighborAgreement * 0.10);
|
||||
float reservoirMaturity = smoothstep(0.5, 1.5, max(centerGuide.y, centerControl.w));
|
||||
float filterStrength = min(0.20, blendNeed * edgePreservation * reservoirMaturity);
|
||||
reconstructionMix = filterStrength;
|
||||
float3 reconstructed = lerp(centerColorTravel.rgb, filtered, filterStrength);
|
||||
float3 neighborhoodMin;
|
||||
float3 neighborhoodMax;
|
||||
currentNeighborhood(centerUv, neighborhoodMin, neighborhoodMax);
|
||||
return clamp(reconstructed, neighborhoodMin, neighborhoodMax);
|
||||
}
|
||||
|
||||
float blueNoiseAt(float2 uv, uint salt)
|
||||
{
|
||||
uint width;
|
||||
|
|
@ -1119,15 +1227,29 @@ float4 reservoirDebugOrColor(FieldReservoirSample candidate, uint2 pixel)
|
|||
return float4(fieldReservoirResolvedColor(candidate, farDistance), candidate.colorTravel.w);
|
||||
}
|
||||
|
||||
float3 overdrawHeat(float overdraw)
|
||||
{
|
||||
if (overdraw <= 0.5)
|
||||
{
|
||||
return float3(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
float hot = saturate(log2(max(overdraw, 1.0)) / 4.0);
|
||||
float3 low = lerp(float3(0.0, 0.22, 0.95), float3(0.0, 0.95, 0.35), saturate(overdraw - 1.0));
|
||||
float3 mid = lerp(low, float3(1.0, 0.92, 0.05), saturate((overdraw - 2.0) / 2.0));
|
||||
return lerp(mid, float3(1.0, 0.05, 0.35), hot);
|
||||
}
|
||||
|
||||
FieldReservoirResolveOut D3D12FieldReservoirResolvePS(VertexOut input)
|
||||
{
|
||||
uint2 pixel = (uint2)pixelFromUv(input.uv);
|
||||
FieldReservoirSample current = currentFrameReservoirSample(pixel, input.uv);
|
||||
|
||||
FieldReservoirResolveOut output;
|
||||
output.colorTravel = sourceTexture.Load(int3(pixel, 0));
|
||||
output.metadata = loadCurrentMetadata(input.uv);
|
||||
output.control = loadCurrentControl(input.uv);
|
||||
output.reservoirGuide = loadCurrentReservoirGuide(input.uv);
|
||||
output.colorTravel = reservoirDebugOrColor(current, pixel);
|
||||
output.metadata = current.metadata;
|
||||
output.control = current.control;
|
||||
output.reservoirGuide = current.guide;
|
||||
return output;
|
||||
}
|
||||
|
||||
|
|
@ -1175,7 +1297,13 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
|
|||
float currentReservoirConfidence = currentReservoirGuide.x > 0.0 ? saturate(currentReservoirGuide.x) : 1.0;
|
||||
float reservoirSampleAge = max(currentReservoirGuide.y, 0.0);
|
||||
float currentReservoirDomainValidity = currentReservoirGuide.z > 0.0 ? saturate(currentReservoirGuide.z) : 1.0;
|
||||
float reconstructionMix = 0.0;
|
||||
float3 resolved = resolvedSample.rgb;
|
||||
if (renderDebugMode < 0.5 || (renderDebugMode >= 20.5 && renderDebugMode < 21.5))
|
||||
{
|
||||
resolved = reconstructReservoirColor(input.uv, reconstructionMix);
|
||||
}
|
||||
|
||||
float3 finalColor = presentColor(resolved, input.uv, 1.0);
|
||||
if (renderDebugMode > 0.5 && renderDebugMode < 1.5)
|
||||
{
|
||||
|
|
@ -1223,6 +1351,11 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
|
|||
{
|
||||
finalColor = float3(currentReservoirConfidence, saturate(reservoirSampleAge / MAX_HISTORY_AGE), currentReservoirDomainValidity);
|
||||
}
|
||||
else if (renderDebugMode >= 20.5 && renderDebugMode < 21.5)
|
||||
{
|
||||
float overdraw = sceneOverdrawTexture.SampleLevel(sourceSampler, input.uv, 0.0);
|
||||
finalColor = overdrawHeat(overdraw);
|
||||
}
|
||||
else if (renderDebugMode >= 12.5 && renderDebugMode < 18.5)
|
||||
{
|
||||
finalColor = saturate(resolved);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
cbuffer AquariumFrame : register(b0)
|
||||
{
|
||||
float2 resolution;
|
||||
float timeSeconds;
|
||||
float viewRadius;
|
||||
float3 cameraPosition;
|
||||
float farDistance;
|
||||
float3 cameraTarget;
|
||||
float sceneFlags;
|
||||
float2 viewCenter;
|
||||
float frameIndex;
|
||||
float previousTimeSeconds;
|
||||
float3 previousCameraPosition;
|
||||
float previousViewRadius;
|
||||
float3 previousCameraTarget;
|
||||
float previousSceneFlags;
|
||||
float2 previousViewCenter;
|
||||
float2 jitterPixels;
|
||||
float2 previousJitterPixels;
|
||||
float renderDebugMode;
|
||||
float exposure;
|
||||
float bloomIntensity;
|
||||
float bloomVeilIntensity;
|
||||
float4 cursorWorlds;
|
||||
float4 temporalGaussianInfo;
|
||||
float4 cameraFrustumXy;
|
||||
float4 cameraFrustumZ;
|
||||
float4 gpuFusionInfo;
|
||||
float4 fractalReservoirInfo;
|
||||
float4 fractalReservoirFrame;
|
||||
float4 reservoirBudgetInfo;
|
||||
};
|
||||
|
||||
Texture2D<float4> sourceTexture : register(t0);
|
||||
Texture2D<float4> currentSceneMetadataTexture : register(t5);
|
||||
Texture2D<float4> currentSceneControlTexture : register(t7);
|
||||
Texture2D<float4> currentReservoirGuideTexture : register(t26);
|
||||
|
||||
#include "D3D12FieldReservoir.hlsli"
|
||||
|
||||
StructuredBuffer<FieldReservoirSample> fieldReservoirCandidates : register(t45);
|
||||
RWStructuredBuffer<FieldReservoirSample> reservoirHistoryWrite : register(u22);
|
||||
RWTexture2D<float4> reservoirResolvedTexture : register(u23);
|
||||
|
||||
float4 loadTexturePixel(Texture2D<float4> source, uint2 pixel)
|
||||
{
|
||||
return source.Load(int3(pixel, 0));
|
||||
}
|
||||
|
||||
FieldReservoirSample sceneFieldReservoirSample(uint2 pixel, float2 uv)
|
||||
{
|
||||
float4 colorTravel = loadTexturePixel(sourceTexture, pixel);
|
||||
float4 metadata = loadTexturePixel(currentSceneMetadataTexture, pixel);
|
||||
float4 control = loadTexturePixel(currentSceneControlTexture, pixel);
|
||||
float4 guide = loadTexturePixel(currentReservoirGuideTexture, pixel);
|
||||
float target = fieldReservoirDefaultTarget(colorTravel, control, guide);
|
||||
return makeFieldReservoirSample(
|
||||
colorTravel,
|
||||
metadata,
|
||||
control,
|
||||
guide,
|
||||
float4(uv, colorTravel.w, 0.0),
|
||||
float4(uv, uv),
|
||||
float4(1.0, 1.0, FieldDomainKindScreen, FieldShiftKindNone),
|
||||
target,
|
||||
1.0,
|
||||
1.0,
|
||||
FieldProposalKindDeterministicStructural);
|
||||
}
|
||||
|
||||
FieldReservoirSample currentFrameReservoirSample(uint2 pixel, float2 uv)
|
||||
{
|
||||
uint width = (uint)max(resolution.x, 1.0);
|
||||
uint baseIndex = (pixel.y * width + pixel.x) * FieldReservoirSlotsPerPixel;
|
||||
FieldReservoirSample current = sceneFieldReservoirSample(pixel, uv);
|
||||
FieldReservoirSample candidate = fieldReservoirCandidates[baseIndex + FieldReservoirRowCurrent];
|
||||
current = mergeFieldReservoirVisibilityProposals(
|
||||
current,
|
||||
candidate,
|
||||
fieldReservoirRandom01(pixel, (uint)frameIndex, 31u),
|
||||
farDistance);
|
||||
current.guide.y = 0.0;
|
||||
current.guide.w = 0.0;
|
||||
return current;
|
||||
}
|
||||
|
||||
float4 reservoirHistoryColor(FieldReservoirSample sample)
|
||||
{
|
||||
if (!fieldReservoirSampleValid(sample, farDistance))
|
||||
{
|
||||
return float4(0.0, 0.0, 0.0, farDistance + 1.0);
|
||||
}
|
||||
|
||||
return float4(fieldReservoirResolvedColor(sample, farDistance), sample.colorTravel.w);
|
||||
}
|
||||
|
||||
[numthreads(8, 8, 1)]
|
||||
void D3D12ReservoirHistoryUpdateCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
uint2 dimensions = (uint2)max(resolution, float2(1.0, 1.0));
|
||||
if (dispatchThreadId.x >= dimensions.x || dispatchThreadId.y >= dimensions.y)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint2 pixel = dispatchThreadId.xy;
|
||||
float2 uv = (float2(pixel) + 0.5) / float2(dimensions);
|
||||
uint baseIndex = (pixel.y * dimensions.x + pixel.x) * FieldReservoirSlotsPerPixel;
|
||||
FieldReservoirSample current = currentFrameReservoirSample(pixel, uv);
|
||||
|
||||
reservoirHistoryWrite[baseIndex + FieldReservoirRowCurrent] = current;
|
||||
reservoirHistoryWrite[baseIndex + FieldReservoirRowTemporal] = current;
|
||||
reservoirHistoryWrite[baseIndex + FieldReservoirRowSpatial] = current;
|
||||
reservoirHistoryWrite[baseIndex + FieldReservoirRowFinal] = current;
|
||||
reservoirResolvedTexture[pixel] = reservoirHistoryColor(current);
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ SamplerState linearSampler : register(s0);
|
|||
|
||||
static const float FIELD_ID_HEIGHT_FIELD = 4.0;
|
||||
static const float PI = 3.14159265359;
|
||||
static const float HEIGHT_FIELD_TEXEL_COUNT = 128.0;
|
||||
static const float HEIGHT_FIELD_TEXEL_COUNT = 512.0;
|
||||
static const float SURFACE_FLAT_REFLECTION_MAX_LOD = 3.0;
|
||||
static const float BACKGROUND_PMREM_LOD = 3.0;
|
||||
static const float BACKGROUND_PMREM_CONE = 0.16;
|
||||
|
|
@ -55,6 +55,7 @@ struct SceneOut
|
|||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
|
|
@ -335,12 +336,18 @@ float3 fractalStarClusters(float3 direction)
|
|||
|
||||
float3 surfaceMirrorRadiance(float3 p, float3 direction, out float3 normal)
|
||||
{
|
||||
float height = terrainHeight(p.xy);
|
||||
float2 gradient = terrainGradient(p.xy);
|
||||
normal = normalize(float3(-gradient.x, -gradient.y, 1.0));
|
||||
float slope = length(gradient);
|
||||
float3 reflectionDirection = reflect(direction, normal);
|
||||
float flatness = 1.0 - smoothstep(SURFACE_FLAT_SLOPE_START, SURFACE_FLAT_SLOPE_END, length(gradient));
|
||||
float flatness = 1.0 - smoothstep(SURFACE_FLAT_SLOPE_START, SURFACE_FLAT_SLOPE_END, slope);
|
||||
float lod = flatness * SURFACE_FLAT_REFLECTION_MAX_LOD;
|
||||
return studioPmremConeSample(reflectionDirection, lod, flatness * 0.055);
|
||||
float3 reflected = studioPmremConeSample(reflectionDirection, lod, flatness * 0.055);
|
||||
float inkMass = saturate((height - 0.006) * 32.0 + smoothstep(0.03, 0.16, slope) * 0.42);
|
||||
float3 paper = float3(0.84, 0.75, 0.55) * (0.62 + flatness * 0.28);
|
||||
float3 ink = float3(0.010, 0.008, 0.006);
|
||||
return lerp(reflected * 0.18 + paper, ink, inkMass * 0.90);
|
||||
}
|
||||
|
||||
float3 backgroundRadiance(float3 direction)
|
||||
|
|
@ -408,6 +415,7 @@ SceneOut D3D12ScenePS(VertexOut input)
|
|||
output.metadata = float4(result.fieldId, result.normal);
|
||||
output.control = float4(result.coverage, result.stepCount / 72.0, 0.0, 0.0);
|
||||
output.reservoirGuide = float4(1.0, 0.0, 1.0, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(result.travel / max(farDistance, 0.001));
|
||||
return output;
|
||||
}
|
||||
|
|
@ -662,6 +670,7 @@ SceneOut D3D12SplinePS(SplineVertexOut input)
|
|||
output.metadata = float4(5000.0, closestT, sdf, radiusPx);
|
||||
output.control = float4(claimCoverage, coverage, saturate(radiusPx / 32.0), 0.0);
|
||||
output.reservoirGuide = float4(claimCoverage, 0.0, coverage, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(travel / max(farDistance, 0.0001));
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ struct SceneOut
|
|||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,19 @@
|
|||
#define SDF_TRACE_MAX_STEP_RADIUS_SCALE 0.012
|
||||
#endif
|
||||
|
||||
float3 sdfNormal(float3 p, int sdfIndex)
|
||||
#ifndef SDF_TRACE_BOUND_RADIUS
|
||||
#define SDF_TRACE_BOUND_RADIUS(sdfObject) max((sdfObject).centerRadius.w * 1.42, 0.001)
|
||||
#endif
|
||||
|
||||
#ifndef SDF_SURFACE_NORMAL
|
||||
#define SDF_SURFACE_NORMAL(p,sdfIndex) sdfFiniteDifferenceNormal(p,sdfIndex)
|
||||
#endif
|
||||
|
||||
#ifndef SDF_TRACE_FUNCTION
|
||||
#define SDF_TRACE_FUNCTION traceSdf
|
||||
#endif
|
||||
|
||||
float3 sdfFiniteDifferenceNormal(float3 p, int sdfIndex)
|
||||
{
|
||||
float epsilon = 0.006;
|
||||
float dx = sdfDistance(p + float3(epsilon, 0.0, 0.0), sdfIndex) - sdfDistance(p - float3(epsilon, 0.0, 0.0), sdfIndex);
|
||||
|
|
@ -43,14 +55,14 @@ bool refineSdfHit(float3 origin, float3 direction, int sdfIndex, float lowTravel
|
|||
travel = highTravel;
|
||||
float3 p = origin + direction * travel;
|
||||
surface = sdfSurface(p, sdfIndex);
|
||||
normal = sdfNormal(p, sdfIndex);
|
||||
normal = SDF_SURFACE_NORMAL(p, sdfIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool traceSdf(float3 origin, float3 direction, int sdfIndex, out float travel, out float3 normal, out SdfSurface surface, out float stepCount)
|
||||
{
|
||||
SdfObject sdfObject = sdfObjects[sdfIndex];
|
||||
float boundRadius = max(sdfObject.centerRadius.w * 1.42, 0.001);
|
||||
float boundRadius = SDF_TRACE_BOUND_RADIUS(sdfObject);
|
||||
if (!traceSphere(origin, direction, sdfObject.centerRadius.xyz, boundRadius, travel))
|
||||
{
|
||||
normal = 0.0;
|
||||
|
|
@ -96,7 +108,7 @@ bool traceSdf(float3 origin, float3 direction, int sdfIndex, out float travel, o
|
|||
}
|
||||
|
||||
surface = sdfSurface(p, sdfIndex);
|
||||
normal = sdfNormal(p, sdfIndex);
|
||||
normal = SDF_SURFACE_NORMAL(p, sdfIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +131,7 @@ SceneOut D3D12SdfProxyPS(SdfObjectProxyVertexOut input)
|
|||
float3 normal;
|
||||
SdfSurface surface;
|
||||
float stepCount;
|
||||
if (!traceSdf(cameraPosition, rayDirection, sdfIndex, travel, normal, surface, stepCount))
|
||||
if (!SDF_TRACE_FUNCTION(cameraPosition, rayDirection, sdfIndex, travel, normal, surface, stepCount))
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
|
@ -135,6 +147,7 @@ SceneOut D3D12SdfProxyPS(SdfObjectProxyVertexOut input)
|
|||
saturate(surface.temporalDetail),
|
||||
saturate(surface.reservoirConfidence));
|
||||
output.reservoirGuide = float4(saturate(surface.reservoirConfidence), 0.0, 1.0, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(travel / max(farDistance, 0.001));
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
69
src/Aquarium.Engine/Render/Shaders/D3D12StereoDepth.hlsl
Normal file
69
src/Aquarium.Engine/Render/Shaders/D3D12StereoDepth.hlsl
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
cbuffer D3D12StereoDepthConstants : register(b0)
|
||||
{
|
||||
float4 Shape; // width, height, minDisparity, disparityLevels
|
||||
float4 Match; // censusRadius, aggregationPathCount, p1, p2
|
||||
float4 Output; // invDisparityScale, reserved
|
||||
};
|
||||
|
||||
Texture2D<float2> PackedStereoIr : register(t28);
|
||||
RWTexture2D<float> DisparityOut : register(u0);
|
||||
|
||||
float PackedLeft(uint2 pixel)
|
||||
{
|
||||
return PackedStereoIr.Load(int3(pixel, 0)).r;
|
||||
}
|
||||
|
||||
float PackedRight(uint2 pixel)
|
||||
{
|
||||
const uint stereoWidth = max(1u, (uint)Shape.x);
|
||||
return PackedStereoIr.Load(int3(uint2(pixel.x + stereoWidth, pixel.y), 0)).r;
|
||||
}
|
||||
|
||||
[numthreads(8, 8, 1)]
|
||||
void D3D12PackedStereoDepthCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
const uint width = max(1u, (uint)Shape.x);
|
||||
const uint height = max(1u, (uint)Shape.y);
|
||||
const uint2 pixel = dispatchThreadId.xy;
|
||||
if (pixel.x >= width || pixel.y >= height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int minDisparity = max(0, (int)Shape.z);
|
||||
const int disparityLevels = max(1, min(256, (int)Shape.w));
|
||||
const int radius = max(1, min(4, (int)Match.x));
|
||||
const int maxDisparity = min(disparityLevels - 1, (int)pixel.x - radius - minDisparity);
|
||||
if (maxDisparity <= 0 || pixel.y < (uint)radius || pixel.y + (uint)radius >= height)
|
||||
{
|
||||
DisparityOut[pixel] = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
float bestCost = 1.0e20f;
|
||||
int bestDisparity = 0;
|
||||
[loop]
|
||||
for (int disparity = minDisparity; disparity <= maxDisparity; disparity++)
|
||||
{
|
||||
float cost = 0.0f;
|
||||
[loop]
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
{
|
||||
[loop]
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
const uint2 leftPixel = uint2((uint)((int)pixel.x + x), (uint)((int)pixel.y + y));
|
||||
const uint2 rightPixel = uint2((uint)((int)pixel.x + x - disparity), (uint)((int)pixel.y + y));
|
||||
cost += abs(PackedLeft(leftPixel) - PackedRight(rightPixel));
|
||||
}
|
||||
}
|
||||
|
||||
if (cost < bestCost)
|
||||
{
|
||||
bestCost = cost;
|
||||
bestDisparity = disparity;
|
||||
}
|
||||
}
|
||||
|
||||
DisparityOut[pixel] = (float)bestDisparity;
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ struct SceneOut
|
|||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
|
|
@ -185,6 +186,7 @@ SceneOut D3D12TemporalGaussianPS(TemporalGaussianVertexOut input)
|
|||
output.metadata = float4(FIELD_ID_TEMPORAL_GAUSSIAN_BASE + gaussian.previousCenterFieldId.w, normal);
|
||||
output.control = float4(opacity, saturate(gaussian.centerHistoryWeight.w), saturate(gaussian.shapePad.x / 8.0), 0.0);
|
||||
output.reservoirGuide = float4(saturate(gaussian.velocityConfidence.w), 0.0, 1.0, 0.0);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(travel / max(farDistance, 0.001));
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ struct TubeFieldSegment
|
|||
float4 tubeData;
|
||||
};
|
||||
|
||||
#include "CultMath/CultMath.hlsl"
|
||||
#include "D3D12FieldReservoir.hlsli"
|
||||
|
||||
cbuffer AquariumFrame : register(b0)
|
||||
|
|
@ -178,16 +179,6 @@ float NormalizedSampleWithOffset(uint logicalColumn, float x, int rollingOffset)
|
|||
}
|
||||
|
||||
|
||||
float Catmull(float p0, float p1, float p2, float p3, float t)
|
||||
{
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
return 0.5 * ((2.0 * p1) +
|
||||
(-p0 + p2) * t +
|
||||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
|
||||
}
|
||||
|
||||
float SampleCurve(uint logicalColumn, float x)
|
||||
{
|
||||
int i1 = (int)floor(x);
|
||||
|
|
@ -196,7 +187,7 @@ float SampleCurve(uint logicalColumn, float x)
|
|||
float p1 = NormalizedSample(logicalColumn, (float)i1);
|
||||
float p2 = NormalizedSample(logicalColumn, (float)(i1 + 1));
|
||||
float p3 = NormalizedSample(logicalColumn, (float)(i1 + 2));
|
||||
return saturate(Catmull(p0, p1, p2, p3, t));
|
||||
return saturate(cultmath_catmullrom(p0, p1, p2, p3, t));
|
||||
}
|
||||
|
||||
float SampleCurveWithOffset(uint logicalColumn, float x, int rollingOffset)
|
||||
|
|
@ -207,7 +198,7 @@ float SampleCurveWithOffset(uint logicalColumn, float x, int rollingOffset)
|
|||
float p1 = NormalizedSampleWithOffset(logicalColumn, (float)i1, rollingOffset);
|
||||
float p2 = NormalizedSampleWithOffset(logicalColumn, (float)(i1 + 1), rollingOffset);
|
||||
float p3 = NormalizedSampleWithOffset(logicalColumn, (float)(i1 + 2), rollingOffset);
|
||||
return saturate(Catmull(p0, p1, p2, p3, t));
|
||||
return saturate(cultmath_catmullrom(p0, p1, p2, p3, t));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -369,6 +360,7 @@ struct SceneOut
|
|||
float4 metadata : SV_Target1;
|
||||
float4 control : SV_Target2;
|
||||
float4 reservoirGuide : SV_Target3;
|
||||
float overdraw : SV_Target4;
|
||||
float depth : SV_Depth;
|
||||
};
|
||||
|
||||
|
|
@ -877,6 +869,7 @@ SceneOut D3D12TubeFieldPS(TubeFieldVertexOut input)
|
|||
output.metadata = float4(candidateFieldId, tubeNormal);
|
||||
output.control = float4(claimCoverage, supportCoverage, saturate(radiusWorld / max(viewRadius, 0.0001)), value);
|
||||
output.reservoirGuide = float4(claimCoverage, 0.0, supportCoverage, value);
|
||||
output.overdraw = 1.0;
|
||||
output.depth = saturate(travel / max(farDistance, 0.0001));
|
||||
float target = fieldReservoirDefaultTarget(output.colorTravel, output.control, output.reservoirGuide);
|
||||
float selectedColumn = round(input.tubeData.x);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,10 @@ internal sealed class DebugUi
|
|||
|
||||
public static DebugUi FromContract(AquariumUiPanel source)
|
||||
{
|
||||
var ui = new DebugUi(source.Title, source.Left, source.Top, source.Width, source.FadeWhenMouseDistant, [], () => 0, _ => { });
|
||||
var ui = new DebugUi(source.Title, source.Left, source.Top, source.Width, source.FadeWhenMouseDistant, [], () => 0, _ => { })
|
||||
{
|
||||
IsVisible = true
|
||||
};
|
||||
foreach (var control in source.Controls)
|
||||
{
|
||||
ui.AddContractControl(control);
|
||||
|
|
|
|||
16
src/Aquarium.EveSurface/Aquarium.EveSurface.csproj
Normal file
16
src/Aquarium.EveSurface/Aquarium.EveSurface.csproj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>Aquarium.EveSurface</AssemblyName>
|
||||
<RootNamespace>Aquarium.EveSurface</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
448
src/Aquarium.EveSurface/EveSurfaceRuntime.cs
Normal file
448
src/Aquarium.EveSurface/EveSurfaceRuntime.cs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
using System.Globalization;
|
||||
using System.Net.WebSockets;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Aquarium.Engine;
|
||||
using Aquarium.Engine.Audio;
|
||||
using Aquarium.Engine.Input;
|
||||
using Aquarium.Engine.Render;
|
||||
using Aquarium.Engine.Ui;
|
||||
|
||||
namespace Aquarium.EveSurface;
|
||||
|
||||
public sealed class EveSurfaceRuntime : IAquariumRuntime
|
||||
{
|
||||
private const string DefaultBrokerUrl = "ws://127.0.0.1:8795/eve/deck";
|
||||
private const string DefaultProviderId = "voidbot.swarm";
|
||||
private readonly object sync = new();
|
||||
private readonly string brokerUrl = Environment.GetEnvironmentVariable("FENSALIR_EVE_BROKER") ?? DefaultBrokerUrl;
|
||||
private readonly string providerId = Environment.GetEnvironmentVariable("FENSALIR_EVE_PROVIDER") ?? DefaultProviderId;
|
||||
private CancellationTokenSource? connectionLifetime;
|
||||
private Task? connectionTask;
|
||||
private EveSurfaceState state = EveSurfaceState.Empty;
|
||||
private string status = "not started";
|
||||
private string selectedNodeId = "";
|
||||
private float timeSeconds;
|
||||
|
||||
public AquariumRuntimeOptions Options { get; private set; }
|
||||
|
||||
public GraphicsSettings GraphicsSettings { get; set; } = GraphicsSettings.Default;
|
||||
|
||||
public AquariumRenderPlan RenderPlan { get; } = CreateRenderPlan();
|
||||
|
||||
public AquariumUiDocument Ui => ComposeUi();
|
||||
|
||||
public AquariumAudioDocument Audio { get; } = new();
|
||||
|
||||
public AquariumSynthDocument Synth { get; } = AquariumSynthDocument.Empty;
|
||||
|
||||
public AquariumFrame Frame => new(
|
||||
new ViewFrame(Vector2.Zero, 24.0f),
|
||||
new Vector3(0.0f, -10.0f, 7.0f),
|
||||
timeSeconds,
|
||||
Vector2.Zero,
|
||||
AquariumSceneState.Empty);
|
||||
|
||||
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input) => frame;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Console.WriteLine($"Eve surface client connecting to {brokerUrl} provider {providerId}.");
|
||||
connectionLifetime = new CancellationTokenSource();
|
||||
connectionTask = Task.Run(() => RunConnectionLoopAsync(connectionLifetime.Token));
|
||||
}
|
||||
|
||||
public void Update(float deltaSeconds, InputState input)
|
||||
{
|
||||
timeSeconds += Math.Max(deltaSeconds, 0.0f);
|
||||
}
|
||||
|
||||
public void FlushState()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
connectionLifetime?.Cancel();
|
||||
try
|
||||
{
|
||||
connectionTask?.Wait(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
}
|
||||
|
||||
connectionLifetime?.Dispose();
|
||||
}
|
||||
|
||||
internal void SetOptions(AquariumRuntimeOptions options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
private AquariumUiDocument ComposeUi()
|
||||
{
|
||||
EveSurfaceState snapshot;
|
||||
string connectionStatus;
|
||||
lock (sync)
|
||||
{
|
||||
snapshot = state;
|
||||
connectionStatus = status;
|
||||
if (string.IsNullOrWhiteSpace(selectedNodeId))
|
||||
{
|
||||
selectedNodeId = snapshot.SelectedNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
var nodes = snapshot.Nodes;
|
||||
var selected = nodes.FirstOrDefault(node => node.Id == selectedNodeId)
|
||||
?? nodes.FirstOrDefault(node => node.Id == snapshot.SelectedNodeId)
|
||||
?? nodes.FirstOrDefault();
|
||||
var ctb = nodes.Where(static node => string.Equals(node.Kind, "ctb-turn", StringComparison.OrdinalIgnoreCase)).Take(12).ToArray();
|
||||
var leaves = nodes.Where(static node => string.Equals(node.Kind, "state-leaf", StringComparison.OrdinalIgnoreCase)).Take(32).ToArray();
|
||||
var primaryNodes = ctb.Length > 0 ? ctb : nodes.Take(16).ToArray();
|
||||
var detailNodes = leaves.Length > 0 ? leaves : nodes.Skip(primaryNodes.Length).Take(32).ToArray();
|
||||
if (detailNodes.Length == 0 && ctb.Length == 0)
|
||||
{
|
||||
detailNodes = nodes.Take(32).ToArray();
|
||||
}
|
||||
var summary = nodes.FirstOrDefault(static node => node.Id == "voidbot-summary");
|
||||
var agent = nodes.FirstOrDefault(static node => node.Id == "agent-detail");
|
||||
|
||||
return new AquariumUiDocument()
|
||||
.Panel("Eve Surface", 18.0f, 82.0f, 420.0f, fadeWhenMouseDistant: true, panel =>
|
||||
{
|
||||
panel.Section("CultMesh Provider");
|
||||
panel.Readout("Provider", () => string.IsNullOrWhiteSpace(snapshot.ProviderId) ? providerId : snapshot.ProviderId);
|
||||
panel.Readout("Title", () => snapshot.Title);
|
||||
panel.Readout("Version", () => snapshot.Version.ToString(CultureInfo.InvariantCulture));
|
||||
panel.Readout("Broker", () => brokerUrl);
|
||||
panel.Readout("Status", () => connectionStatus);
|
||||
panel.Button("Reconnect", Reconnect, "Restart the WebSocket subscription and reopen the provider.");
|
||||
})
|
||||
.Panel(ctb.Length > 0 ? "CTB" : "Surface Nodes", 456.0f, 82.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
|
||||
{
|
||||
panel.Section(ctb.Length > 0 ? "VoidBot Turn Bar" : snapshot.Title);
|
||||
foreach (var node in primaryNodes)
|
||||
{
|
||||
panel.TreeItem(
|
||||
CompactLabel(node.Label),
|
||||
0,
|
||||
canExpand: false,
|
||||
isExpanded: static () => false,
|
||||
setExpanded: static _ => { },
|
||||
isSelected: () => selectedNodeId == node.Id,
|
||||
select: () => selectedNodeId = node.Id,
|
||||
detail: CompactDetail(node));
|
||||
}
|
||||
})
|
||||
.Panel(ctb.Length > 0 ? "Selected Face" : "Selected Node", 18.0f, 382.0f, 420.0f, fadeWhenMouseDistant: true, panel =>
|
||||
{
|
||||
panel.Section(summary?.Label ?? "VoidBot Swarm");
|
||||
panel.Readout("Summary", () => summary?.Health ?? "waiting");
|
||||
panel.TextBox("Selected", () => selected?.Label ?? "none", _ => { }, lines: 1, acceptsReturn: false, monospace: false);
|
||||
panel.TextBox("Detail", () => selected?.Detail ?? agent?.Detail ?? "No provider detail yet.", _ => { }, lines: 12, acceptsReturn: false, monospace: true);
|
||||
})
|
||||
.Panel("State Graph", 456.0f, 222.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
|
||||
{
|
||||
panel.Section(leaves.Length > 0 ? "Leaves" : "Details");
|
||||
foreach (var node in detailNodes)
|
||||
{
|
||||
panel.TreeItem(
|
||||
CompactLabel(node.Label),
|
||||
0,
|
||||
canExpand: false,
|
||||
isExpanded: static () => false,
|
||||
setExpanded: static _ => { },
|
||||
isSelected: () => selectedNodeId == node.Id,
|
||||
select: () => selectedNodeId = node.Id,
|
||||
detail: CompactDetail(node));
|
||||
}
|
||||
})
|
||||
.Panel("Surface Detail", 994.0f, 82.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
|
||||
{
|
||||
panel.Section(selected?.Kind ?? "detail");
|
||||
panel.Readout("Node", () => selected?.Id ?? "none");
|
||||
panel.Readout("Health", () => selected?.Health ?? "");
|
||||
panel.TextBox("State", () => selected?.Detail ?? "No selected node.", _ => { }, lines: 18, acceptsReturn: false, monospace: true);
|
||||
})
|
||||
.Command("eve-status", _ => $"{connectionStatus}; {snapshot.ProviderId} v{snapshot.Version}; nodes {nodes.Count}; surface {snapshot.Surface?.Schema ?? "none"} root {snapshot.Surface?.Root?.Kind ?? "none"}", "Report the active Eve surface subscription.");
|
||||
}
|
||||
|
||||
private void Reconnect()
|
||||
{
|
||||
connectionLifetime?.Cancel();
|
||||
connectionLifetime = new CancellationTokenSource();
|
||||
connectionTask = Task.Run(() => RunConnectionLoopAsync(connectionLifetime.Token));
|
||||
}
|
||||
|
||||
private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
SetStatus("connecting");
|
||||
using var socket = new ClientWebSocket();
|
||||
await socket.ConnectAsync(new Uri(brokerUrl), cancellationToken).ConfigureAwait(false);
|
||||
SetStatus("connected");
|
||||
await SendOpenProviderAsync(socket, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var buffer = new byte[64 * 1024];
|
||||
while (socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var message = await ReceiveTextAsync(socket, buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (message is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var next = EveSurfaceState.Parse(message);
|
||||
if (!string.Equals(next.ProviderId, providerId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lock (sync)
|
||||
{
|
||||
state = next;
|
||||
status = $"live {next.UpdatedAt}";
|
||||
if (string.IsNullOrWhiteSpace(selectedNodeId))
|
||||
{
|
||||
selectedNodeId = next.SelectedNodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
SetStatus($"error {exception.Message}");
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendOpenProviderAsync(ClientWebSocket socket, CancellationToken cancellationToken)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes($$"""{"type":"open-provider","providerId":"{{providerId}}"}""");
|
||||
await socket.SendAsync(payload, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<string?> ReceiveTextAsync(ClientWebSocket socket, byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
while (true)
|
||||
{
|
||||
var result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, result.Count);
|
||||
if (result.EndOfMessage)
|
||||
{
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetStatus(string value)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
status = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CompactLabel(string value)
|
||||
{
|
||||
var label = value.Replace('\n', ' ').Replace('\r', ' ').Trim();
|
||||
return label.Length <= 72 ? label : label[..69] + "...";
|
||||
}
|
||||
|
||||
private static string CompactDetail(EveSurfaceNode node)
|
||||
{
|
||||
var parts = new[] { node.Kind, node.Health, node.StatePath }
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value));
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private static AquariumRenderPlan CreateRenderPlan()
|
||||
{
|
||||
var app = new AquariumApp();
|
||||
var scene = app.RenderTargets.Hdr("scene");
|
||||
app.Cameras.Perspective("main");
|
||||
app.Features.Presentation(scene.Color);
|
||||
app.Features.DirectWriteOverlay();
|
||||
app.Debug.View("Scene", scene.Color);
|
||||
return app.Plan;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveSurfaceRuntimeFactory : IAquariumRuntimeFactory
|
||||
{
|
||||
public IAquariumRuntime Create(AquariumRuntimeOptions options)
|
||||
{
|
||||
var runtime = new EveSurfaceRuntime();
|
||||
runtime.SetOptions(options);
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record EveSurfaceState(
|
||||
string ProviderId,
|
||||
string Title,
|
||||
long Version,
|
||||
string UpdatedAt,
|
||||
string SelectedNodeId,
|
||||
IReadOnlyList<EveSurfaceNode> Nodes,
|
||||
EveCompositionSurface? Surface)
|
||||
{
|
||||
public static EveSurfaceState Empty { get; } = new(
|
||||
"",
|
||||
"Eve Surface",
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
[],
|
||||
null);
|
||||
|
||||
public static EveSurfaceState Parse(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
var nodes = new List<EveSurfaceNode>();
|
||||
if (root.TryGetProperty("nodes", out var nodeArray) && nodeArray.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var node in nodeArray.EnumerateArray())
|
||||
{
|
||||
nodes.Add(new EveSurfaceNode(
|
||||
ReadString(node, "id"),
|
||||
ReadString(node, "label"),
|
||||
ReadString(node, "kind"),
|
||||
ReadDouble(node, "x"),
|
||||
ReadDouble(node, "y"),
|
||||
ReadDouble(node, "width"),
|
||||
ReadDouble(node, "height"),
|
||||
ReadString(node, "health"),
|
||||
ReadString(node, "detail"),
|
||||
ReadString(node, "statePath"),
|
||||
ReadString(node, "avatarUrl")));
|
||||
}
|
||||
}
|
||||
|
||||
return new EveSurfaceState(
|
||||
ReadString(root, "providerId"),
|
||||
ReadString(root, "title", "Eve Surface"),
|
||||
ReadInt64(root, "version"),
|
||||
ReadString(root, "updatedAt"),
|
||||
ReadString(root, "selectedNodeId"),
|
||||
nodes,
|
||||
ReadSurface(root));
|
||||
}
|
||||
|
||||
private static EveCompositionSurface? ReadSurface(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("surface", out var surface) || surface.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EveCompositionSurface(
|
||||
ReadString(surface, "schema"),
|
||||
ReadString(surface, "id"),
|
||||
ReadString(surface, "title"),
|
||||
surface.TryGetProperty("root", out var rootElement) ? ReadElement(rootElement) : null,
|
||||
surface.TryGetProperty("assets", out var assets) && assets.ValueKind == JsonValueKind.Array
|
||||
? assets.EnumerateArray()
|
||||
.Select(static asset => new EveCompositionAsset(
|
||||
ReadString(asset, "id"),
|
||||
ReadString(asset, "kind"),
|
||||
ReadString(asset, "uri")))
|
||||
.Where(static asset => !string.IsNullOrWhiteSpace(asset.Id))
|
||||
.ToArray()
|
||||
: []);
|
||||
}
|
||||
|
||||
private static EveCompositionElement? ReadElement(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var children = element.TryGetProperty("children", out var childArray) && childArray.ValueKind == JsonValueKind.Array
|
||||
? childArray.EnumerateArray().Select(ReadElement).OfType<EveCompositionElement>().ToArray()
|
||||
: [];
|
||||
return new EveCompositionElement(
|
||||
ReadString(element, "id"),
|
||||
ReadString(element, "kind"),
|
||||
ReadString(element, "role"),
|
||||
ReadString(element, "text"),
|
||||
ReadString(element, "assetRef"),
|
||||
ReadString(element, "assetUri"),
|
||||
ReadString(element, "bindNodeId"),
|
||||
ReadString(element, "commandId"),
|
||||
children);
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement element, string property, string fallback = "")
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString() ?? fallback
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static long ReadInt64(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.TryGetInt64(out var number)
|
||||
? number
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static double ReadDouble(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.TryGetDouble(out var number)
|
||||
? number
|
||||
: 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record EveSurfaceNode(
|
||||
string Id,
|
||||
string Label,
|
||||
string Kind,
|
||||
double X,
|
||||
double Y,
|
||||
double Width,
|
||||
double Height,
|
||||
string Health,
|
||||
string Detail,
|
||||
string StatePath,
|
||||
string AvatarUrl);
|
||||
|
||||
internal sealed record EveCompositionSurface(
|
||||
string Schema,
|
||||
string Id,
|
||||
string Title,
|
||||
EveCompositionElement? Root,
|
||||
IReadOnlyList<EveCompositionAsset> Assets);
|
||||
|
||||
internal sealed record EveCompositionElement(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Role,
|
||||
string Text,
|
||||
string AssetRef,
|
||||
string AssetUri,
|
||||
string BindNodeId,
|
||||
string CommandId,
|
||||
IReadOnlyList<EveCompositionElement> Children);
|
||||
|
||||
internal sealed record EveCompositionAsset(string Id, string Kind, string Uri);
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
<ItemGroup>
|
||||
<Content Include="Shaders\**\*.hlsl" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
<Content Include="Shaders\**\*.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfCommon.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfCommon.hlsli" />
|
||||
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfMath.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfMath.hlsli" />
|
||||
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfProxy.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfProxy.hlsli" />
|
||||
<Content Include="Worlds\**\*.aquageo" CopyToOutputDirectory="PreserveNewest" TargetPath="Worlds\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,20 @@ namespace Aquarium.Sample.Minimal;
|
|||
public sealed class MinimalRuntime : IAquariumRuntime
|
||||
{
|
||||
private float timeSeconds;
|
||||
private int selectedFeed;
|
||||
private int selectedLut;
|
||||
private bool feedEnabled = true;
|
||||
private bool visible = true;
|
||||
private bool locked;
|
||||
private float opacity = 1.0f;
|
||||
private float gain = 0.72f;
|
||||
private float lutStrength = 1.0f;
|
||||
private float x = -3.2f;
|
||||
private float y = 2.1f;
|
||||
private float z;
|
||||
private float rotation;
|
||||
private float scaleX = 2.4f;
|
||||
private float scaleY = 0.8f;
|
||||
|
||||
public AquariumRuntimeOptions Options { get; private set; }
|
||||
|
||||
|
|
@ -17,7 +31,7 @@ public sealed class MinimalRuntime : IAquariumRuntime
|
|||
|
||||
public AquariumRenderPlan RenderPlan { get; } = CreateRenderPlan();
|
||||
|
||||
public AquariumUiDocument Ui { get; } = AquariumUiDocument.Empty;
|
||||
public AquariumUiDocument Ui { get; }
|
||||
|
||||
public AquariumAudioDocument Audio { get; } = new();
|
||||
|
||||
|
|
@ -30,6 +44,11 @@ public sealed class MinimalRuntime : IAquariumRuntime
|
|||
Vector2.Zero,
|
||||
AquariumSceneState.Empty);
|
||||
|
||||
public MinimalRuntime()
|
||||
{
|
||||
Ui = CreateUi();
|
||||
}
|
||||
|
||||
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input)
|
||||
{
|
||||
return frame;
|
||||
|
|
@ -58,6 +77,119 @@ public sealed class MinimalRuntime : IAquariumRuntime
|
|||
Options = options;
|
||||
}
|
||||
|
||||
private AquariumUiDocument CreateUi()
|
||||
{
|
||||
var feeds = new[]
|
||||
{
|
||||
new AquariumUiOption(0, "PS3 Eye 0 high-speed"),
|
||||
new AquariumUiOption(1, "Kiyo Pro RGB"),
|
||||
new AquariumUiOption(2, "Synthetic checker field"),
|
||||
};
|
||||
var luts = new[]
|
||||
{
|
||||
new AquariumUiOption(0, "Neutral"),
|
||||
new AquariumUiOption(1, "Thermal"),
|
||||
new AquariumUiOption(2, "Depth debug"),
|
||||
};
|
||||
|
||||
return new AquariumUiDocument()
|
||||
.Surface("aquarium.ui_smoke", "CultUI Smoke", 16.0f, 56.0f, 980.0f, 620.0f, surface =>
|
||||
{
|
||||
surface.Horizontal("columns", columns =>
|
||||
{
|
||||
columns.Vertical("left", left =>
|
||||
{
|
||||
left.Pane("program", "Program", pane =>
|
||||
{
|
||||
pane.Options("feed", "Feed", () => selectedFeed, value => selectedFeed = value, feeds);
|
||||
pane.Toggle("feed-enabled", "Visible", () => feedEnabled, value => feedEnabled = value);
|
||||
pane.Slider("opacity", "Opacity", () => opacity, value => opacity = value, 0.0f, 1.0f, "0.00");
|
||||
pane.Text("program-compose", () => $"1:{feeds[selectedFeed].Label}@{opacity:0.00} | LUT:{luts[selectedLut].Label}", "mono");
|
||||
pane.Options("stem", "Stem", () => 0, _ => { }, [new AquariumUiOption(0, "Focusrite Input 1")]);
|
||||
pane.Slider("gain", "Gain", () => gain, value => gain = value, 0.0f, 1.5f, "0.00");
|
||||
pane.Options("lut", "LUT", () => selectedLut, value => selectedLut = value, luts);
|
||||
pane.Slider("lut-strength", "LUT Strength", () => lutStrength, value => lutStrength = value, 0.0f, 1.0f, "0.00");
|
||||
}, weight: 1.2f);
|
||||
|
||||
left.Pane("scene", "Scene", pane =>
|
||||
{
|
||||
pane.Options("editor-view", "Editor View", () => 0, _ => { }, [new AquariumUiOption(0, "Scene")]);
|
||||
pane.Card("tree", card =>
|
||||
{
|
||||
card.Text("tree-0", "Scene", "strong");
|
||||
card.Text("tree-1", " - [eye] Editor Camera <Camera>", "mono");
|
||||
card.Text("tree-2", " - Sensor Feeds (4)", "mono");
|
||||
card.Text("tree-3", " - [eye] Kiyo Pro RGB context <SensorFeed>", "mono");
|
||||
card.Text("tree-4", " - [eye] Synthetic checker field <SensorFeed>", "mono");
|
||||
});
|
||||
pane.Row("scene-buttons", row =>
|
||||
{
|
||||
row.Button("previous", "Previous", () => selectedFeed = Math.Max(0, selectedFeed - 1));
|
||||
row.Button("next", "Next", () => selectedFeed = Math.Min(feeds.Length - 1, selectedFeed + 1));
|
||||
});
|
||||
pane.Text("scene-status", () => $"SdfTextPanel pos={x:0.00},{y:0.00},{z:0.00} scale={scaleX:0.00},{scaleY:0.00}", "mono");
|
||||
});
|
||||
}, weight: 1.05f);
|
||||
|
||||
columns.Vertical("middle", middle =>
|
||||
{
|
||||
middle.Pane("sync", "Sync", pane =>
|
||||
{
|
||||
pane.Text("streams", "4 video / 4 audio buffers", "strong");
|
||||
pane.Metric("sources", "sources", () => 5.0);
|
||||
pane.Metric("ingested", "samples ingested", () => 533_939.0 + (timeSeconds * 120.0));
|
||||
pane.Text("buffer-detail", "Focusrite Input 1 [asio-ch0]: 3751 1ch 192000Hz; passive mode waiting for program audio column", "body");
|
||||
pane.Text("audio-sync", () => $"asio-ch0: {1484.0 + Math.Sin(timeSeconds) * 0.2:0.00} samples {779.2 + Math.Cos(timeSeconds) * 0.1:0.00} us", "mono");
|
||||
pane.Text("chirplet", "chirplet reference: asio-ch1 passive; calibration emission command-ready", "body");
|
||||
pane.Slider("perspective", "Perspective", () => 10.0f, _ => { }, 1.0f, 20.0f, "0.0x");
|
||||
pane.Slider("angle", "Angle", () => 25.0f, _ => { }, 0.0f, 90.0f, "0.0 deg");
|
||||
pane.Text("tube-budget", "40/40 age columns x 4/8 lanes x 4 subdivisions", "mono");
|
||||
}, weight: 1.15f);
|
||||
|
||||
middle.Pane("transform", "Transform", pane =>
|
||||
{
|
||||
pane.Toggle("visible", "Visible", () => visible, value => visible = value);
|
||||
pane.Toggle("locked", "Locked", () => locked, value => locked = value);
|
||||
pane.Text("mode", "Mode: Grab", "mono");
|
||||
pane.Slider("x", "X", () => x, value => x = value, -10.0f, 10.0f, "0.00");
|
||||
pane.Slider("y", "Y", () => y, value => y = value, -10.0f, 10.0f, "0.00");
|
||||
pane.Slider("z", "Z", () => z, value => z = value, -10.0f, 10.0f, "0.00");
|
||||
pane.Slider("rotation", "Rotation", () => rotation, value => rotation = value, -180.0f, 180.0f, "0.00");
|
||||
pane.Slider("scale-x", "Scale X", () => scaleX, value => scaleX = value, 0.1f, 4.0f, "0.00");
|
||||
pane.Slider("scale-y", "Scale Y", () => scaleY, value => scaleY = value, 0.1f, 4.0f, "0.00");
|
||||
pane.Row("transform-buttons", row =>
|
||||
{
|
||||
row.Button("reset-transform", "Reset Transform", ResetTransform);
|
||||
row.Button("reset-camera", "Reset Camera", ResetTransform);
|
||||
});
|
||||
});
|
||||
}, weight: 1.1f);
|
||||
|
||||
columns.Pane("create", "Create", pane =>
|
||||
{
|
||||
pane.Text("text-label", "Text", "body");
|
||||
pane.Text("text-value", "Mimir", "strong");
|
||||
pane.Button("add-sdf-text", "Add SDF Text", () => { });
|
||||
pane.Text("model-path-label", "Model Path", "body");
|
||||
pane.Text("model-path", "assets/models/example.glb", "mono");
|
||||
pane.Button("import-model", "Import Model", () => { });
|
||||
pane.Metric("fps", "ui smoke fps proxy", () => 1.0 / Math.Max(0.001, Math.Min(0.25, timeSeconds % 1.0)));
|
||||
pane.Text("budget", "This pane is intentionally narrow enough to prove labels clip inside their cells instead of wandering across neighbors.", "body");
|
||||
}, weight: 0.75f);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void ResetTransform()
|
||||
{
|
||||
x = -3.2f;
|
||||
y = 2.1f;
|
||||
z = 0.0f;
|
||||
rotation = 0.0f;
|
||||
scaleX = 2.4f;
|
||||
scaleY = 0.8f;
|
||||
}
|
||||
|
||||
private static AquariumRenderPlan CreateRenderPlan()
|
||||
{
|
||||
var app = new AquariumApp();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
3
src/Aquarium.Zyphos/Properties/AssemblyInfo.cs
Normal file
3
src/Aquarium.Zyphos/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Aquarium.Engine.Tests")]
|
||||
|
|
@ -3,6 +3,8 @@ static const int ZYPHOS_GEOMETRY_BRUSH_LIMIT = 4;
|
|||
|
||||
#include "D3D12SdfCommon.hlsli"
|
||||
#include "D3D12SdfMath.hlsli"
|
||||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
#include "D3D12ZyphosTerrain.hlsli"
|
||||
|
||||
float3 zyRotateZ(float3 p, float angle)
|
||||
{
|
||||
|
|
@ -28,19 +30,6 @@ float zyUmbrosEclipse(float3 starDirectionLocal)
|
|||
return smoothstep(cos(umbrosAngularRadius * 1.45), cos(umbrosAngularRadius * 0.45), alignment);
|
||||
}
|
||||
|
||||
float zySphericalField(float3 dir)
|
||||
{
|
||||
float latitude = asin(saturate(abs(dir.z)) * 2.0 - 1.0);
|
||||
float longitude = atan2(dir.y, dir.x);
|
||||
float plates =
|
||||
sin(longitude * 2.0 + dir.z * 4.5) * 0.28 +
|
||||
sin(longitude * 3.0 - dir.z * 7.0 + 1.7) * 0.22 +
|
||||
sin((dir.x + dir.y * 0.7 + dir.z * 0.4) * 8.0) * 0.16 +
|
||||
sin((dir.x * 1.3 - dir.y + dir.z * 1.9) * 13.0) * 0.08;
|
||||
float equatorialWarmth = cos(latitude) * 0.18;
|
||||
return 0.50 + plates + equatorialWarmth;
|
||||
}
|
||||
|
||||
float zyHash21(float2 p)
|
||||
{
|
||||
p = frac(p * float2(123.34, 456.21));
|
||||
|
|
@ -68,39 +57,21 @@ float zyCompactBrush(float2 delta, float2 radii, float rotation, float falloff,
|
|||
|
||||
float2 zyCubeFaceUv(float3 dir, out float face)
|
||||
{
|
||||
// Authored brush packets retain their legacy cube-ratio chart. Planetary
|
||||
// Page addressing and patch geometry use GameCult.Geometry's QSC topology.
|
||||
float3 a = abs(dir);
|
||||
if (a.x >= a.y && a.x >= a.z)
|
||||
{
|
||||
if (dir.x >= 0.0)
|
||||
{
|
||||
face = 0.0;
|
||||
return float2(-dir.z, dir.y) / max(a.x, 0.0001);
|
||||
}
|
||||
|
||||
face = 1.0;
|
||||
return float2(dir.z, dir.y) / max(a.x, 0.0001);
|
||||
if (dir.x >= 0.0) { face = 0.0; return float2(-dir.z, dir.y) / max(a.x, 0.0001); }
|
||||
face = 1.0; return float2(dir.z, dir.y) / max(a.x, 0.0001);
|
||||
}
|
||||
|
||||
if (a.y >= a.z)
|
||||
{
|
||||
if (dir.y >= 0.0)
|
||||
{
|
||||
face = 2.0;
|
||||
return float2(dir.x, -dir.z) / max(a.y, 0.0001);
|
||||
}
|
||||
|
||||
face = 3.0;
|
||||
return float2(dir.x, dir.z) / max(a.y, 0.0001);
|
||||
if (dir.y >= 0.0) { face = 2.0; return float2(dir.x, -dir.z) / max(a.y, 0.0001); }
|
||||
face = 3.0; return float2(dir.x, dir.z) / max(a.y, 0.0001);
|
||||
}
|
||||
|
||||
if (dir.z >= 0.0)
|
||||
{
|
||||
face = 4.0;
|
||||
return dir.xy / max(a.z, 0.0001);
|
||||
}
|
||||
|
||||
face = 5.0;
|
||||
return float2(-dir.x, dir.y) / max(a.z, 0.0001);
|
||||
if (dir.z >= 0.0) { face = 4.0; return dir.xy / max(a.z, 0.0001); }
|
||||
face = 5.0; return float2(-dir.x, dir.y) / max(a.z, 0.0001);
|
||||
}
|
||||
|
||||
float2 zyTileBrushPlane(float2 faceUv, float level, float tileX, float tileY, out float inTile)
|
||||
|
|
@ -224,11 +195,11 @@ float zyPebbleCluster(float3 dir)
|
|||
return coast * smoothstep(0.48, 0.94, grains * 0.5 + 0.5);
|
||||
}
|
||||
|
||||
float zyTerrainOffset(float3 dir, SdfObject sdfObject)
|
||||
float zyTerrainBaseOffset(float3 dir, SdfObject sdfObject, out float land)
|
||||
{
|
||||
float field = zySphericalField(dir);
|
||||
float seaLevel = sdfObject.state.z;
|
||||
float land = smoothstep(seaLevel - 0.04, seaLevel + 0.08, field);
|
||||
land = smoothstep(seaLevel - 0.04, seaLevel + 0.08, field);
|
||||
float mountain = pow(saturate(field - seaLevel), 1.65);
|
||||
float polarCap = pow(abs(dir.z), 8.0) * 0.035;
|
||||
float authoredMaterial;
|
||||
|
|
@ -237,13 +208,57 @@ float zyTerrainOffset(float3 dir, SdfObject sdfObject)
|
|||
return (field - seaLevel) * 0.10 + mountain * 0.13 + polarCap * land + tileRelief + authoredGeometry * 0.52;
|
||||
}
|
||||
|
||||
float zyTerrainOffset(float3 dir, SdfObject sdfObject)
|
||||
{
|
||||
float land; float baseOffset=zyTerrainBaseOffset(dir,sdfObject,land);
|
||||
float4 pageHeightGradient; float2 pageMasks;
|
||||
float erosionHeight=zyTrySampleErosionPage(dir,pageHeightGradient,pageMasks)?pageHeightGradient.x:0.0;
|
||||
return baseOffset+erosionHeight*land;
|
||||
}
|
||||
|
||||
float sdfDistance(float3 p, int sdfIndex)
|
||||
{
|
||||
SdfObject sdfObject = sdfObjects[sdfIndex];
|
||||
float planetRadius = max(sdfObject.state.x, 0.001);
|
||||
float3 local = p - sdfObject.centerRadius.xyz;
|
||||
float3 dir = zyPlanetDir(local, sdfObject);
|
||||
return length(local) - (planetRadius + zyTerrainOffset(dir, sdfObject));
|
||||
float radialDistance = length(local) - (planetRadius + zyTerrainOffset(dir, sdfObject));
|
||||
return radialDistance * zyTerrainConservativeDistanceScale(dir);
|
||||
}
|
||||
|
||||
float zyPlanetTraceBoundRadius(SdfObject sdfObject)
|
||||
{
|
||||
float legacyBound = max(sdfObject.centerRadius.w * 1.42, 0.001);
|
||||
float erosionExtent=0.0; int pageCount=min((int)zy_planet_page_set[0].state.x,64);
|
||||
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
|
||||
{
|
||||
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex]; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
if(metadata.state.x<0.5||summary.metadata.w<0.5)continue;
|
||||
erosionExtent+=max(abs(summary.bounds.x),abs(summary.bounds.y))*saturate(metadata.state.y)+max(summary.bounds.w,0.0);
|
||||
}
|
||||
return max(legacyBound, max(sdfObject.state.x, 0.001) + erosionExtent);
|
||||
}
|
||||
|
||||
float3 zyPlanetSurfaceNormal(float3 p, int sdfIndex)
|
||||
{
|
||||
SdfObject sdfObject=sdfObjects[sdfIndex]; float radius=max(sdfObject.state.x,0.001);
|
||||
float3 local=p-sdfObject.centerRadius.xyz; float3 dir=zyPlanetDir(local,sdfObject);
|
||||
float3 reference=abs(dir.z)<0.8?float3(0,0,1):float3(0,1,0);
|
||||
float3 tangentX=normalize(cross(reference,dir)); float3 tangentY=normalize(cross(dir,tangentX));
|
||||
float spacing=max(radius/8192.0,0.0005); float angular=spacing/radius;
|
||||
float landCenter; zyTerrainBaseOffset(dir,sdfObject,landCenter);
|
||||
float landXp,landXm,landYp,landYm;
|
||||
float baseXp=zyTerrainBaseOffset(normalize(dir+tangentX*angular),sdfObject,landXp);
|
||||
float baseXm=zyTerrainBaseOffset(normalize(dir-tangentX*angular),sdfObject,landXm);
|
||||
float baseYp=zyTerrainBaseOffset(normalize(dir+tangentY*angular),sdfObject,landYp);
|
||||
float baseYm=zyTerrainBaseOffset(normalize(dir-tangentY*angular),sdfObject,landYm);
|
||||
float3 baseGradient=tangentX*((baseXp-baseXm)/(2.0*spacing))+tangentY*((baseYp-baseYm)/(2.0*spacing));
|
||||
float3 landGradient=tangentX*((landXp-landXm)/(2.0*spacing))+tangentY*((landYp-landYm)/(2.0*spacing));
|
||||
float4 erosion; float2 masks; erosion=0.0;
|
||||
zyTrySampleErosionPage(dir,erosion,masks);
|
||||
float3 erosionGradient=erosion.yzw;
|
||||
float3 planetNormal=normalize(dir-(baseGradient+landCenter*erosionGradient+erosion.x*landGradient));
|
||||
return normalize(zyRotateZ(planetNormal,sdfObject.state.y));
|
||||
}
|
||||
|
||||
SdfSurface sdfSurface(float3 p, int sdfIndex)
|
||||
|
|
@ -261,6 +276,9 @@ SdfSurface sdfSurface(float3 p, int sdfIndex)
|
|||
float leafCluster = zyLeafCluster(dir);
|
||||
float pebbleCluster = zyPebbleCluster(dir);
|
||||
float polar = smoothstep(0.72, 0.92, abs(dir.z));
|
||||
float4 erosionDifferential; float2 erosionMasks;
|
||||
zyTrySampleErosionPage(dir,erosionDifferential,erosionMasks);
|
||||
float ridge=erosionMasks.x, gully=erosionMasks.y;
|
||||
float cloud = smoothstep(0.73, 0.91, sin(dir.x * 18.0 + dir.y * 13.0 + dir.z * 9.0 + timeSeconds * 0.19) * 0.5 + 0.5);
|
||||
|
||||
float3 ocean = lerp(float3(0.004, 0.070, 0.130), float3(0.010, 0.220, 0.330), saturate(field));
|
||||
|
|
@ -273,18 +291,46 @@ SdfSurface sdfSurface(float3 p, int sdfIndex)
|
|||
landColor = lerp(landColor, float3(0.78, 0.56, 0.30), saturate(authoredMaterial * 2.6 + authoredHeight * 5.0));
|
||||
landColor = lerp(landColor, float3(0.025, 0.24, 0.08), leafCluster * 0.82);
|
||||
landColor = lerp(landColor, float3(0.42, 0.39, 0.34), pebbleCluster * 0.65);
|
||||
landColor = lerp(landColor, float3(0.49,0.44,0.36), ridge*0.34);
|
||||
landColor = lerp(landColor, float3(0.035,0.16,0.11), gully*0.28);
|
||||
|
||||
SdfSurface surface;
|
||||
surface.baseColor = lerp(ocean, landColor, land);
|
||||
surface.baseColor = lerp(surface.baseColor, float3(0.78, 0.84, 0.80), cloud * 0.16);
|
||||
surface.metallic = 0.0;
|
||||
surface.roughness = lerp(0.34, 0.84, land);
|
||||
surface.roughness = saturate(lerp(0.34,0.84,land)+ridge*0.09-gully*0.12);
|
||||
surface.emission = 0.0;
|
||||
surface.temporalDetail = saturate(authoredMaterial * 3.0);
|
||||
surface.reservoirConfidence = saturate(0.42 + authoredMaterial * 2.2 + leafCluster * 0.25 + pebbleCluster * 0.20);
|
||||
return surface;
|
||||
}
|
||||
|
||||
bool zyTracePlanet(float3 origin,float3 direction,int sdfIndex,out float travel,out float3 normal,out SdfSurface surface,out float stepCount)
|
||||
{
|
||||
SdfObject sdfObject=sdfObjects[sdfIndex]; float boundRadius=zyPlanetTraceBoundRadius(sdfObject);
|
||||
float3 oc=origin-sdfObject.centerRadius.xyz; float b=dot(oc,direction); float c=dot(oc,oc)-boundRadius*boundRadius;
|
||||
float discriminant=b*b-c; normal=0.0; stepCount=0.0;
|
||||
surface.baseColor=0.0; surface.metallic=0.0; surface.roughness=0.0; surface.emission=0.0; surface.temporalDetail=0.0; surface.reservoirConfidence=1.0;
|
||||
if(discriminant<0.0){travel=farDistance+1.0;return false;}
|
||||
float extent=sqrt(discriminant); float entry=max(-b-extent,0.0); float closest=max(-b,entry); float exit=min(-b+extent,farDistance);
|
||||
if(entry>exit){travel=farDistance+1.0;return false;}
|
||||
float low=entry; float lowDistance=sdfDistance(origin+direction*low,sdfIndex);
|
||||
if(lowDistance<=0.0){travel=low;float3 p=origin+direction*travel;normal=zyPlanetSurfaceNormal(p,sdfIndex);surface=sdfSurface(p,sdfIndex);return true;}
|
||||
float high=low; bool bracketed=false;
|
||||
[unroll] for(int coarse=1;coarse<=24;coarse++)
|
||||
{
|
||||
high=lerp(entry,closest,(float)coarse/24.0); float highDistance=sdfDistance(origin+direction*high,sdfIndex); stepCount=(float)coarse;
|
||||
if(highDistance<=0.0){bracketed=true;break;} low=high; lowDistance=highDistance;
|
||||
}
|
||||
if(!bracketed){travel=farDistance+1.0;return false;}
|
||||
[unroll] for(int refine=0;refine<10;refine++)
|
||||
{
|
||||
float mid=(low+high)*0.5; float value=sdfDistance(origin+direction*mid,sdfIndex);
|
||||
if(value<=0.0)high=mid;else low=mid;
|
||||
}
|
||||
travel=high; float3 p=origin+direction*travel; normal=zyPlanetSurfaceNormal(p,sdfIndex); surface=sdfSurface(p,sdfIndex); stepCount+=10.0; return true;
|
||||
}
|
||||
|
||||
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
|
||||
{
|
||||
SdfObject sdfObject = sdfObjects[sdfIndex];
|
||||
|
|
@ -310,4 +356,7 @@ float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex,
|
|||
return shadeSdfPbr(p, normal, surface) * daylight + city + atmosphere + eclipseTint;
|
||||
}
|
||||
|
||||
#define SDF_TRACE_BOUND_RADIUS(sdfObject) zyPlanetTraceBoundRadius(sdfObject)
|
||||
#define SDF_SURFACE_NORMAL(p,sdfIndex) zyPlanetSurfaceNormal(p,sdfIndex)
|
||||
#define SDF_TRACE_FUNCTION zyTracePlanet
|
||||
#include "D3D12SdfProxy.hlsli"
|
||||
|
|
|
|||
156
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli
Normal file
156
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#ifndef D3D12_ZYPHOS_TERRAIN_HLSLI
|
||||
#define D3D12_ZYPHOS_TERRAIN_HLSLI
|
||||
|
||||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
|
||||
struct ZyPlanetPageOutput { float4 height_gradient; float4 masks; };
|
||||
struct ZyPlanetPageMetadata { float4 address; float4 layout; float4 bounds; float4 state; };
|
||||
struct ZyPlanetPageSummary { float4 bounds; float4 metadata; };
|
||||
struct ZyPlanetPageSet { float4 state; };
|
||||
StructuredBuffer<ZyPlanetPageOutput> zy_planet_page : register(t81);
|
||||
StructuredBuffer<ZyPlanetPageMetadata> zy_planet_page_metadata : register(t82);
|
||||
StructuredBuffer<ZyPlanetPageSummary> zy_planet_page_summary : register(t83);
|
||||
StructuredBuffer<ZyPlanetPageSet> zy_planet_page_set : register(t84);
|
||||
|
||||
static const float ZY_TERRAIN_NON_EROSION_SLOPE_BOUND = 8.0;
|
||||
|
||||
bool zyTerrainPageLocal(float3 dir, ZyPlanetPageMetadata metadata, out float2 local)
|
||||
{
|
||||
return gamecult_geometry_planetary_page_local(dir,metadata.address,metadata.state.x,local);
|
||||
}
|
||||
|
||||
void zySampleErosionPage(int pageIndex, float2 local, out float4 heightGradient, out float2 masks)
|
||||
{
|
||||
ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
float storage=metadata.layout.y, interior=metadata.layout.z, border=metadata.layout.w;
|
||||
float2 texel=local*(interior-1.0)+border; float2 base=floor(texel); float2 fraction=frac(texel);
|
||||
int2 p0=(int2)clamp(base,0.0,storage-1.0); int2 p1=min(p0+1,(int2)(storage-1.0));
|
||||
int offset=(int)metadata.layout.x;
|
||||
ZyPlanetPageOutput a=zy_planet_page[offset+p0.y*(int)storage+p0.x], b=zy_planet_page[offset+p0.y*(int)storage+p1.x];
|
||||
ZyPlanetPageOutput c=zy_planet_page[offset+p1.y*(int)storage+p0.x], d=zy_planet_page[offset+p1.y*(int)storage+p1.x];
|
||||
GameCultGeometryPlanetaryPageSample ca,cb,cc,cd;
|
||||
ca.height_gradient=a.height_gradient; ca.masks=a.masks.xy;
|
||||
cb.height_gradient=b.height_gradient; cb.masks=b.masks.xy;
|
||||
cc.height_gradient=c.height_gradient; cc.masks=c.masks.xy;
|
||||
cd.height_gradient=d.height_gradient; cd.masks=d.masks.xy;
|
||||
GameCultGeometryPlanetaryPageSample sample=gamecult_geometry_planetary_page_lerp(ca,cb,cc,cd,fraction);
|
||||
heightGradient=sample.height_gradient; masks=sample.masks;
|
||||
}
|
||||
|
||||
bool zyTrySampleErosionPage(float3 dir, out float4 heightGradient, out float2 masks)
|
||||
{
|
||||
heightGradient=0.0; masks=0.0; bool found=false;
|
||||
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
|
||||
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
|
||||
{
|
||||
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
if(!zyTerrainPageLocal(dir,metadata,local))continue;
|
||||
float4 contribution; float2 pageMasks; zySampleErosionPage(pageIndex,local,contribution,pageMasks);
|
||||
float blend=saturate(metadata.state.y); heightGradient+=contribution*blend;
|
||||
masks=found?lerp(masks,pageMasks,blend):pageMasks; found=true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bool zyTerrainDeepestPageEvidence(
|
||||
float3 dir,
|
||||
out ZyPlanetPageMetadata selectedMetadata,
|
||||
out ZyPlanetPageSummary selectedSummary,
|
||||
out float4 selectedHeightGradient,
|
||||
out float2 selectedMasks)
|
||||
{
|
||||
selectedMetadata=(ZyPlanetPageMetadata)0;
|
||||
selectedSummary=(ZyPlanetPageSummary)0;
|
||||
selectedHeightGradient=0.0;
|
||||
selectedMasks=0.0;
|
||||
float selectedLevel=-1.0;
|
||||
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
|
||||
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
|
||||
{
|
||||
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex];
|
||||
if(metadata.address.y<selectedLevel||summary.metadata.w<=0.5||!zyTerrainPageLocal(dir,metadata,local))continue;
|
||||
selectedLevel=metadata.address.y;
|
||||
selectedMetadata=metadata;
|
||||
selectedSummary=summary;
|
||||
zySampleErosionPage(pageIndex,local,selectedHeightGradient,selectedMasks);
|
||||
}
|
||||
return selectedLevel>=0.0;
|
||||
}
|
||||
|
||||
float zyTerrainConservativeDistanceScale(float3 dir)
|
||||
{
|
||||
float erosionSlope=0.0; int pageCount=min((int)zy_planet_page_set[0].state.x,64);
|
||||
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
|
||||
{
|
||||
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
if(zyTerrainPageLocal(dir,metadata,local)&&zy_planet_page_summary[pageIndex].metadata.w>0.5)
|
||||
erosionSlope+=max(zy_planet_page_summary[pageIndex].bounds.z,0.0)*saturate(metadata.state.y);
|
||||
}
|
||||
float totalSlopeBound = ZY_TERRAIN_NON_EROSION_SLOPE_BOUND + max(erosionSlope, 0.0);
|
||||
return rsqrt(1.0 + totalSlopeBound * totalSlopeBound);
|
||||
}
|
||||
|
||||
float zyTerrainPatchCaptureMargin(float3 dir)
|
||||
{
|
||||
// The raster shell must contain unresolved relief between coarse vertices;
|
||||
// otherwise the pixel refinement has no fragment on which to recover it.
|
||||
float margin=0.03;
|
||||
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
|
||||
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
|
||||
{
|
||||
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
|
||||
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex];
|
||||
if(!zyTerrainPageLocal(dir,metadata,local)||summary.metadata.w<=0.5)continue;
|
||||
float blend=saturate(metadata.state.y);
|
||||
margin+=max(abs(summary.bounds.x),abs(summary.bounds.y))*blend;
|
||||
margin+=max(summary.bounds.w,0.0);
|
||||
}
|
||||
// Four 0.08 world-unit pixel corrections define the capture interval.
|
||||
return min(margin,0.30);
|
||||
}
|
||||
|
||||
float zySphericalField(float3 dir)
|
||||
{
|
||||
float latitude = asin(saturate(abs(dir.z)) * 2.0 - 1.0);
|
||||
float longitude = atan2(dir.y, dir.x);
|
||||
float plates = sin(longitude * 2.0 + dir.z * 4.5) * 0.28 + sin(longitude * 3.0 - dir.z * 7.0 + 1.7) * 0.22
|
||||
+ sin((dir.x + dir.y * 0.7 + dir.z * 0.4) * 8.0) * 0.16 + sin((dir.x * 1.3 - dir.y + dir.z * 1.9) * 13.0) * 0.08;
|
||||
return 0.50 + plates + cos(latitude) * 0.18;
|
||||
}
|
||||
|
||||
float3 zySphericalFieldGradient(float3 dir)
|
||||
{
|
||||
const float epsilon = 0.0015;
|
||||
float dx = zySphericalField(normalize(dir + float3(epsilon,0,0))) - zySphericalField(normalize(dir - float3(epsilon,0,0)));
|
||||
float dy = zySphericalField(normalize(dir + float3(0,epsilon,0))) - zySphericalField(normalize(dir - float3(0,epsilon,0)));
|
||||
float dz = zySphericalField(normalize(dir + float3(0,0,epsilon))) - zySphericalField(normalize(dir - float3(0,0,epsilon)));
|
||||
float3 gradient = float3(dx,dy,dz)/(2.0*epsilon); return gradient-dir*dot(gradient,dir);
|
||||
}
|
||||
|
||||
GameCultGeometryAdvancedErosionParameters zyErosionParameters(float radius)
|
||||
{
|
||||
GameCultGeometryAdvancedErosionParameters p;
|
||||
p.scale=radius*0.075; p.strength=0.12; p.gully_weight=0.58; p.detail=1.45;
|
||||
p.rounding=float4(0.1,0.015,0.1,2.0); p.onset=float4(1.25,1.25,2.8,1.5); p.assumed_slope=float2(0.7,0.85);
|
||||
p.cell_scale=0.7; p.normalization=0.5; p.octaves=7; p.lacunarity=2.0; p.gain=0.5; return p;
|
||||
}
|
||||
|
||||
GameCultGeometryPlanetarySurfaceSample zyAdvancedErosionSurface(float3 dir, float field, float radius, float sampleSpacing)
|
||||
{
|
||||
GameCultGeometryPlanetaryFieldDefinition definition;
|
||||
definition.radius=radius; definition.seed=0; definition.erosion=zyErosionParameters(radius);
|
||||
GameCultGeometryPlanetaryBaseFieldSample base_sample;
|
||||
base_sample.radial_displacement=0.0; base_sample.radial_gradient=0.0;
|
||||
base_sample.field_value=field; base_sample.field_gradient=zySphericalFieldGradient(dir)/radius;
|
||||
base_sample.fade_target=saturate(field*0.5+0.5)*2.0-1.0;
|
||||
return gamecult_geometry_planetary_field_sample(definition,dir,base_sample,sampleSpacing);
|
||||
}
|
||||
|
||||
float4 zyAdvancedErosion(float3 dir, float field, float radius, float sampleSpacing)
|
||||
{
|
||||
GameCultGeometryPlanetarySurfaceSample sample=zyAdvancedErosionSurface(dir,field,radius,sampleSpacing);
|
||||
return float4(sample.radial_displacement,sample.ridge,sample.gully,sample.unresolved_height_bound);
|
||||
}
|
||||
|
||||
#endif
|
||||
30
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl
Normal file
30
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#include "D3D12ZyphosTerrain.hlsli"
|
||||
|
||||
struct PageInput { float4 direction_radius; float4 sampling; };
|
||||
struct PageOutput { float4 height_gradient; float4 masks; };
|
||||
StructuredBuffer<PageInput> page_inputs : register(t0);
|
||||
RWStructuredBuffer<PageOutput> page_outputs : register(u0);
|
||||
cbuffer PageConstants : register(b0) { uint page_sample_count; }
|
||||
|
||||
GameCultGeometryPlanetaryPageSample zyPageErosion(float3 direction, float radius, float spacing, float parent_spacing)
|
||||
{
|
||||
float3 dir=normalize(direction);
|
||||
float field=zySphericalField(dir);
|
||||
GameCultGeometryPlanetarySurfaceSample child=zyAdvancedErosionSurface(dir,field,radius,spacing);
|
||||
GameCultGeometryPlanetarySurfaceSample parent=(GameCultGeometryPlanetarySurfaceSample)0;
|
||||
bool has_parent=parent_spacing>0.0;
|
||||
if(has_parent)parent=zyAdvancedErosionSurface(dir,field,radius,parent_spacing);
|
||||
return gamecult_geometry_planetary_residual_sample(child,parent,has_parent);
|
||||
}
|
||||
|
||||
|
||||
[numthreads(64,1,1)]
|
||||
void D3D12ZyphosTerrainPageCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if(id.x>=page_sample_count) return;
|
||||
PageInput input=page_inputs[id.x]; float3 dir=normalize(input.direction_radius.xyz); float radius=input.direction_radius.w; float spacing=input.sampling.x; float parent_spacing=input.sampling.y;
|
||||
GameCultGeometryPlanetaryPageSample sample=zyPageErosion(dir,radius,spacing,parent_spacing);
|
||||
GameCultGeometryPlanetarySurfaceSample child=zyAdvancedErosionSurface(dir,zySphericalField(dir),radius,spacing);
|
||||
page_outputs[id.x].height_gradient=sample.height_gradient;
|
||||
page_outputs[id.x].masks=float4(sample.masks,0,child.unresolved_height_bound);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
|
||||
|
||||
struct PageOutput { float4 height_gradient; float4 masks; };
|
||||
struct PageSummary { float4 bounds; float4 metadata; };
|
||||
StructuredBuffer<PageOutput> summary_inputs : register(t0);
|
||||
RWStructuredBuffer<PageSummary> summary_outputs : register(u0);
|
||||
cbuffer PageConstants : register(b0) { uint page_sample_count; uint page_sample_offset; uint page_summary_index; }
|
||||
|
||||
[numthreads(1,1,1)]
|
||||
void D3D12ZyphosTerrainPageSummaryCS(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
if(id.x!=0 || page_sample_count==0) return;
|
||||
float4 summary=gamecult_geometry_planetary_summary_empty();
|
||||
[loop] for(uint index=0;index<page_sample_count;index++)
|
||||
{
|
||||
PageOutput sample=summary_inputs[page_sample_offset+index];
|
||||
summary=gamecult_geometry_planetary_summary_accumulate(summary,sample.height_gradient,sample.masks.w);
|
||||
}
|
||||
summary_outputs[page_summary_index].bounds=summary;
|
||||
summary_outputs[page_summary_index].metadata=float4(page_sample_count,page_sample_offset,page_summary_index,1);
|
||||
}
|
||||
99
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPatch.hlsl
Normal file
99
src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPatch.hlsl
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#include "D3D12ZyphosPlanet.hlsl"
|
||||
|
||||
struct ZyPatchVertexOut
|
||||
{
|
||||
float4 position : SV_Position;
|
||||
float3 worldPosition : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct ZyPatchSceneOut
|
||||
{
|
||||
float4 colorTravel:SV_Target0; float4 metadata:SV_Target1; float4 control:SV_Target2;
|
||||
float4 reservoirGuide:SV_Target3; float overdraw:SV_Target4;
|
||||
float depth:SV_Depth;
|
||||
};
|
||||
|
||||
float3 zyPatchDirection(uint face,float2 local)
|
||||
{
|
||||
return gamecult_geometry_planetary_face_direction((int)face,local*2.0-1.0);
|
||||
}
|
||||
|
||||
ZyPatchVertexOut D3D12ZyphosTerrainPatchVS(uint vertexId:SV_VertexID,uint instanceId:SV_InstanceID)
|
||||
{
|
||||
uint patchCells=(uint)max(zy_planet_page_set[0].state.y,1.0);
|
||||
uint cell=vertexId/6u,corner=vertexId%6u; uint x=cell%patchCells,y=cell/patchCells;
|
||||
uint2 offsets[6]={uint2(0,0),uint2(1,0),uint2(1,1),uint2(0,0),uint2(1,1),uint2(0,1)};
|
||||
float2 local=(float2(x,y)+offsets[corner])/(float)patchCells;
|
||||
ZyPlanetPageMetadata root=zy_planet_page_metadata[instanceId]; float3 dir=zyPatchDirection((uint)root.address.x,local);
|
||||
SdfObject planet=sdfObjects[0]; float height=zyTerrainOffset(dir,planet); if(!isfinite(height))height=0.0;
|
||||
float captureMargin=zyTerrainPatchCaptureMargin(dir);
|
||||
float3 worldPosition=planet.centerRadius.xyz+zyRotateZ(dir,planet.state.y)*(planet.state.x+height+captureMargin);
|
||||
float3 forward,right,up; cameraBasis(cameraPosition,cameraTarget,forward,right,up);
|
||||
float3 delta=worldPosition-cameraPosition; float z=max(dot(delta,forward),0.0001);
|
||||
float2 frustumMin=float2(cameraFrustumXy.x,cameraFrustumXy.z),frustumMax=float2(cameraFrustumXy.y,cameraFrustumXy.w);
|
||||
float2 slope=float2(dot(delta,right),dot(delta,up))/z; float2 uv=(slope-frustumMin)/max(frustumMax-frustumMin,0.0001);
|
||||
float2 clip=uv*float2(2.0,-2.0)+float2(-1.0,1.0);
|
||||
ZyPatchVertexOut output; output.position=float4(clip,saturate(z/max(farDistance,0.0001)),1); output.worldPosition=worldPosition; return output;
|
||||
}
|
||||
|
||||
ZyPatchSceneOut D3D12ZyphosTerrainPatchPS(ZyPatchVertexOut input)
|
||||
{
|
||||
SdfObject planet=sdfObjects[0]; float3 coarsePosition=input.worldPosition; float3 ray=normalize(coarsePosition-cameraPosition); float3 p=coarsePosition;
|
||||
[unroll] for(int step=0;step<4;step++)
|
||||
{
|
||||
float3 local=p-planet.centerRadius.xyz; float3 dir=zyPlanetDir(local,planet);
|
||||
float targetRadius=planet.state.x+zyTerrainOffset(dir,planet);
|
||||
p=gamecult_geometry_planetary_radial_refinement_step(p,ray,planet.centerRadius.xyz,targetRadius,0.2,0.08);
|
||||
}
|
||||
if(!all(isfinite(p)))p=coarsePosition;
|
||||
float travel=length(p-cameraPosition); if(!isfinite(travel)||travel<=0.0||travel>farDistance)discard;
|
||||
float3 radial=normalize(p-planet.centerRadius.xyz);
|
||||
float3 normal=cross(ddy(p),ddx(p));
|
||||
if(!all(isfinite(normal))||dot(normal,normal)<1.0e-10)normal=radial; else normal=normalize(normal);
|
||||
if(dot(normal,cameraPosition-p)<0.0)normal=-normal;
|
||||
float3 terrainDir=zyPlanetDir(p-planet.centerRadius.xyz,planet);
|
||||
ZyPlanetPageMetadata debugMetadata; ZyPlanetPageSummary debugSummary; float4 debugHeightGradient; float2 debugMasks;
|
||||
bool hasDebugPage=zyTerrainDeepestPageEvidence(terrainDir,debugMetadata,debugSummary,debugHeightGradient,debugMasks);
|
||||
float3 debugColor=0.0;
|
||||
if(renderDebugMode>=21.5&&renderDebugMode<22.5)
|
||||
debugColor=hasDebugPage?float3(debugMetadata.address.x/5.0,debugMetadata.address.y/5.0,saturate(debugMetadata.state.y)):float3(1,0,1);
|
||||
else if(renderDebugMode>=22.5&&renderDebugMode<23.5)
|
||||
debugColor=hasDebugPage?float3(saturate(0.5+debugHeightGradient.x*2.0),saturate(debugSummary.bounds.z*0.1),saturate(debugSummary.bounds.w*10.0)):float3(1,0,1);
|
||||
else if(renderDebugMode>=23.5&&renderDebugMode<24.5)
|
||||
debugColor=abs(normal);
|
||||
else if(renderDebugMode>=24.5&&renderDebugMode<25.5)
|
||||
debugColor=hasDebugPage?float3(saturate(debugMasks),saturate(debugMetadata.state.y)):float3(1,0,1);
|
||||
else if(renderDebugMode>=25.5&&renderDebugMode<26.5)
|
||||
{
|
||||
float3 absoluteDirection=abs(terrainDir);
|
||||
float dominant=max(absoluteDirection.x,max(absoluteDirection.y,absoluteDirection.z));
|
||||
float second=absoluteDirection.x+absoluteDirection.y+absoluteDirection.z-dominant-min(absoluteDirection.x,min(absoluteDirection.y,absoluteDirection.z));
|
||||
float3 dominantAxis=absoluteDirection.x>=absoluteDirection.y&&absoluteDirection.x>=absoluteDirection.z?float3(sign(terrainDir.x),0,0):absoluteDirection.y>=absoluteDirection.z?float3(0,sign(terrainDir.y),0):float3(0,0,sign(terrainDir.z));
|
||||
float3 secondaryAxis=absoluteDirection.x<dominant&&absoluteDirection.x>=min(absoluteDirection.y,absoluteDirection.z)?float3(sign(terrainDir.x),0,0):absoluteDirection.y<dominant&&absoluteDirection.y>=min(absoluteDirection.x,absoluteDirection.z)?float3(0,sign(terrainDir.y),0):float3(0,0,sign(terrainDir.z));
|
||||
float3 crossDirection=normalize(secondaryAxis-dominantAxis*dot(secondaryAxis,terrainDir));
|
||||
float4 sideA,sideB; float2 sideMasks; bool hasA=zyTrySampleErosionPage(normalize(terrainDir-crossDirection*0.001),sideA,sideMasks); bool hasB=zyTrySampleErosionPage(normalize(terrainDir+crossDirection*0.001),sideB,sideMasks);
|
||||
float edgeProximity=1.0-saturate((dominant-second)*100.0);
|
||||
debugColor=float3(saturate(abs(sideA.x-sideB.x)*100.0)*edgeProximity,edgeProximity,(hasA&&hasB)?0.0:1.0);
|
||||
}
|
||||
else if(renderDebugMode>=26.5&&renderDebugMode<27.5)
|
||||
{
|
||||
float4 pageField; float2 pageMasks; bool hasPage=zyTrySampleErosionPage(terrainDir,pageField,pageMasks);
|
||||
float directHeight=hasDebugPage?zyAdvancedErosion(terrainDir,zySphericalField(terrainDir),planet.state.x,max(debugMetadata.state.w,1.0e-5)).x:0.0;
|
||||
float error=abs(directHeight-pageField.x);
|
||||
float bound=hasDebugPage?max(debugSummary.bounds.w,1.0e-5):1.0;
|
||||
debugColor=hasPage?float3(saturate(error/bound),saturate(error*100.0),error<=bound?0.0:1.0):float3(1,0,1);
|
||||
}
|
||||
else if(renderDebugMode>=27.5&&renderDebugMode<28.5)
|
||||
{
|
||||
float spacing=hasDebugPage?max(debugMetadata.state.w,1.0e-6):1.0;
|
||||
float wavelengthRatio=hasDebugPage?saturate(log2(max(planet.state.x,0.001)/spacing)/16.0):0.0;
|
||||
debugColor=float3(frac(zy_planet_page_set[0].state.z),wavelengthRatio,hasDebugPage?saturate(debugMetadata.address.y/5.0):0.0);
|
||||
}
|
||||
SdfSurface surface=sdfSurface(p,0);
|
||||
float3 shaded=(renderDebugMode>=21.5&&renderDebugMode<28.5)?debugColor:shadeSdf(0.0,travel,p,normal,0,surface);
|
||||
ZyPatchSceneOut output; output.colorTravel=float4(shaded,min(travel,farDistance+1.0));
|
||||
output.metadata=float4(FIELD_ID_SDF_OBJECT_BASE,normal); output.control=float4(1,4.0/384.0,saturate(surface.temporalDetail),saturate(surface.reservoirConfidence));
|
||||
float3 forward,right,up; cameraBasis(cameraPosition,cameraTarget,forward,right,up);
|
||||
output.reservoirGuide=float4(saturate(surface.reservoirConfidence),0,1,0); output.overdraw=1;
|
||||
output.depth=saturate(dot(p-cameraPosition,forward)/max(farDistance,0.0001)); return output;
|
||||
}
|
||||
104
src/Aquarium.Zyphos/ZyphosPlanetarySurfacePages.cs
Normal file
104
src/Aquarium.Zyphos/ZyphosPlanetarySurfacePages.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Render;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Zyphos;
|
||||
|
||||
internal static class ZyphosPlanetarySurfacePages
|
||||
{
|
||||
private const int InteriorSize = 17;
|
||||
private const int BorderSize = 2;
|
||||
private const int MaximumLevel = 5;
|
||||
private const float ArrivalSeconds = 0.35f;
|
||||
private static readonly Dictionary<ulong, CachedPageContent> Content = [];
|
||||
private static readonly PlanetaryResidualResidency Residency = new();
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
Residency.Reset();
|
||||
}
|
||||
|
||||
internal static void PrimeForCapture(Vector3 cameraPosition, float pageAgeSeconds)
|
||||
{
|
||||
Reset();
|
||||
_ = ForCamera(cameraPosition, -Math.Max(pageAgeSeconds, 0.0f));
|
||||
}
|
||||
|
||||
internal static void PrimeEvictionForCapture(Vector3 cameraPosition, float pageAgeSeconds)
|
||||
{
|
||||
var center=ZyphosUmbrosSystem.ZyphosCenter;
|
||||
var relative=cameraPosition-center;
|
||||
var oldDirection=Vector3.Normalize(new Vector3(relative.Z,relative.Y,-relative.X));
|
||||
var oldCamera=center+oldDirection*Math.Max(relative.Length(),0.001f);
|
||||
Reset();
|
||||
_=ForCamera(oldCamera,-1.0f);
|
||||
_=ForCamera(oldCamera,-0.65f);
|
||||
_=ForCamera(cameraPosition,-Math.Max(pageAgeSeconds,0.0f));
|
||||
}
|
||||
|
||||
public static AquariumPlanetarySurfacePageSet ForCamera(Vector3 cameraPosition, float timeSeconds)
|
||||
{
|
||||
var relative = cameraPosition - ZyphosUmbrosSystem.ZyphosCenter;
|
||||
var distance = Math.Max(relative.Length(), 0.001f);
|
||||
var direction = relative / distance;
|
||||
var field = FieldDefinition(ZyphosUmbrosSystem.ZyphosSurfaceRadius);
|
||||
var lod = new PlanetaryLodParameters(MaximumLevel, InteriorSize, BorderSize, 720, field.Radius / 65536);
|
||||
var desired = PlanetaryLodSelector.SelectAncestorChain(field, (float3)direction, distance, lod);
|
||||
var snapshot = Residency.Update(desired, timeSeconds, ArrivalSeconds);
|
||||
var pages = snapshot.Tiles.Select(resident => BuildPage(resident.Tile, resident.Blend)).ToArray();
|
||||
return new AquariumPlanetarySurfacePageSet
|
||||
{
|
||||
GeneratorEntryPoint = "D3D12ZyphosTerrainPageCS",
|
||||
ContentVersion = unchecked((long)snapshot.ContentVersion),
|
||||
PresentationVersion = unchecked((long)snapshot.PresentationVersion),
|
||||
Pages = pages,
|
||||
RenderCoarsePatches = true,
|
||||
PatchCells = 64,
|
||||
};
|
||||
}
|
||||
|
||||
private static AquariumPlanetarySurfacePage BuildPage(PlanetaryTileAddress tile, float blend)
|
||||
{
|
||||
var layout = new PlanetaryPageLayout(tile, InteriorSize, BorderSize);
|
||||
var key = tile.StableKey;
|
||||
var radius = ZyphosUmbrosSystem.ZyphosSurfaceRadius;
|
||||
if (!Content.TryGetValue(key, out var cached))
|
||||
{
|
||||
var common = PlanetaryGpuPageBuilder.BuildContent(layout, radius);
|
||||
var samples = common.Inputs.Select(input => new AquariumPlanetarySurfacePageInput(
|
||||
ToNumerics(input.DirectionRadius), ToNumerics(input.Sampling))).ToArray();
|
||||
cached = new(common, samples);
|
||||
Content[key] = cached;
|
||||
}
|
||||
|
||||
var metadata = PlanetaryGpuPageBuilder.Metadata(cached.Common, 0, blend);
|
||||
|
||||
return new AquariumPlanetarySurfacePage
|
||||
{
|
||||
ContentKey = unchecked((long)key),
|
||||
Samples = cached.Samples,
|
||||
Metadata = new AquariumPlanetarySurfacePageMetadata(
|
||||
ToNumerics(metadata.Address),
|
||||
ToNumerics(metadata.Layout),
|
||||
ToNumerics(metadata.Bounds),
|
||||
ToNumerics(metadata.State)),
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector4 ToNumerics(float4 value) => new(value.x, value.y, value.z, value.w);
|
||||
|
||||
private sealed record CachedPageContent(
|
||||
PlanetaryGpuPageContent Common,
|
||||
AquariumPlanetarySurfacePageInput[] Samples);
|
||||
|
||||
private static PlanetaryFieldDefinition FieldDefinition(float radius)
|
||||
{
|
||||
var erosion = new AdvancedErosionParameters(
|
||||
radius * 0.075f, 0.12f, 0.58f, 1.45f,
|
||||
new float4(0.1f, 0.015f, 0.1f, 2.0f),
|
||||
new float4(1.25f, 1.25f, 2.8f, 1.5f),
|
||||
new float2(0.7f, 0.85f), 0.7f, 0.5f, 7, 2.0f, 0.5f);
|
||||
return new(1, radius, 0, erosion);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,11 @@ public static class ZyphosRenderPlan
|
|||
var app = new AquariumApp();
|
||||
app.Shaders
|
||||
.Core("D3D12HeightField.hlsl", "D3D12Scene.hlsl", "D3D12Post.hlsl")
|
||||
.PlanetarySurfacePage("D3D12ZyphosTerrainPage.hlsl")
|
||||
.PlanetarySurfacePageSummary("D3D12ZyphosTerrainPageSummary.hlsl")
|
||||
.PlanetarySurfacePatch("D3D12ZyphosTerrainPatch.hlsl")
|
||||
.SdfShader("D3D12ZyphosPlanet.hlsl")
|
||||
.SdfShader("D3D12ZyphosUmbros.hlsl")
|
||||
.SdfShader("D3D12ZyphosStar.hlsl");
|
||||
|
||||
var heightField = app.RenderTargets.Create("height-field")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using Aquarium.Engine;
|
||||
using Aquarium.Engine.Audio;
|
||||
using Aquarium.Engine.Input;
|
||||
|
|
@ -70,13 +71,53 @@ public sealed class ZyphosRuntime : IAquariumRuntime
|
|||
shot.CameraTarget,
|
||||
timeSeconds,
|
||||
Vector2.Zero,
|
||||
ZyphosSceneBuilder.Build(timeSeconds, previousTimeSeconds, fractalPlan));
|
||||
ZyphosSceneBuilder.Build(timeSeconds, previousTimeSeconds, shot.CameraPosition, fractalPlan));
|
||||
}
|
||||
}
|
||||
|
||||
public ZyphosRuntime()
|
||||
{
|
||||
Ui = new AquariumUiDocument()
|
||||
var captureDomain = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_DOMAIN");
|
||||
if (!string.IsNullOrWhiteSpace(captureDomain))
|
||||
{
|
||||
var domain = ZyphosSpatialDomainCatalog.Domains.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Key.Value, captureDomain, StringComparison.OrdinalIgnoreCase));
|
||||
if (domain is not null)
|
||||
{
|
||||
selectedDomainKey = domain.Key;
|
||||
autoOrbit = false;
|
||||
}
|
||||
}
|
||||
if (float.TryParse(
|
||||
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_ORBIT_DISTANCE"),
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var captureDistance))
|
||||
{
|
||||
orbitDistance = ClampOrbitDistance(captureDistance);
|
||||
autoOrbit = false;
|
||||
}
|
||||
if (float.TryParse(
|
||||
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_PAGE_AGE_SECONDS"),
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var pageAgeSeconds))
|
||||
{
|
||||
timeScale = 0.0f;
|
||||
autoOrbit = false;
|
||||
ZyphosPlanetarySurfacePages.PrimeForCapture(CurrentShot().CameraPosition, pageAgeSeconds);
|
||||
}
|
||||
if (float.TryParse(
|
||||
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_EVICTION_AGE_SECONDS"),
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var evictionAgeSeconds))
|
||||
{
|
||||
timeScale = 0.0f;
|
||||
autoOrbit = false;
|
||||
ZyphosPlanetarySurfacePages.PrimeEvictionForCapture(CurrentShot().CameraPosition, evictionAgeSeconds);
|
||||
}
|
||||
var ui = new AquariumUiDocument()
|
||||
.Panel("Zyphos", 18.0f, 82.0f, 340.0f, panel =>
|
||||
{
|
||||
panel.Section("Planetary Demo");
|
||||
|
|
@ -119,6 +160,7 @@ public sealed class ZyphosRuntime : IAquariumRuntime
|
|||
.Command("zyphos-fractal", _ => ZyphosFractalTerrain.DebugDump, "Dump the compiled Zyphos fractal terrain grammar.")
|
||||
.Command("zyphos-fractal-plan", _ => ZyphosFractalTerrain.BuildPlanDebugDump(ZyphosFractalTerrain.BuildRenderPlan(CurrentShot())), "Dump the current Zyphos fractal selected cut and resource budget.")
|
||||
.Command("zyphos-system", _ => $"Zyphos-Umbros: separation {ZyphosUmbrosSystem.SeparationInZyphosRadii:0.0} Zyphos radii, Umbros radius {ZyphosUmbrosSystem.UmbrosRadiusRatio:0.00} Zyphos, apparent diameter {ZyphosUmbrosSystem.UmbrosAngularDiameterDegrees:0.0} degrees.", "Report the modeled Zyphos/Umbros/star baseline.");
|
||||
Ui = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_HIDE_UI") == "1" ? AquariumUiDocument.Empty : ui;
|
||||
}
|
||||
|
||||
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input)
|
||||
|
|
@ -129,6 +171,8 @@ public sealed class ZyphosRuntime : IAquariumRuntime
|
|||
public void Start()
|
||||
{
|
||||
Console.WriteLine("Zyphos planetary demo booted.");
|
||||
var shot=CurrentShot();
|
||||
Console.WriteLine($"Zyphos camera: position {shot.CameraPosition}; target {shot.CameraTarget}; planet distance {Vector3.Distance(shot.CameraPosition,ZyphosUmbrosSystem.ZyphosCenter):0.###}");
|
||||
}
|
||||
|
||||
public void Update(float deltaSeconds, InputState input)
|
||||
|
|
|
|||
|
|
@ -6,21 +6,22 @@ namespace Aquarium.Zyphos;
|
|||
public static class ZyphosSceneBuilder
|
||||
{
|
||||
public static AquariumSceneState Build(float timeSeconds, float previousTimeSeconds, ZyphosFractalRenderPlan fractalPlan)
|
||||
=> Build(timeSeconds, previousTimeSeconds, ZyphosUmbrosSystem.ZyphosCenter + Vector3.UnitZ * 12.0f, fractalPlan);
|
||||
|
||||
public static AquariumSceneState Build(float timeSeconds, float previousTimeSeconds, Vector3 cameraPosition, ZyphosFractalRenderPlan fractalPlan)
|
||||
{
|
||||
return new AquariumSceneState
|
||||
{
|
||||
TraceHeightFieldSurface = false,
|
||||
UseStarfieldBackground = true,
|
||||
HeightFieldBrushes = fractalPlan.HeightBrushes,
|
||||
FractalReservoirField = new AquariumFractalReservoirField
|
||||
{
|
||||
SplatCount = 2_000_000,
|
||||
SplatUpdatesPerFrame = 50_000,
|
||||
ReservoirUpdatesPerPass = 20_000,
|
||||
WorldCenterRadius = new Vector4(ZyphosUmbrosSystem.ZyphosCenter, ZyphosUmbrosSystem.ZyphosSurfaceRadius),
|
||||
PriorityFocus = fractalPlan.ReservoirPriorityFocus,
|
||||
ProgramTransforms = fractalPlan.GpuProgramTransforms,
|
||||
},
|
||||
// Planet visibility belongs to the page-backed raster patches.
|
||||
// The fractal grammar still feeds authored terrain inputs, but its
|
||||
// legacy splats may not independently repaint the same surface.
|
||||
FractalReservoirField = AquariumFractalReservoirField.Empty,
|
||||
PlanetarySurfacePages = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_DISABLE_PLANETARY_PAGES") == "1"
|
||||
? AquariumPlanetarySurfacePageSet.Empty
|
||||
: ZyphosPlanetarySurfacePages.ForCamera(cameraPosition, timeSeconds),
|
||||
SdfObjects = BuildSdfObjects(timeSeconds, previousTimeSeconds),
|
||||
SdfLights = BuildSdfLights(timeSeconds),
|
||||
};
|
||||
|
|
@ -29,10 +30,22 @@ public static class ZyphosSceneBuilder
|
|||
private static AquariumSdfObject[] BuildSdfObjects(float timeSeconds, float previousTimeSeconds)
|
||||
{
|
||||
var rotation = ZyphosUmbrosSystem.MutualPhase(timeSeconds);
|
||||
var previousRotation = ZyphosUmbrosSystem.MutualPhase(previousTimeSeconds);
|
||||
var umbrosCenter = ZyphosUmbrosSystem.UmbrosCenter(timeSeconds);
|
||||
var previousUmbrosCenter = ZyphosUmbrosSystem.UmbrosCenter(previousTimeSeconds);
|
||||
var starCenter = ZyphosUmbrosSystem.PrimaryStarCenter(timeSeconds);
|
||||
var previousStarCenter = ZyphosUmbrosSystem.PrimaryStarCenter(previousTimeSeconds);
|
||||
|
||||
var objects = new AquariumSdfObject[ZyphosRenderPlan.SdfObjectCount];
|
||||
var planetBoundRadius = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_ENABLE_PLANET_SDF_ORACLE") == "1" ? ZyphosUmbrosSystem.ZyphosBoundRadius : 0.0f;
|
||||
objects[ZyphosRenderPlan.PlanetIndex] = new AquariumSdfObject(
|
||||
new Vector4(ZyphosUmbrosSystem.ZyphosCenter, planetBoundRadius),
|
||||
new Vector4(ZyphosUmbrosSystem.ZyphosCenter, previousRotation),
|
||||
new Vector4(ZyphosUmbrosSystem.ZyphosSurfaceRadius, rotation, ZyphosUmbrosSystem.SeaLevel, rotation));
|
||||
objects[ZyphosRenderPlan.UmbrosIndex] = new AquariumSdfObject(
|
||||
new Vector4(umbrosCenter, ZyphosUmbrosSystem.UmbrosBoundRadius),
|
||||
new Vector4(previousUmbrosCenter, 0.0f),
|
||||
new Vector4(ZyphosUmbrosSystem.UmbrosSurfaceRadius, rotation, 0.0f, 0.0f));
|
||||
objects[ZyphosRenderPlan.StarIndex] = new AquariumSdfObject(
|
||||
new Vector4(starCenter, ZyphosUmbrosSystem.PrimaryStarVisualRadius),
|
||||
new Vector4(previousStarCenter, 0.0f),
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ because the schedule got nervous.
|
|||
|
||||
Saga lives inside Fensalir because Fensalir already owns the machinery it needs:
|
||||
|
||||
- `CubeTileKey` and cube-sphere projection math;
|
||||
- CultMath `PlanetaryTileAddress` and cube-sphere projection math;
|
||||
- field evidence layers, especially `Transport`;
|
||||
- selected cuts and residency planning;
|
||||
- GPU resource pages and debug telemetry;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Saga;
|
||||
|
|
@ -19,7 +20,7 @@ public readonly record struct SagaFlowDomain(
|
|||
|
||||
public readonly record struct SagaFlowRequest(
|
||||
SagaFlowDomain Domain,
|
||||
CubeTileKey Tile,
|
||||
PlanetaryTileAddress Tile,
|
||||
int SampleResolution,
|
||||
double TimeSeconds,
|
||||
SagaCoriolisFrame Coriolis,
|
||||
|
|
|
|||
|
|
@ -305,7 +305,38 @@
|
|||
{"timestamp":"2026-05-30T01:40:00+01:00","topic":"reservoir-budget-pressure-probe","lesson":"The reservoir quality harness now converts same-time higher-work-grid reference error into per-tile budget pressure without making the probe a runtime owner. measure-reservoir-budget-pressure.ps1 reads a sequence manifest, compares a candidate final capture against the native-domain reference final capture at the same ready frame, applies disocclusion/rejection masks when present, and ranks fixed tiles by mean reference error times mask coverage. On reservoir-budget-pressure-20260530-001046.* the probe measured 1306/42864 pixels and found the top 5/50 tiles carried 99.3305% of measured reference-error delta, giving a concrete target for adaptive update policy while preserving the no-native-resolution-reservoir invariant.","files":["scripts/measure-reservoir-budget-pressure.ps1","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-budget-pressure-20260530-001046.*"}
|
||||
{"timestamp":"2026-05-30T01:58:00+01:00","topic":"reservoir-spatial-reuse-budget","lesson":"The renderer now has a first runtime sampling budget inside the reservoir history update owner. GraphicsSettings.FieldReservoirSpatialReuseBudget flows through UI, runtime options, CLI/env overrides, capture scripts, frame constants, and D3D12ReservoirHistoryUpdateCS. Current and temporal rows are still written for every reservoir work-grid pixel; only the 3x3 spatial neighbor reuse pass is spent on the deterministic budget phase. The default is 0.5, so the machine combines half presentation grid reservoir surfaces with half spatial reuse sampling by default. Smoke reservoir-sequence-20260530-003006-* compared native scale 0.5/spatial budget 0.5 against reference scale 0.75/spatial budget 1.0; masked reference-error-native covered 3.8261% of frame pixels, and reservoir-budget-pressure-20260530-003006.* found the top 5/50 tiles carried 90.8321% of measured reference-error delta.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/AquariumHost.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","scripts/capture-fensalir-frame.ps1","scripts/capture-reservoir-mode-sequence.ps1","tests/Aquarium.Engine.Tests/FieldReservoirAbiTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-sequence-20260530-003006-*"}
|
||||
{"timestamp":"2026-05-30T02:10:00+01:00","topic":"reservoir-spatial-budget-debug","lesson":"The spatial reuse sampling budget now has a visible owner-layer probe. Render debug mode 20, Reservoir Spatial Budget, colors work-grid pixels that spend 3x3 spatial reuse green and skipped pixels blue; capture-reservoir-mode-sequence.ps1 names the mode spatial-budget. Smoke reservoir-sequence-20260530-003746-* captured the debug view at native scale 0.5/spatial budget 0.5. A bitmap probe on the 304x141 present output counted 32078 green pixels, 6818 blue pixels, and 3968 filtering/other pixels, proving the budget branch is observable where the user sees it.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","scripts/capture-reservoir-mode-sequence.ps1","tests/Aquarium.Engine.Tests/FieldReservoirAbiTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-sequence-20260530-003746-*"}
|
||||
{"timestamp":"2026-05-30T22:02:00+01:00","topic":"d3d12-resize-reference-release","lesson":"The Fensalir resize crash was a hidden-reference ownership bug in the swapchain path. Resize disposed DirectWrite overlays and D3D11-on-12 wrapped backbuffers, but the D3D11 immediate context could still retain references unless it was explicitly cleared and flushed before IDXGISwapChain::ResizeBuffers. Resize now clears/flushes the overlay context and drops program-output shared textures before recreating size-dependent surfaces. A visible dev slot survived seven programmatic window resizes and logged target rebuilds without stderr.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","tests/Aquarium.Engine.Tests/D3D12ResizeTests.cs","state/scratch.md","state/map.yaml"],"source":"artifacts/dev-reload/latest.out.log"}
|
||||
{"timestamp":"2026-05-31T02:58:00+01:00","topic":"eve-direct2d-client-surface","lesson":"Fensalir's DirectWrite/Direct2D overlay is a first-class Eve client surface. It should consume shared Eve/CultMesh UI documents and lower them into AquariumUiDocument, while provider repos keep ownership of dashboard state, accepted commands, and app-specific semantics.","files":["docs/eve-direct2d-client-surface.md","README.md","docs/aquarium-engine-doctrine.md","state/map.yaml"],"source":"Eve cross-client surface cut"}
|
||||
{"timestamp":"2026-05-30T22:24:00+01:00","topic":"reservoir-reconstruction-loss-curve","lesson":"Reservoir superresolution quality now has a curve metric instead of only smoke deltas. measure-reservoir-loss-curve.ps1 reads candidate/reference sequence manifests and reports MAE, MSE, RMSE, PSNR, changed pixels, max channel delta, and optional disocclusion/rejection-masked loss. The 320x180 smoke sweep reservoir-sequence-20260530-221043-* compared native scale 0.5/spatial budget 0.5 against reference scale 0.75/spatial budget 1.0 at ready frames 1,2,4,8. Whole-frame PSNR was 21.668, 20.981, 20.911, and 21.690 dB; the non-monotonic curve means current sampling/reconstruction is not yet converging cleanly against the higher-budget reference. Performance still needs GPU timestamp queries; capture wall time is process-level and not a pass cost metric.","files":["scripts/measure-reservoir-loss-curve.ps1","scripts/capture-reservoir-mode-sequence.ps1","state/scratch.md","state/map.yaml"],"source":"artifacts/fensalir-captures/reservoir-loss-curve-20260530-221043.csv"}
|
||||
{"timestamp":"2026-05-30T00:29:26+01:00","topic":"surfacepage-uav-depth-socket","lesson":"SurfacePage resources can now be GPU-resident unordered-access outputs as well as shader-readable pages. D3D12FieldTexture2D tracks UAV capability, creates UAV descriptors, and D3D12FieldResourceRegistry resolves UnorderedAccess SurfacePages while reusing empty pages across version changes when shape/format/access are unchanged. This gives Mimir stereo-depth/height outputs a real compute-writable resource socket without private output textures or CPU readback.","files":["src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs","tests/Aquarium.Engine.Tests/D3D12SharedTextureInteropTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter RegistryResolvesUnorderedAccessSurfacePageForComputeDepthOutput"}
|
||||
{"timestamp":"2026-05-30T00:37:29+01:00","topic":"stereo-depth-lowering-contract","lesson":"FieldEvidence now carries typed stereo-depth lowering metadata for future D3D12 SGM dispatch. AquariumFieldStereoDepthLowering binds a Height claim to shader-readable left/right texture resources, profile/calibration/camera-pair identity, depth range/disparity settings, and a compute-writable UAV disparity SurfacePage. Validation rejects claim encoding mismatches, payload/resource mismatches, non-UAV disparity outputs, and missing/non-readable inputs. This keeps Mimir libSGM-provenance depth inside Fensalir resource authority rather than a private texture, CPU readback, CUDA, or TensorRT path.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","tests/Aquarium.Engine.Tests/FieldEvidenceContractTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter StereoDepth"}
|
||||
{"timestamp":"2026-05-30T00:43:53+01:00","topic":"stereo-depth-dispatch-readiness","lesson":"D3D12Renderer now evaluates stereo-depth lowerings for dispatch readiness without installing a fake depth kernel. A lowering is counted ready only when the SurfacePage Height claim is planned, left/right input Texture2D resources resolve through the field resource registry, and the disparity SurfacePage resolves with UAV capability. Capacity logs report ready/unplanned/unresolved counts and explicitly say the kernel is not installed.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore"}
|
||||
{"timestamp":"2026-05-30T00:45:50+01:00","topic":"stereo-depth-sgm-kernel-constants","lesson":"AquariumFieldStereoDepthLowering now carries SGM-shaped kernel constants instead of leaving the future D3D12 shader to infer policy: min disparity, disparity levels, aggregation path count, census radius, and P1/P2 smoothness penalties. Validation requires positive census/P1/P2 values and P2 \u003e= P1. This preserves libSGM-style provenance as typed dispatch data before the HLSL kernel exists.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","tests/Aquarium.Engine.Tests/FieldEvidenceContractTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter StereoDepth"}
|
||||
{"timestamp":"2026-05-30T21:59:00+01:00","topic":"packed-leap-stereo-depth-kernel","lesson":"D3D12Renderer now installs and dispatches the first stereo-depth compute lane for the live Mimir Leap shape. D3D12StereoDepth.hlsl samples a packed R8G8 Texture2D as left/right IR lanes, performs a small local SAD/block match over the lowering's disparity/radius constants, and writes R16F disparity to the planned UAV SurfacePage. The dispatch only runs for planned Height claims whose left/right keys are the same packed texture and whose input/output resources resolve. This is a live depth proof and provenance-bearing socket for the later libSGM-style SGM rebuild, not calibrated metric depth or full SGM.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12StereoDepth.hlsl","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; temp Vortice.D3DCompiler probe compiled D3D12PackedStereoDepthCS bytes=2168; full headless host compile attempted but hung before capture"}
|
||||
{"timestamp":"2026-05-30T22:33:00+01:00","topic":"headless-pipeline-hang-root-cause","lesson":"The apparent Fensalir headless hang was an initial shader pipeline failure hidden behind the loading-frame loop. dotnet-stack showed the main thread sleeping in D3D12Renderer.RenderPipelineLoadingFrame; a dump of the background pipeline task revealed FileNotFoundException for Render/Shaders/D3D12SdfCommon.hlsli beside the Aquarium.Fensalir client shader output. D3D12FensalirSpine.hlsl includes engine SDF shared files by relative path, but Aquarium.Fensalir.csproj only copied client shaders. The fix copies D3D12SdfCommon.hlsli, D3D12SdfMath.hlsli, and D3D12SdfProxy.hlsli into the client shader output and makes initial pipeline failure throw instead of spinning silently. Verification: Fensalir and Engine builds passed, then headless Engine run compiled pipelines, captured artifacts/runtime/fensalir-stereo-depth-pipeline-smoke.png, and exited.","files":["src/Aquarium.Fensalir/Aquarium.Fensalir.csproj","src/Aquarium.Engine/Render/D3D12Renderer.cs","state/evidence.jsonl","state/map.yaml"],"source":"dotnet-stack report plus dotnet-dump analyze on stuck headless process; dotnet run --project src/Aquarium.Engine/Aquarium.Engine.csproj --no-build -- --headless --capture-frame artifacts/runtime/fensalir-stereo-depth-pipeline-smoke.png"}
|
||||
{"ts":"2026-05-30T23:10:27+01:00","type":"decision","status":"ok","note":"Added the first Fensalir Leap point-cloud render lane: Mesh resources whose SourceUri is derived-from a SurfacePage allocate UAV-capable point-list buffers, D3D12PointCloudFromDisparityCS fills PositionNormalUvColor vertices/indices from disparity, and D3D12PointCloudPS renders them into scene candidate MRTs. Verified engine/client build, stereo/resize tests, and headless shader pipeline capture artifacts/runtime/fensalir-point-cloud-pipeline-smoke.png."}
|
||||
{"timestamp":"2026-05-30T23:41:55+01:00","topic":"reservoir-presentation-reconstruction","lesson":"The first final-pass reservoir reconstruction denoiser is worth keeping only after maturity gating. The shader samples a bounded resolved-reservoir neighborhood in D3D12ReservoirPresentationResolvePS and weights neighbors by field id, travel, normal, coverage, detail, color, guide confidence, domain validity, and neighbor agreement. Ungated and age-eager versions improved some frames but regressed the curve; the kept gate requires reservoir maturity before filter strength is allowed. Latest 320x180 sweep reservoir-sequence-20260530-232852-* versus higher-work-grid reference improved f2/f4/f8 and reduced average RMSE from 21.944 to 21.221 against the pre-reconstruction 20260530-221043 curve, while f1 still regressed and remains the next target.","files":["src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","state/scratch.md","state/map.yaml"],"source":"artifacts/fensalir-captures/reservoir-loss-curve-20260530-232852.csv"}
|
||||
{"timestamp":"2026-05-31T23:58:00+01:00","topic":"system-loopback-audio-stems","lesson":"System speaker capture now enters Fensalir through an explicit AquariumAudioDocument capture request. AquariumSynthHost owns the WASAPI loopback device, publishes mono system_mix AquariumAudioStemFrame packets through IAquariumAudioStemBus, and clients consume those stems without owning capture APIs. The host drains pending audio requests once when the scene-ready audio host is created, so short clients do not strand Start-time requests. This closes the live-audio input seam for visualizers such as Perlines while preserving the engine audio host as the authority for OS audio I/O.","files":["src/Aquarium.Engine.Contracts/Audio/AquariumAudio.cs","src/Aquarium.Engine/AquariumHost.cs","src/Aquarium.Engine/Audio/AquariumSynthHost.cs","src/Aquarium.Engine/Audio/WasapiLoopbackCaptureDevice.cs","tests/Aquarium.Engine.Tests/SynthPlaybackTests.cs","state/map.yaml"],"source":"dotnet build Fensalir.sln; dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --no-build; scripts/dev-reload.ps1 -Headless -RetainSlots 1 -ClientProject E:\\Projects\\Perlines\\src\\Perlines\\Perlines.csproj showed WASAPI loopback started for profile perlines:system-loopback"}
|
||||
{"ts":"2026-05-31T03:52:00+01:00","type":"verification","status":"ok","note":"Mimir FieldEvidence camera Feature/SensorObservation claims now plan to Fensalir's GpuSensorFusion backend instead of deferring. D3D12Renderer derives sensor metadata and SRV bindings from resolved camera Texture2D resources and dispatches D3D12GpuSensorFusionCS. Live Mimir proof planned 12 packets with 0 deferred; headless D3D12 dispatched LeapStereoIr plus both Bayer8 PS3 Eyes at 16,384 sparse samples per texture, generated 49,152 gaussians, and reported sensor-fusion timing around 0.059 ms/frame. Kiyo YUY2 claims are selected but skipped until a format-aware YUY2 conversion/feature lane exists; packed video must not masquerade as RGBA.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs","docs/temporal-sdf-gaussian-field.md","state/map.yaml"],"source":"dotnet build Fensalir.sln; Mimir --perfect-machine-live-smoke --seconds 1.0; Mimir.App headless captured artifacts/runtime/mimir-perfect-machine-gpu-feature-extraction-headless.png"}
|
||||
{"ts":"2026-05-31T04:18:00+01:00","type":"verification","status":"ok","note":"GpuSensorFusion now samples YUY2 camera Texture2D resources instead of skipping them. Fensalir exposes DXGI_FORMAT_YUY2 through the Microsoft-documented R8G8B8A8_UNORM SRV view, passes declared pixel format into the GPU sensor camera packet, and D3D12GpuSensorFusion.hlsl decodes Y0/U/Y1/V to RGB before feature scoring. Mimir.App headless all-sensor proof dispatched five camera textures with zero unsupported field textures, generated 81,920 sparse gaussians at 16,384 samples per texture, reported sensor-fusion GPU timing around 0.08 ms/frame, and captured artifacts/runtime/mimir-perfect-machine-yuy2-gpu-feature-extraction-headless.png.","files":["src/Aquarium.Engine.Contracts/Render/AquariumRenderPlan.cs","src/Aquarium.Engine/Render/D3D12ExternalSensorTexture.cs","src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12GpuSensorFusion.hlsl","docs/temporal-sdf-gaussian-field.md","state/map.yaml"],"source":"Microsoft DXGI_FORMAT docs for YUY2 view mapping; dotnet build Fensalir.sln; Mimir.App headless captured artifacts/runtime/mimir-perfect-machine-yuy2-gpu-feature-extraction-headless.png"}
|
||||
{"timestamp":"2026-05-31T08:48:00+01:00","type":"verification","status":"ok","note":"Aquarium.EveSurface no longer assumes every Eve dashboard document is VoidBot-shaped. Generic providers now render their node list and selected detail through Surface Nodes / State Graph panels. Verified by connecting to Mimir.EveDashboard provider mimir.stream.layout and capturing the decoded surface at E:/Projects/Mimir/artifacts/runtime/mimir-eve-surface-layout-generic-decoder.png.","files":["src/Aquarium.EveSurface/EveSurfaceRuntime.cs"],"source":"dotnet build src/Aquarium.EveSurface/Aquarium.EveSurface.csproj; live Mimir.EveDashboard ws://127.0.0.1:8795/eve/deck provider mimir.stream.layout"}
|
||||
{"ts":"2026-05-31T18:06:36+01:00","type":"implementation","status":"ok","note":"AquariumUiDocument now carries Eve/CultUI-style composition surfaces in addition to legacy debug panels. Surface authoring defaults to vertical groups and uses explicit Horizontal scopes for columns; DirectWrite lowering subdivides each parent rectangle by axis and child weight instead of measuring children into sibling space, making overlap structurally impossible for lowered siblings.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj --no-restore"}
|
||||
{"ts":"2026-05-31T18:18:00+01:00","type":"implementation","status":"ok","note":"Eve/CultUI Direct2D lowering now distinguishes fixed preferred control rows from flexible panes/groups. Vertical scopes allocate compact preferred extents to leaf controls such as text, toggles, selects, sliders, metrics, and buttons, then give remaining space to flexible children by weight. This preserves the no-overlap subdivision invariant while avoiding equal-height slabs for ordinary controls.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj --no-restore; relaunched Mimir.App with mimir-runtime.desktop-editor.no-leap.mic-reference-sync.local.json"}
|
||||
{"ts":"2026-05-31T18:24:00+01:00","type":"verification","status":"ok","note":"Aquarium.Sample.Minimal now carries a reusable CultUI smoke surface so Eve/CultUI layout can be verified without Mimir camera, USB, FFT, or sync workload. The smoke surface exercises nested vertical/horizontal scopes, panes, cards, rows, selects, toggles, sliders, buttons, metrics, live text, and clipped long readouts. DirectWrite lowering now derives preferred container height from nested child layout and displays metric raw values while clamping only the fill bar; the headless capture showed readable nested grids with no sibling overlap.","files":["src/Aquarium.Sample.Minimal/MinimalRuntime.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj --no-restore; scripts/dev-reload.ps1 -ClientProject src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj; capture artifacts/fensalir-captures/cultui-smoke-minimal-fixed.png"}
|
||||
{"ts":"2026-05-31T22:05:00+01:00","type":"implementation","status":"ok","note":"Eve/CultUI surfaces now include a DirectWrite preview primitive for editor/compositor surfaces. The primitive renders a bounded 16:9 program canvas and normalized item rectangles supplied by the client, allowing Mimir to show OBS-style program arrangement without inventing a Mimir-only overlay path.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj --no-restore"}
|
||||
{"ts":"2026-05-31T22:17:00+01:00","type":"implementation","status":"ok","note":"CultUI preview elements are now flexible layout surfaces rather than fixed preferred-height controls. Containers with flexible children report as flexible to their parent, so editor panes containing a compositor preview can take the available center-panel space instead of collapsing to a small fixed strip.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; Mimir headless capture artifacts/runtime/mimir-editor-preview-maximized-headless.png"}
|
||||
{"ts":"2026-05-31T22:55:00+01:00","type":"implementation","status":"ok","note":"CultUI preview lowering treats an empty preview label as an intentional chrome-free canvas: no nested frame/title is drawn, and padding collapses to two pixels. Mimir uses this for the central OBS-style program compositor so the preview fills the editor center instead of rendering as a card-within-a-pane.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj; Mimir.App --headless --headless-width 1280 --headless-height 720 --capture-frame artifacts/runtime/mimir-editor-flat-2d-program-mixer-headless.png"}
|
||||
{"ts":"2026-06-01T13:48:00+01:00","type":"implementation","status":"ok","note":"CultUI preview elements now support direct compositor gestures. AquariumUiElement can carry a preview interaction callback, D3D12Renderer hit-tests preview item bodies and selected corner handles in normalized canvas coordinates, captures input while dragging, and DirectWriteOverlay draws visible selected corner handles. Clients still own gesture meaning; Fensalir owns the reusable UI hit-test and rendering affordance.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"}
|
||||
{"ts":"2026-06-01T14:12:00+01:00","type":"implementation","status":"ok","note":"CultUI preview affordances now include hover/active state, empty-canvas hits, drag-end events, and normalized guide rendering. D3D12Renderer emits hover/begin/drag/end interactions for preview bodies, handles, and empty canvas; DirectWriteOverlay draws hovered/active selected handles and client-provided x/y guides. Added Up/Down/PageUp/PageDown/Control key codes for editor hotkeys.","files":["src/Aquarium.Engine.Contracts/Input/InputState.cs","src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Platform/Win32Window.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"}
|
||||
{"ts":"2026-06-01T15:05:00+01:00","type":"implementation","status":"ok","note":"Eve remains the portable dashboard authority; the C# Aquarium/CultUI UI contract is the reference API shape. Added content padding and fixed-extent layout fields to retained surface documents, made pane headers conditional, and kept DirectWrite drawing plus D3D12 preview hit-testing on the same document geometry. Mimir now uses this for an edge-to-edge headerless editor shell with fixed output controls and an expanding inspector.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj -p:UseSharedCompilation=false; Mimir headless capture artifacts/runtime/mimir-output-panel-headless.png"}
|
||||
{"ts":"2026-06-01T15:40:00+01:00","type":"implementation","status":"ok","note":"AquariumUiElement now carries an optional typed AquariumUiBinding descriptor for Verse-backed CultCache/CultMesh fields: document schema/id, field path, value kind, access, authority, and optional command id. Fensalir lowerings keep using local callbacks for native actuation; the binding is provenance/state authority metadata, not renderer-owned dashboard semantics.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","state/map.yaml"]}
|
||||
{"ts":"2026-06-01T15:48:00+01:00","type":"verification","status":"ok","note":"Release build passed for Aquarium.Engine after adding AquariumUiBinding; Mimir Release builds and scene-editor smoke also passed against the updated contract.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -c Release -p:UseSharedCompilation=false; dotnet run --project E:/Projects/Mimir/src/Mimir.BufferSmoke/Mimir.BufferSmoke.csproj -c Release -- --scene-editor-smoke"}
|
||||
{"ts":"2026-06-01T20:15:00+01:00","type":"implementation","status":"ok","note":"Render debug mode 21 is now an owner-layer overdraw view. Scene/evidence pixel shaders write a fifth additive R16_FLOAT overdraw target after discard/depth eligibility; presentation samples that target as a heat map. Coverage, step count, reservoir stats, and spatial-budget modes remain separate diagnostics instead of being relabeled as overdraw.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12SdfCommon.hlsli","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Engine/Render/Shaders/D3D12TemporalGaussian.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12FractalSplatRender.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12PointCloud.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl"],"source":"dotnet build Fensalir.sln; AQUARIUM_RENDER_DEBUG_MODE=21 .\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -ClientProject src\\Aquarium.Fensalir\\Aquarium.Fensalir.csproj"}
|
||||
{"timestamp":"2026-07-12T19:25:00+01:00","topic":"stateless-spherical-erosion","lesson":"Cube-sphere erosion must be a pure planet-space point function rather than six face-local filters or a repair pass. CultMath now owns matching C# and HLSL kernels keyed by unit direction, tangent base gradient, seed, and bounded octave parameters; Zyphos composes the result after its continental field, while cube faces remain lookup and scheduling charts.","files":["E:\\Projects\\CultMath\\src\\CultMath\\SphericalErosion.cs","E:\\Projects\\CultMath\\shaders\\CultMath.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","state/map.yaml"],"source":"user requested a C# CPU implementation in CultMath matching the GPU code"}
|
||||
{"timestamp":"2026-07-12T20:30:00+01:00","topic":"advanced-erosion-gpu-parity","lesson":"Float-based cell hashes diverged enough between C# and HLSL to select different erosion pivots across a 2052-sample corpus. CultMath now uses the same integer avalanche hash in both paths, and Fensalir verifies the actual HLSL kernel with a standalone D3D12 compute/readback probe instead of inferring parity from similar source text or booting the renderer.","files":["E:\\Projects\\CultMath\\src\\CultMath\\AdvancedErosionFilter.cs","E:\\Projects\\CultMath\\shaders\\AdvancedErosionFilter.hlsl","src/Aquarium.Engine/Render/D3D12ComputeProbe.cs","src/Aquarium.Engine/Render/D3D12ShaderCompiler.cs","tests/Aquarium.Engine.Tests/D3D12AdvancedErosionParityTests.cs"],"source":"measured CPU/GPU parity implementation"}
|
||||
{"timestamp":"2026-07-12T21:00:00+01:00","topic":"erosion-physical-frequency-bands","lesson":"Planetary erosion LOD is now a filter over one world-space spectrum: CultMath selects active octaves from physical wavelength and sample spacing, fractionally weights the last resolvable octave, and reports omitted height. Zyphos uses that contract through smooth planet-space triplanar charts, so changing view footprint changes represented bands rather than switching terrain authorities.","files":["E:\\Projects\\CultMath\\src\\CultMath\\ErosionFrequencyBands.cs","E:\\Projects\\CultMath\\src\\CultMath\\AdvancedErosionFilter.cs","E:\\Projects\\CultMath\\shaders\\AdvancedErosionFilter.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl"],"source":"phase 3 of stateless planetary erosion rendering plan"}
|
||||
{"timestamp":"2026-07-12T21:35:00+01:00","topic":"planetary-page-boundary-authority","lesson":"Using each tile's locally measured angular texel size caused independently generated sibling pages to choose different erosion weights at the same shared boundary. Band selection now uses a level-global nominal angular footprint; local projection distortion remains summary evidence. GPU-generated sibling boundaries are identical and borders sample the spherical field without neighbor residency.","files":["src/Aquarium.Engine.Fractal/Lod/PlanetarySurfacePage.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 4 cross-page GPU verification"}
|
||||
{"timestamp":"2026-07-12T22:00:00+01:00","topic":"planetary-page-summary-reduction","lesson":"Planetary erosion pages now reduce minimum/maximum displacement, maximum tangent slope, and maximum unresolved height through the GPU kernel and verify those results against CPU reduction. These summaries are the conservative evidence needed by later intersection and residency; they are not inferred from parameters.","files":["src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPageSummary.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 4 GPU summary gate"}
|
||||
{"timestamp":"2026-07-12T23:15:00+01:00","topic":"page-backed-planetary-intersection","lesson":"Planetary erosion rendering now generates a persistent D3D12 page and GPU summary, binds both to Zyphos SDF tracing, uses measured displacement/slope/unresolved evidence for bounds and conservative radial stepping, and retains the shared bracketed sign-change refiner. Compiling the previously unregistered planet proxy proved that advanced erosion cannot remain nested in the Shader Model 5 pixel trace; exact direct evaluation is therefore the CPU/compute oracle, while visible misses use the resident ancestor or base field. Zyphos now registers and publishes planet, Umbros, and star in matching object-index order.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","src/Aquarium.Zyphos/ZyphosRenderPlan.cs","src/Aquarium.Zyphos/ZyphosSceneBuilder.cs","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 5 compile, D3D12 summary, lifecycle, and direct-versus-page hit verification"}
|
||||
{"timestamp":"2026-07-13T00:10:00+01:00","topic":"planetary-residual-quadtree-lod","lesson":"Planetary LOD now keeps six root pages and represents a selected camera path as parent low frequencies plus child-only residual bands. CultMath's declared unresolved-height bound divided by projected world footprint chooses the level. Content and presentation versions are separate, so arrival/departure fades update metadata without regenerating pages; departing chains remain resident through zero weight before eviction. GPU tests prove parent plus child residual reconstructs the child filtered field, and timeline tests cover descent, teleport, midpoint, eviction, and reload. Visible capture evidence is still required before claiming the no-pop Phase 6 exit.","files":["src/Aquarium.Engine.Contracts/Render/AquariumRenderPlan.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Zyphos/ZyphosPlanetarySurfacePages.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 6 residual LOD implementation and numerical/timeline verification"}
|
||||
{"timestamp":"2026-07-13T00:40:00+01:00","topic":"planetary-composed-differentials","lesson":"Planetary pages now carry a full 3D planet-space tangent gradient in world-distance units. Zyphos combines that derivative with the base/authored terrain differential and the erosion-times-land product derivative, then derives its visible radial-graph normal from the same composed field. The generic SDF proxy delegates normals through a client hook instead of differentiating the conservatively scaled trace distance. Ridge and gully outputs from the same pages now affect planet color and roughness, and the CPU surface-normal helper implements the same contract.","files":["src/Aquarium.Engine.Fractal/Lod/PlanetarySurfacePage.cs","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPageSummary.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 7 differential, normal, material, CPU/GPU contract verification"}
|
||||
|
|
|
|||
193
state/map.yaml
193
state/map.yaml
|
|
@ -50,6 +50,11 @@ invariants:
|
|||
- Fensalir may own reusable built-in field resources, such as the blackbody
|
||||
ramp, when the resource is generic engine utility. Clients should reference
|
||||
those through contracts instead of hardcoded project-local asset paths.
|
||||
- Client shaders that include engine shared HLSL files must receive those
|
||||
includes in their output shader directory or use explicit engine include
|
||||
paths. Missing shared includes are shader-pipeline failures, not loading
|
||||
states. Initial pipeline build failure now throws instead of spinning behind
|
||||
the startup/loading frame.
|
||||
- Epiphany Aquarium depends on Fensalir; Fensalir must not depend on Epiphany.
|
||||
- Demo projects inside this repo are allowed to exercise engine features but must not become hidden engine policy.
|
||||
- Public `Aquarium.*` assembly names remain for continuity until a deliberate namespace/package migration.
|
||||
|
|
@ -66,6 +71,98 @@ invariants:
|
|||
terrain should be a quadtree of brush-painted surface tiles, subdivided by
|
||||
projected footprint, curvature, brush/material detail, silhouettes, and
|
||||
temporal uncertainty before SDF probes emit detail-adaptive surface splats.
|
||||
- Deterministic planetary geometry is owned by GameCult.Geometry's scale-aware
|
||||
planetary field runtime. It owns field identity, point/batch and
|
||||
double-position CPU queries, tangent/QSC cube-sphere topology, bordered
|
||||
pages and child residual composition, projected-error LOD, residual
|
||||
lifecycle, ray/path/region queries, coarse patch topology, and map
|
||||
projections/tiles. GameCult.Geometry maintains matching HLSL for field evaluation,
|
||||
topology, page interpolation/residuals, and radial refinement. Clients own
|
||||
base fields, terrain parameters, gameplay meaning, and presentation policy.
|
||||
- The production lowering for stateless planetary erosion is specified in
|
||||
docs/stateless-planetary-erosion-rendering-plan.md: direct evaluation is the
|
||||
oracle, while GPU-generated cube-sphere pages carry cached displacement,
|
||||
differential, mask, bound, and frequency-band evidence for rendering.
|
||||
- CPU/GPU terrain parity is measured through a standalone D3D12 compute and
|
||||
readback probe. Shader include expansion is owned by D3D12ShaderCompiler
|
||||
and shared with the renderer; parity probes must not boot a swapchain or
|
||||
duplicate GameCult.Geometry equations.
|
||||
- Planetary erosion frequencies are selected in world-distance wavelengths
|
||||
from the current sample footprint. The last resolvable octave fades
|
||||
fractionally and reports its omitted-height bound; Zyphos lowers the planar
|
||||
reference filter through smooth planet-space triplanar charts, not cube-face
|
||||
terrain ownership.
|
||||
- Planetary surface pages use one level-global nominal angular footprint for
|
||||
band selection. Per-tile projection distortion is summary evidence only;
|
||||
it may not change terrain at a shared boundary. Page borders evaluate the
|
||||
spherical field beyond tile bounds without reading neighbor residency.
|
||||
- D3D12 owns persistent planetary page input, output, metadata, and summary
|
||||
buffers, their bindings, dispatch, and draw submission. GameCult.Geometry owns the
|
||||
page layout, spherical sample directions, canonical GPU input/metadata
|
||||
payload, residual semantics, interpolation, and summaries. Client adapters
|
||||
only convert CultMath primitives to their host ABI. Client compute entry points bind the client base field to
|
||||
those functions before GPU reduction. Zyphos registers planet, Umbros, and
|
||||
star proxies in object index order and publishes all three SDF objects from
|
||||
one scene builder.
|
||||
- Production planet visibility is owned by coarse cube-sphere raster patches,
|
||||
not the general SDF proxy. Patch vertices consume the resident ancestor
|
||||
field; covered pixels perform four bounded radial corrections against the
|
||||
complete resident page field, then use the shared differential/material
|
||||
contract. Missing pages lower to the nearest resident ancestor (the base
|
||||
field at root). Direct GameCult.Geometry evaluation and the bracketed planet SDF are
|
||||
verification oracles only; neither owns production visibility.
|
||||
- D3D12 binds the page atlas, metadata, summaries, and page set to both patch
|
||||
shader stages at t81-t84. Patch projection follows the renderer's established
|
||||
camera-basis convention, including clip-space Y inversion and normalized
|
||||
forward depth. Ordinary `LessEqual` depth owns front/back visibility;
|
||||
diagnostic `Always` depth is not a production path.
|
||||
- Zyphos's legacy two-million-splat fractal reservoir is no longer a visible
|
||||
planet writer. It previously ran after the raster patch and erased the
|
||||
page-backed candidate. Fractal grammar and authored brushes remain inputs
|
||||
to terrain composition; `PlanetarySurfacePages` plus the patch lowering own
|
||||
the visible surface and corrected pixel depth.
|
||||
- Patch density is explicit page-set policy, carried to GPU in page-set state
|
||||
rather than hardcoded independently in C# and HLSL. Zyphos currently uses a
|
||||
64 by 64 grid per root face; fine relief remains a four-step per-pixel page
|
||||
refinement. Raster normals use derivatives of the refined world position,
|
||||
while CPU/direct queries retain the analytic planet-space differential.
|
||||
`AQUARIUM_PROFILE_PLANETARY_PAGES=1` deliberately regenerates unchanged
|
||||
pages so their one-shot GPU cost can be measured; production content-version
|
||||
caching remains authoritative.
|
||||
- Deterministic capture controls can select spatial domain, orbit distance,
|
||||
exact residual age, or a cross-face eviction midpoint. Capture priming is
|
||||
presentation evidence only: production arrival/departure ownership remains
|
||||
the time-based resident-page lifecycle.
|
||||
- Planet patch debug modes 22-28 expose selected face/level/blend, page
|
||||
bounds and unresolved error, refined normal, ridge/gully evidence, and
|
||||
cross-edge discontinuity/missing residency. Mode 27 evaluates direct
|
||||
filtered erosion against the summed page field at the selected spacing;
|
||||
mode 28 exposes content fingerprint, spacing ratio, and deepest level.
|
||||
The unresolved bound and measured direct error remain distinct signals.
|
||||
- The bounded radial correction step is owned by GameCult.Geometry in matching C# and
|
||||
HLSL bodies. Zyphos composes four steps with its terrain-offset callback;
|
||||
the renderer does not carry a private correction equation.
|
||||
- Planetary page residency is owned by GameCult.Geometry and separates content from
|
||||
presentation state. ContentVersion changes only when the resident tile set
|
||||
changes; PresentationVersion carries arrival/departure weights without
|
||||
regenerating erosion. Six root pages remain resident. GameCult.Geometry's
|
||||
projected-error selector uses unresolved-height bounds against camera
|
||||
footprint to request one ancestor chain, and its residual lifecycle keeps
|
||||
departing chains addressable until their weights reach zero.
|
||||
- Root pages contain the resolvable low-frequency erosion field. Every child
|
||||
page stores `child filtered field - parent filtered field`; the planet
|
||||
shader sums the containing ancestor contributions and weights only those
|
||||
residuals during transitions. Independent full-spectrum pages are never
|
||||
blended. D3D12 flattens resident payloads into one output atlas with indexed
|
||||
metadata and one GPU summary per page at t81-t84.
|
||||
- Planetary page differentials are world-distance tangent gradients expressed
|
||||
as a 3D planet-space vector, not face-local UV derivatives. Zyphos composes
|
||||
the page gradient with finite differentials of its authored/base residual,
|
||||
including the land-mask product term, and derives the radial graph normal
|
||||
as `normalize(direction - tangentGradient)`. The generic SDF proxy exposes
|
||||
a client normal hook; other SDFs retain its finite-distance fallback.
|
||||
Ridge and gully page evidence feeds the same planet material evaluation.
|
||||
`PlanetarySurfaceDifferential.SurfaceNormal` is the matching CPU contract.
|
||||
- The shared reservoir is a fractal domain reservoir, not a texel cache.
|
||||
Pixel rows are the current D3D12 execution projection of selected domain
|
||||
evidence. Area-domain contributors must carry selected subpixel or local
|
||||
|
|
@ -84,6 +181,12 @@ invariants:
|
|||
direct SDF proxies, meshes, volumes, fractal probes, and Mimir sensor
|
||||
observations must publish bounded Form/Appearance/Transport field claims
|
||||
before they can participate in reusable field evidence.
|
||||
- Eve is the portable retained-dashboard authority. The C# Aquarium/CultUI
|
||||
contract is the reference API shape for those documents; Fensalir Direct2D
|
||||
and D3D12 hit testing lower that document locally, but must not become the
|
||||
owner of dashboard semantics or client policy. `AquariumUiBinding` names
|
||||
the Verse-backed CultCache/CultMesh field behind a UI element; local
|
||||
callbacks remain lowering actuators, not a second state authority.
|
||||
- Final composition reads the resolved HDR field texture produced by the
|
||||
shared reservoir history update pass. TubeField render lowerings inject
|
||||
visibility proposals into the shared field reservoir; they must not regain
|
||||
|
|
@ -112,11 +215,29 @@ invariants:
|
|||
owns an `IAquariumAudioStemBus` service for published processed stems,
|
||||
optional monitor playback is explicit, and OBS-facing publication remains
|
||||
a later host cut.
|
||||
- System audio capture is an engine-owned audio-host request, not a client
|
||||
sidecar. Clients enqueue a profile-keyed system-loopback capture request on
|
||||
`AquariumAudioDocument`; Fensalir owns WASAPI loopback capture, publishes a
|
||||
mono `system_mix` stem through `IAquariumAudioStemBus`, and clients consume
|
||||
it through the same stem service as streaming DSP outputs.
|
||||
- The named D3D12 program-output texture is an external publication surface.
|
||||
After copying the completed backbuffer into it, the renderer must leave the
|
||||
resource in `Common` so OBS/EVE/native consumers can transition it for
|
||||
GPU-side reads without inheriting Fensalir's internal copy-destination
|
||||
state.
|
||||
- Fensalir's DirectWrite/Direct2D overlay is an Eve client surface for shared
|
||||
CultMesh UI documents. The adapter should lower those documents into
|
||||
`AquariumUiDocument` and route commands back through Eve/CultNet; provider
|
||||
repos still own dashboard state, accepted commands, and meaning. CultUI
|
||||
bindings carry document schema/id, path, value kind, access, authority, and
|
||||
optional command id so provider state can move into CultCache without
|
||||
changing the native renderer contract.
|
||||
preview elements may expose reusable item-body/corner-handle gestures in
|
||||
normalized preview coordinates, but the client remains the owner of what
|
||||
those gestures do. The preview primitive also carries hover/active state,
|
||||
empty-canvas hits, drag-end events, and client-provided normalized guides
|
||||
so compositor surfaces can show snap/selection feedback without becoming
|
||||
renderer-owned policy.
|
||||
- Planetary ocean/atmosphere flow is an engine field organ when reduced to
|
||||
reusable stochastic advection machinery. Fensalir owns cube-sphere flow
|
||||
addressing, dataset provenance, model contracts, stochastic advection
|
||||
|
|
@ -174,6 +295,30 @@ next_actions:
|
|||
`reservoir-sequence-20260530-003746-*` proves it. The next cut is turning
|
||||
budget-pressure evidence into a renderer-owned per-tile adaptive update
|
||||
contract plus future exact producer SDF replay.
|
||||
- Window resize no longer crashes on the D3D12 swapchain path: resize now
|
||||
disposes DirectWrite overlays and wrapped backbuffers, clears/flushes the
|
||||
D3D11-on-12 context before `ResizeBuffers`, and drops program-output shared
|
||||
textures before recreating size-dependent surfaces. Visible resize probe
|
||||
`20260530-215113-639e6612` survived seven programmatic resizes.
|
||||
- Reservoir reconstruction now has a loss-curve reporter:
|
||||
`scripts/measure-reservoir-loss-curve.ps1` computes MAE/MSE/RMSE/PSNR
|
||||
against same-time higher-work-grid native references. Smoke
|
||||
`reservoir-sequence-20260530-221043-*` at 320x180 gave whole-frame PSNR
|
||||
21.668, 20.981, 20.911, and 21.690 dB for ready frames 1, 2, 4, and 8.
|
||||
D3D12 now owns fixed per-frame GPU timestamp queries for frame record,
|
||||
scene candidate, reservoir resolve/update, bloom, presentation resolve, and
|
||||
active compute lowerings, so quality can be plotted against pass cost
|
||||
instead of process capture wall time. The presentation resolve pass also
|
||||
owns the first full-resolution reconstruction filter: it samples a bounded
|
||||
resolved-reservoir neighborhood, validates neighbors by field id, travel,
|
||||
normal, coverage, detail, color, guide confidence, domain validity, and
|
||||
maturity, and writes only to the final present buffer. Latest smoke
|
||||
`reservoir-sequence-20260530-232852-*` improved f2/f4/f8 and lowered
|
||||
average RMSE from 21.944 to 21.221 against the 20260530-221043
|
||||
pre-reconstruction curve, while f1 still regressed. Next cut: merge loss
|
||||
and GPU timing into one benchmark artifact and tune first-frame
|
||||
reconstruction maturity without adding native-resolution reservoir
|
||||
intermediates.
|
||||
- Build the planetary flow organ in staged cuts: provenance manifest, static
|
||||
relief and reanalysis cube-sphere bake pipeline, stochastic advection
|
||||
contracts, synthetic runtime slice, ERA5/OSCAR baseline learning, then
|
||||
|
|
@ -230,10 +375,24 @@ next_actions:
|
|||
until a real planar copy path exists; GPU producers can still write NV12
|
||||
leases directly. This makes imported handles an edge case, not the primary
|
||||
live camera authority. Missing or unresolved TubeField ramps bind a Fensalir-owned linear fallback ramp.
|
||||
Camera texture claims can therefore resolve resources, but their first
|
||||
visual use stays debug-view-only for now; production visual-fusion and
|
||||
feature-extraction lowerings are deferred until the renderer and audio work
|
||||
are fixed. TubeField compute now
|
||||
Camera texture claims can therefore resolve resources. Camera-sensor
|
||||
Feature claims with `SensorObservation` proposal policy now plan to the
|
||||
`GpuSensorFusion` backend instead of deferring. `D3D12Renderer` derives
|
||||
sensor-camera packets and SRV table entries from planned FieldEvidence
|
||||
Texture2D resources, then dispatches `D3D12GpuSensorFusionCS` into the
|
||||
temporal-gaussian buffer. The 2026-05-31 Mimir all-sensor proof planned all
|
||||
12 packets with zero deferred requests; the first D3D12 host dispatched
|
||||
feature extraction for LeapStereoIr plus both Bayer8 PS3 Eyes at 16,384
|
||||
sparse samples per texture, generating 49,152 gaussians with sensor-fusion
|
||||
timing around 0.059 ms/frame. The follow-up YUY2 decode cut binds
|
||||
`DXGI_FORMAT_YUY2` resources through the Microsoft-documented
|
||||
`R8G8B8A8_UNORM` SRV view, decodes `Y0/U/Y1/V` in
|
||||
`D3D12GpuSensorFusion.hlsl`, and the Mimir all-sensor proof dispatched all
|
||||
five camera textures with zero unsupported field textures, generating
|
||||
81,920 sparse gaussians at about 0.08 ms/frame sensor-fusion GPU time.
|
||||
Packed video is not allowed to masquerade as ordinary RGBA; declared format
|
||||
metadata selects the decode path.
|
||||
TubeField compute now
|
||||
emits one GPU-resident D3D12 DrawIndexed indirect argument packet per
|
||||
material/resource batch, and render binds the batch source/ramp state then
|
||||
executes the packet with ExecuteIndirect. The first material is opaque field
|
||||
|
|
@ -250,20 +409,34 @@ next_actions:
|
|||
compute-writable UAV-backed pages. Empty SurfacePage version is content
|
||||
freshness, not texture identity, so compute outputs can update without
|
||||
reallocating when shape/format/access are unchanged. FieldEvidence now has
|
||||
an explicit `AquariumFieldStereoDepthLowering` sidecar for future calibrated
|
||||
an explicit `AquariumFieldStereoDepthLowering` sidecar for calibrated
|
||||
stereo compute dispatch: validation requires a Height claim whose payload is
|
||||
the compute-writable disparity SurfacePage, shader-readable left/right
|
||||
Texture2D or RollingTexture resources, profile/calibration/camera-pair
|
||||
identity, and coherent output shape/range. This keeps Mimir's libSGM-shaped
|
||||
D3D12 depth path inside typed field/resource authority instead of a private
|
||||
texture or CUDA/TensorRT dependency. The D3D12 renderer now evaluates
|
||||
stereo-depth dispatch readiness without writing fake depth: a lowering is
|
||||
ready only when its Height claim is planned and left/right input textures
|
||||
plus UAV disparity SurfacePage resolve through the field resource registry;
|
||||
capacity logs still state that the kernel is not installed. The lowering
|
||||
stereo-depth dispatch readiness and runs the first packed-Leap compute
|
||||
kernel: a lowering is eligible only when its Height claim is planned and
|
||||
left/right input textures plus UAV disparity SurfacePage resolve through
|
||||
the field resource registry. The first `D3D12StereoDepth.hlsl` lane handles
|
||||
the immediate Mimir Leap shape where left and right observations are packed
|
||||
into the R/G channels of one Texture2D, then writes crude R16F disparity by
|
||||
local SAD/block matching. This is a live depth proof, not final
|
||||
libSGM-style SGM or calibrated metric depth. The lowering
|
||||
sidecar now carries the first SGM-shaped kernel constants too: min
|
||||
disparity, disparity levels, aggregation paths, census radius, and P1/P2
|
||||
smoothness penalties. VolumeTexture
|
||||
smoothness penalties. The first Leap point-cloud render lane now follows
|
||||
that disparity socket: Mesh packages with
|
||||
`SourceUri=derived-from:<surface-page-resource>` allocate UAV-capable
|
||||
vertex/index buffers, `D3D12PointCloudFromDisparityCS` fills a standard
|
||||
`PositionNormalUvColor` PointList from the disparity SurfacePage, and
|
||||
`D3D12PointCloudPS` renders it into the scene candidate MRTs through the
|
||||
normal depth/camera path. This is intentionally sourced from typed Mesh and
|
||||
SurfacePage resources; CPU point arrays and unproven RGB depth are not live
|
||||
authorities. Headless pipeline smoke captured
|
||||
`artifacts/runtime/fensalir-point-cloud-pipeline-smoke.png`.
|
||||
VolumeTexture
|
||||
resources now have the same
|
||||
first resolver shape: contracts and DSL can declare GPU-resident 3D
|
||||
textures with dimensions, depth, format, and version, and the D3D12 registry
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
## Current Slice
|
||||
|
||||
Leap/Mimir point-cloud bridge:
|
||||
Mimir now declares a Leap disparity-derived `FieldMesh` PointList. Fensalir now
|
||||
preserves mesh `SourceUri`, allocates `derived-from:*` mesh buffers as UAV-capable
|
||||
GPU resources, fills those vertices/indices from the disparity SurfacePage in
|
||||
`D3D12PointCloudFromDisparityCS`, and renders the standard
|
||||
`PositionNormalUvColor` point list through `D3D12PointCloudPS` into the scene
|
||||
candidate targets. This is the first visible point-cloud lane; full SGM,
|
||||
metric calibration, splat sizing, and the global residual/calibration owner are
|
||||
still separate cuts.
|
||||
|
||||
Turn the native-domain reservoir from a native-resolution cache into a
|
||||
budgeted spatiotemporal sampler. The work grid now defaults to half present
|
||||
resolution and is clamped below native presentation scale; scene evidence,
|
||||
|
|
@ -87,6 +97,59 @@ for the current frame. Smoke
|
|||
mode at scale 0.5 / spatial budget 0.5. A bitmap probe counted 32078 green,
|
||||
6818 blue, and 3968 other/filtering pixels in the 304x141 present output.
|
||||
|
||||
Window resize fix:
|
||||
The Fensalir window resize path now explicitly clears and flushes the D3D11-on-12
|
||||
overlay context after disposing DirectWrite overlays and wrapped backbuffer
|
||||
resources, before `IDXGISwapChain::ResizeBuffers`. Resize also disposes
|
||||
program-output shared textures before recreating size-dependent output surfaces.
|
||||
Probe `scripts/dev-reload.ps1` visible slot `20260530-215113-639e6612` survived
|
||||
seven programmatic resizes and logged rebuilt D3D12 targets at each size with no
|
||||
stderr.
|
||||
|
||||
Reservoir loss curve:
|
||||
`scripts/measure-reservoir-loss-curve.ps1` now reports RGB reconstruction loss
|
||||
against same-time higher-work-grid native references. Metrics include MAE, MSE,
|
||||
RMSE, PSNR, changed pixels, max channel delta, and optional masked loss when
|
||||
disocclusion/rejection masks are present. Smoke
|
||||
`artifacts/fensalir-captures/reservoir-sequence-20260530-221043-*` compared
|
||||
native scale 0.5 / spatial budget 0.5 against reference scale 0.75 / spatial
|
||||
budget 1.0 at ready frames 1, 2, 4, and 8. Whole-frame PSNR was 21.668, 20.981,
|
||||
20.911, and 21.690 dB respectively; RMSE was 21.043, 22.777, 22.962, and 20.992.
|
||||
This is a higher-budget online reference, not offline ground truth.
|
||||
|
||||
GPU pass timing:
|
||||
The D3D12 renderer now owns fixed pass-level GPU timestamp queries in per-frame
|
||||
resources and reports once-per-second averages beside CPU capacity logs. The
|
||||
timed passes are frame command recording, GPU sensor fusion, stereo depth,
|
||||
fractal reservoir update, tube-field update, height field, scene candidate,
|
||||
field-reservoir resolve, reservoir-history update, bloom, and presentation
|
||||
resolve. A 320x180 headless timing smoke after the reconstruction pass logged
|
||||
record 1.338 ms, scene-candidate 0.364 ms, reservoir-resolve 0.004 ms,
|
||||
reservoir-update 0.853 ms, bloom 0.029 ms, and presentation 0.077 ms over 61
|
||||
frames in `artifacts/dev-reload/fensalir-capture.out.log`.
|
||||
|
||||
Reservoir presentation reconstruction:
|
||||
`D3D12ReservoirPresentationResolvePS` now performs the first final-pass
|
||||
spatiotemporal reservoir reconstruction filter. The reservoir/history grid
|
||||
remains below native resolution; the final present pass is the only
|
||||
full-resolution surface. Reconstruction samples a bounded 3x3 neighborhood from
|
||||
the resolved reservoir and weights neighbors by field id, travel/depth, normal,
|
||||
coverage, detail, color, guide confidence, domain validity, and neighbor
|
||||
agreement. The filter is maturity-gated so the first visible frame cannot blur
|
||||
from unearned history. Debug mode 21 visualizes reconstruction mix, confidence,
|
||||
and domain validity.
|
||||
|
||||
Latest measured curve:
|
||||
`artifacts/fensalir-captures/reservoir-sequence-20260530-232852-*` compared
|
||||
native scale 0.5 / spatial budget 0.5 against reference scale 0.75 / spatial
|
||||
budget 1.0 at 320x180, ready frames 1, 2, 4, and 8. Whole-frame RMSE/PSNR:
|
||||
f1 21.788 / 21.366 dB, f2 21.068 / 21.658 dB, f4 21.473 / 21.493 dB, f8 20.556
|
||||
/ 21.872 dB. Against the pre-reconstruction curve
|
||||
`reservoir-sequence-20260530-221043-*`, f2/f4/f8 improved and f1 regressed;
|
||||
average RMSE dropped from 21.944 to 21.221. This is competent enough to keep,
|
||||
but the next quality cut should target the first-frame regression and plot the
|
||||
curve against GPU pass timings in one artifact.
|
||||
|
||||
## Hot Lesson
|
||||
|
||||
Area ReSTIR confirms that reuse over an area domain is invalid unless the
|
||||
|
|
@ -113,15 +176,23 @@ SDF history reconstructs the previous selected world hit from stored UV/travel,
|
|||
carries it through object center motion, and validates current ray/travel/support
|
||||
before temporal reuse can merge it. TubeField batches beyond the manifest source
|
||||
cap remain intentionally non-authoritative.
|
||||
Swapchain resize correctness depends on releasing both D3D12 backbuffer owners
|
||||
and D3D11-on-12 overlay references before `ResizeBuffers`. Disposing wrapped
|
||||
resources is not enough; the D3D11 immediate context must be cleared and flushed
|
||||
so it stops holding hidden references.
|
||||
Reservoir quality must be golfed against loss-over-time, not single-frame smoke
|
||||
deltas. The current curve is noisy and not monotonically improving, which means
|
||||
the candidate/reference comparison is exposing temporal instability rather than
|
||||
a settled convergence story.
|
||||
|
||||
## Next Bounded Move
|
||||
|
||||
Follow `docs/perfect-fensalir-machine-roadmap.md` Phase 1:
|
||||
|
||||
1. Convert the fixed spatial-reuse budget into a pressure-guided adaptive
|
||||
update contract only after the renderer owns a per-tile pressure input path.
|
||||
Debug mode 20 is the visible layer to validate the active budget phase. Do
|
||||
not add native-resolution reservoir intermediates.
|
||||
1. Put loss and GPU timing into one benchmark artifact, then tune the
|
||||
presentation reconstruction first-frame maturity gate so f1 no longer
|
||||
regresses while f2/f4/f8 keep their improvement. Do not add native-resolution
|
||||
reservoir intermediates.
|
||||
2. Add exact SDF producer replay only when client SDF distance functions have a
|
||||
shared replay include or manifest instead of duplicating shader policy in
|
||||
post.
|
||||
|
|
@ -129,6 +200,9 @@ Follow `docs/perfect-fensalir-machine-roadmap.md` Phase 1:
|
|||
can tell whether extra samples buy visible stability.
|
||||
4. Replace the bounded TubeField replay source descriptor table with a replay
|
||||
atlas only if real scenes hit the cap.
|
||||
5. For Mimir geometry, verify live Leap point clouds in the editor, then add
|
||||
the residual/calibration owner that consumes Leap point error before other
|
||||
cameras contribute motion/RGB evidence.
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<ProjectReference Include="..\..\src\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\src\Aquarium.Fensalir\Aquarium.Fensalir.csproj" />
|
||||
<ProjectReference Include="..\..\src\Aquarium.Zyphos\Aquarium.Zyphos.csproj" />
|
||||
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Tests;
|
||||
|
||||
public sealed class CubeTileKeyTests
|
||||
public sealed class PlanetaryTileAddressTests
|
||||
{
|
||||
[Fact]
|
||||
public void PositionAtMapsTileLocalCoordinatesIntoFaceCoordinates()
|
||||
{
|
||||
var key = new CubeTileKey(CubeFace.PositiveZ, 2, 1, 2);
|
||||
var key = new PlanetaryTileAddress(PlanetaryCubeFace.PositiveZ, 2, 1, 2);
|
||||
|
||||
var lowerLeft = key.PositionAt(0.0, 0.0);
|
||||
var upperRight = key.PositionAt(1.0, 1.0);
|
||||
|
|
@ -21,11 +22,11 @@ public sealed class CubeTileKeyTests
|
|||
[Fact]
|
||||
public void ChildAndParentPreserveQuadtreeAddress()
|
||||
{
|
||||
var root = new CubeTileKey(CubeFace.NegativeY, 0, 0, 0);
|
||||
var root = new PlanetaryTileAddress(PlanetaryCubeFace.NegativeY, 0, 0, 0);
|
||||
var child = root.Child(1, 0).Child(0, 1);
|
||||
|
||||
Assert.Equal(new CubeTileKey(CubeFace.NegativeY, 2, 2, 1), child);
|
||||
Assert.Equal(new CubeTileKey(CubeFace.NegativeY, 1, 1, 0), child.Parent());
|
||||
Assert.Equal(new PlanetaryTileAddress(PlanetaryCubeFace.NegativeY, 2, 2, 1), child);
|
||||
Assert.Equal(new PlanetaryTileAddress(PlanetaryCubeFace.NegativeY, 1, 1, 0), child.Parent());
|
||||
Assert.Equal("NegativeY/L02/2/1", child.ToString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Tests;
|
||||
|
||||
|
|
@ -7,7 +9,7 @@ public sealed class FractalBoundaryTests
|
|||
[Fact]
|
||||
public void FractalAssemblyDoesNotReferenceRendererOrD3D12Assemblies()
|
||||
{
|
||||
var references = typeof(CubeTileKey)
|
||||
var references = typeof(PlanetaryTileAddress)
|
||||
.Assembly
|
||||
.GetReferencedAssemblies()
|
||||
.Select(reference => reference.Name ?? string.Empty);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using Aquarium.Engine.Fractal.Grammar;
|
||||
using Aquarium.Engine.Fractal.Lod;
|
||||
using CultMath;
|
||||
using GameCult.Geometry;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Tests;
|
||||
|
||||
|
|
@ -30,7 +32,7 @@ public sealed class FractalGpuProgramCompilerTests
|
|||
Assert.InRange(transform.MaterialSeedShape.X, 0.0f, 1.0f);
|
||||
});
|
||||
Assert.Contains(transforms, transform =>
|
||||
transform.TileAddress.X == (float)CubeFace.PositiveZ
|
||||
transform.TileAddress.X == (float)PlanetaryCubeFace.PositiveZ
|
||||
&& transform.TileAddress.Y == 2.0f
|
||||
&& transform.TileAddress.Z == 1.0f
|
||||
&& transform.TileAddress.W == 1.0f);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue