Create Fensalir engine repo boundary

This commit is contained in:
Meta Vens 2026-05-23 18:54:26 +01:00
commit a7ea2d7577
45 changed files with 110 additions and 6102 deletions

View file

@ -1,52 +1,34 @@
# Aquarium Engine Instructions
# Fensalir Instructions
## Purpose
Aquarium Engine is the native C# runtime for the Epiphany Aquarium client. It
owns the window, renderer, input loop, live-reload boundary, persistent client
state, and eventual diegetic UI machinery.
Fensalir is the native C# runtime and renderer dependency for Aquarium-style
clients. It owns the window, D3D12 renderer, input loop, live-reload boundary,
audio host, debug chrome, render contracts, and reusable field/fractal machinery.
This repo is not the Epiphany harness. The visible frame belongs to Aquarium.
Client-specific meaning belongs to client repos. Epiphany Aquarium lives at
`E:\Projects\Aquarium-Engine`.
## Operating Doctrine
- The renderer is the source of truth for visible world pixels.
- Keep the frame legible: explicit passes, explicit ownership, explicit state.
- Do not leave lies in the walls. When building engine systems meant to last,
refuse half-assed shortcuts, hidden compensators, mystery state, and
prototype-shaped production code. Prefer full-ass, research-backed,
steel-plated resilience with padded failure boundaries so users do not cut
themselves on sharp internals.
- This is game engine work: every millisecond counts. Do not spend runtime
casually on avoidable JSON churn, spaghetti state, redundant ray work,
unbounded full-resolution filters, or convenience abstractions that hide
performance costs. If a shortcut is temporary, mark it as scaffolding and cut
it before it becomes architecture.
refuse hidden compensators, mystery state, and prototype-shaped production
code.
- Every millisecond counts. Avoid JSON churn, redundant ray work, unbounded
full-resolution filters, and abstractions that hide performance costs.
- Before inventing renderer, simulation, storage, or post-processing machinery,
check standard literature, production talks, engine docs, or proven local
patterns. Build the durable machine, not a sequence of shy approximations that
accrete into Jenga.
- When a visual, physical, or simulation spec describes a continuous form, start
with continuous field math or a coherent parameterization that owns the form.
Do not assemble the thing from piecewise capsules, boxes, slabs, adapters, or
helper organs just because that is the shortest path to pixels. Piecewise
construction is allowed only when the form itself is piecewise or when a
measured constraint makes the continuous model impractical; document that cut
line and keep the compensator small.
- The camera target is the Grid center. Grid radius follows zoom distance.
Pitch, yaw, and screen projection do not resize the Grid.
- Body anchors are world-space simulation state. They do not ride with the Grid
visibility window.
- Lighting is diegetic. Self, field sources, and Aquarium-owned transport light
the scene; ambient convenience light is guilty until proven useful.
patterns.
- Continuous visual/physical forms should start from continuous field math or a
coherent parameterization. Piecewise construction needs a measured reason.
- Lighting is diegetic. Ambient convenience light is guilty until proven useful.
- Overlay UI and diegetic UI are different beasts. Overlay text can use
DirectWrite. Scene text should become renderer-owned MSDF/SDF billboards when
it belongs in the world.
- CultCache is the runtime memory organ. Camera state, reload state, renderer
knobs, UI state, body state, and future Epiphany connection state should be
typed Cult documents, not loose JSON or process-only folklore.
- CultNet is the communication surface for Epiphany agents. Add transport
deliberately and keep authority boundaries typed.
DirectWrite; world text needs renderer-owned billboards when it belongs in the
scene.
- Fensalir owns reusable contracts and mechanisms. Client repos own policy,
narrative semantics, role names, and app-specific state.
## Persistent State
@ -56,31 +38,23 @@ This repo is not the Epiphany harness. The visible frame belongs to Aquarium.
behavior.
- `state/scratch.md` is disposable working context for the active slice.
Update these when the engine learns something durable. Delete stale guidance
instead of embalming it.
Update state when engine ownership or durable renderer doctrine changes.
## Verification
Use focused checks:
```powershell
dotnet build Aquarium.Engine.sln
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4
dotnet build Fensalir.sln
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4 -ClientProject src\Aquarium.Fensalir\Aquarium.Fensalir.csproj
```
For normal iteration:
```powershell
.\scripts\dev-watch.ps1
```
For shipping:
```powershell
.\scripts\publish-win-x64.ps1 -Clean -Zip
.\scripts\dev-watch.ps1 -ClientProject src\Aquarium.Fensalir\Aquarium.Fensalir.csproj
```
## Style
Prefer small, intentional commits. Push completed work. Keep docs about the live
system; put old branch scars only in evidence when they alter future decisions.
Prefer small, intentional commits. Push completed work once the real remote
exists. Keep docs about the live system; put old branch scars only in evidence
when they alter future decisions.

View file

@ -11,28 +11,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine", "src\Aqua
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "..\CultMath\src\CultMath\CultMath.csproj", "{0089E341-25F2-43FC-8FD9-CBD1AE646D91}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany", "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj", "{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack", "..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj", "{A8EDAE19-805B-4B09-9566-BE63937CA1B9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching", "..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj", "{6E874823-17F8-453C-A077-4A101D425C84}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Logging", "..\CultLib\src\GameCult.Logging\GameCult.Logging.csproj", "{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack.Analyzers", "..\CultLib\src\GameCult.Caching.MessagePack.Analyzers\GameCult.Caching.MessagePack.Analyzers.csproj", "{225265F1-8278-48D7-B271-BE8841D0C1CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack.Generator", "..\CultLib\src\GameCult.Caching.MessagePack.Generator\GameCult.Caching.MessagePack.Generator.csproj", "{435C602F-025E-4948-8285-E83E693DAA7B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking", "..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj", "{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Sample.Minimal", "src\Aquarium.Sample.Minimal\Aquarium.Sample.Minimal.csproj", "{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Tests", "tests\Aquarium.Engine.Tests\Aquarium.Engine.Tests.csproj", "{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany.AgentPreview", "src\Aquarium.Epiphany.AgentPreview\Aquarium.Epiphany.AgentPreview.csproj", "{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Zyphos", "src\Aquarium.Zyphos\Aquarium.Zyphos.csproj", "{801B1913-6AE1-4B8C-8030-4669AC4FE25E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Fractal", "src\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj", "{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}"
@ -95,90 +79,6 @@ Global
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x64.Build.0 = Release|Any CPU
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x86.ActiveCfg = Release|Any CPU
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x86.Build.0 = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.ActiveCfg = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.Build.0 = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.ActiveCfg = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.Build.0 = Debug|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.Build.0 = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.ActiveCfg = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.Build.0 = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.ActiveCfg = Release|Any CPU
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.Build.0 = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x64.ActiveCfg = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x64.Build.0 = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x86.ActiveCfg = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x86.Build.0 = Debug|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|Any CPU.Build.0 = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x64.ActiveCfg = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x64.Build.0 = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x86.ActiveCfg = Release|Any CPU
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x86.Build.0 = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x64.ActiveCfg = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x64.Build.0 = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x86.ActiveCfg = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x86.Build.0 = Debug|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|Any CPU.Build.0 = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x64.ActiveCfg = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x64.Build.0 = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x86.ActiveCfg = Release|Any CPU
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x86.Build.0 = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x64.ActiveCfg = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x64.Build.0 = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x86.ActiveCfg = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x86.Build.0 = Debug|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|Any CPU.Build.0 = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x64.ActiveCfg = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x64.Build.0 = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x86.ActiveCfg = Release|Any CPU
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x86.Build.0 = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x64.ActiveCfg = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x64.Build.0 = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x86.ActiveCfg = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x86.Build.0 = Debug|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|Any CPU.Build.0 = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x64.ActiveCfg = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x64.Build.0 = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x86.ActiveCfg = Release|Any CPU
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x86.Build.0 = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x64.ActiveCfg = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x64.Build.0 = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x86.ActiveCfg = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x86.Build.0 = Debug|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|Any CPU.Build.0 = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x64.ActiveCfg = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x64.Build.0 = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x86.ActiveCfg = Release|Any CPU
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x86.Build.0 = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x64.ActiveCfg = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x64.Build.0 = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x86.ActiveCfg = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x86.Build.0 = Debug|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|Any CPU.Build.0 = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x64.ActiveCfg = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x64.Build.0 = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x86.ActiveCfg = Release|Any CPU
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x86.Build.0 = Release|Any CPU
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -203,18 +103,6 @@ Global
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x64.Build.0 = Release|Any CPU
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x86.ActiveCfg = Release|Any CPU
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x86.Build.0 = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.Build.0 = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.Build.0 = Debug|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.Build.0 = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.ActiveCfg = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.Build.0 = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.ActiveCfg = Release|Any CPU
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.Build.0 = Release|Any CPU
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -306,12 +194,9 @@ Global
GlobalSection(NestedProjects) = preSolution
{08D15A38-B6AB-459B-8BF9-3CCA8555A022} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{801B1913-6AE1-4B8C-8030-4669AC4FE25E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F9274D99-3CC6-42B6-9817-EC7F19663B1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A9D41C48-7E78-4D75-A814-0C1500245EFC} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}

255
README.md
View file

@ -4,245 +4,48 @@
<img src="Fensalir-Icon.png" alt="Fensalir icon" width="160" />
</p>
Fensalir is the C# native rendering spine for Aquarium clients: windowing,
D3D12 rendering, input, reload, persistent state, debug UI, and packageable
engine machinery.
Fensalir is the native C# runtime for Aquarium-style clients: Win32 hosting,
D3D12 rendering, input, hot reload, audio output, debug UI, render contracts,
fractal/field machinery, and engine demo clients.
Epiphany is the first serious client in this repo. It is not the engine's
identity.
## Intent
- Native C# host with a legible renderer spine and explicit ownership.
- Vortice/D3D12 backend with graphics API details contained inside the engine.
- `Aquarium.Engine.Contracts` as the client-facing boundary.
- `Aquarium.Epiphany` as the current Epiphany Aquarium client.
- `Aquarium.Sample.Minimal` as a small non-Epiphany boundary proof.
- DirectWrite/Direct2D overlay text after the scene pass for crisp debug and UI
typography.
- Diegetic scene/UI surfaces driven by client objects instead of admin chrome.
- CultCache for typed settings, persistent client state, and reload recovery.
- CultNet-ready runtime identity for clients that need Epiphany communication.
- Procedural audio and visual state treated as coupled signals.
## Current Shape
The engine opens a Win32 window, owns a D3D12 swapchain, compiles and hot-reloads
HLSL, allocates render targets, preserves last-good shader/runtime state across
failed reloads, hosts the debug overlay, and presents scene-linear HDR through
the current post stack.
Client code is loaded behind `Aquarium.Engine.Contracts`, so a client can reload
while the engine host, window, renderer, and D3D device stay alive. Runtime state
is banked through CultCache MessagePack documents instead of loose JSON.
`Aquarium.Epiphany` currently supplies the Grid-centered camera policy, Epiphany
agent SDF shaders, body placement, CultCache documents, and future CultNet
interpretation. Those are client semantics. The engine consumes them as generic
rendering and runtime data.
Epiphany Aquarium is no longer inside this repo. It lives next door at
`E:\Projects\Aquarium-Engine` and consumes Fensalir as a dependency.
## Repository Shape
This repository intentionally contains both the reusable Aquarium engine and the
Epiphany client while the client API is still being carved into its final shape:
- `src/Aquarium.Engine.Contracts`: Vortice-free client API and shared data
contracts.
- `src/Aquarium.Engine.Contracts`: client-facing runtime, render, UI, audio,
input, and frame contracts.
- `src/Aquarium.Engine`: Win32 host, D3D12 backend, reload transport, debug UI,
audio host, and engine-owned assets.
- `src/Aquarium.Epiphany`: Epiphany Aquarium runtime, state interpretation,
render-plan configuration, and agent shaders.
- `src/Aquarium.Zyphos`: planetary-scale rendering demo client with its own
runtime, controls, scene state, and SDF planet shaders.
- `src/Aquarium.Sample.Minimal`: tiny non-Epiphany client that proves the host
can run something other than Epiphany.
audio host, bundled engine assets, and renderer-owned shaders.
- `src/Aquarium.Engine.Fractal`: reusable fractal, field, reservoir, and DSL
machinery.
- `src/Aquarium.Fensalir`: Fensalir splash/art reconstruction demo client.
- `src/Aquarium.Sample.Minimal`: minimal client boundary proof.
- `src/Aquarium.Zyphos`: planetary-scale engine demo client.
- `src/Aquarium.LocalCast`: LocalCast/Mimir-facing runtime bridge experiments.
- `tests`: engine, fractal, LocalCast, and demo coverage.
- `tools`: engine/fractal receipt tools.
Splitting Aquarium and Epiphany into separate repos is plausible, but not owed
yet. The cut becomes clean when the contracts are stable enough that Epiphany can
move out without taking renderer policy, shader include plumbing, or reload
folklore with it. Until then, the repo split would mostly create paperwork and a
new place for confusion to hide.
The assemblies still use `Aquarium.*` names for API continuity. Rename the
public namespace deliberately later if the package boundary earns that churn.
## Run
Aquarium currently expects the sibling `CultMath` and `CultLib` repos at
`E:\Projects\CultMath` and `E:\Projects\CultLib`, or the equivalent
`..\..\..\CultMath` / `..\..\..\CultLib` paths from the engine project.
Aquarium consumes `AquaSynth.Faust` from the repo-local `packages` NuGet feed,
not as a live project reference to `AquaSynth`. That package pulls
`AquaSynth.Core` transitively for the built-in patch editor. This keeps Aquarium
builds stable while synth work is in flight. To intentionally update that
boundary, pack a new synth version and bump the `Aquarium.Engine` package
reference:
## Build
```powershell
.\scripts\update-synth-package.ps1
dotnet build Fensalir.sln
```
## Run A Client
```powershell
dotnet build Aquarium.Engine.sln
dotnet run --project src\Aquarium.Engine\Aquarium.Engine.csproj -- --client-assembly src\Aquarium.Epiphany\bin\Debug\net10.0\Aquarium.Epiphany.dll
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.Fensalir\Aquarium.Fensalir.csproj
```
Open or build `Aquarium.Engine.sln`. It is the single solution inventory for the
repo.
Other demo clients can be launched by changing `-ClientProject`.
`src\Aquarium.Sample.Minimal` is a tiny non-Epiphany client used to keep the
engine boundary honest. It references only `Aquarium.Engine.Contracts`, declares
an `AquariumRenderPlan`, and boots through the same host/client loader path.
## Boundary
`src\Aquarium.Zyphos` is the first nontrivial non-Epiphany demo. It renders a
rotating spherical planet, procedural continents, atmospheric rim light,
night-side city glints, and an orbiting moon through the same host, renderer,
reload, UI, bloom, and SDF proxy machinery used by Epiphany.
For iteration while an Aquarium window is already open, use the dev reload
runner instead of `dotnet run`:
```powershell
.\scripts\dev-reload.ps1
```
It builds an apphost executable into a fresh disposable slot under
`artifacts\dev-reload`, records the slot/PID in PowerShell CLIXML, and launches
the runtime with a MessagePack CultCache store at
`artifacts\dev-reload\cultcache\aquarium-client.msgpack`.
The previous script-owned process is stopped only after the replacement build
succeeds. This keeps MSBuild away from the locked normal `bin\Debug` output
while a live window is still open.
Run a different client project through the same slot machinery with
`-ClientProject`:
```powershell
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.Zyphos\Aquarium.Zyphos.csproj
```
If the visible dev window was closed but the last slot is still present, reopen
it without rebuilding:
```powershell
.\scripts\dev-reload.ps1 -Reopen
```
Visible and headless runs keep separate PID state and logs, so a headless smoke
does not replace the visible dev window.
For automatic rebuild/restart on source changes, use the watcher:
```powershell
.\scripts\dev-watch.ps1
```
Watch a specific client project the same way:
```powershell
.\scripts\dev-watch.ps1 -ClientProject src\Aquarium.Zyphos\Aquarium.Zyphos.csproj
```
The watcher polls source files, waits for writes to settle, builds into a new
slot, and only replaces the running Aquarium after the new build succeeds. If a
build fails, the previous good process keeps running and that same broken source
fingerprint is not retried until files change again.
It also watches the recorded script-owned process. If the visible window is
closed or the recorded process dies, the watcher relaunches from a fresh slot on
the next poll instead of sitting there with its hands folded like this is fine.
To reopen a closed visible window from the last good slot without rebuilding,
run the watcher with:
```powershell
.\scripts\dev-watch.ps1 -ReopenWhenClosed
```
Shader edits do not restart the process. The dev watcher fingerprints
`.hlsl`/`.hlsli` files under both the engine and selected client shader roots,
copies them into the running apphost slot, and lets the renderer rebuild the
active D3D12 pipelines from its current shader source root. A bad shader edit
leaves the previous working shaders bound and writes the compiler failure to
stderr.
Epiphany client/runtime code is split into `Aquarium.Epiphany` behind
`Aquarium.Engine.Contracts`. The host loads that client DLL through a collectible
assembly load context, while `dev-watch.ps1` builds client-only changes into
`artifacts\dev-reload\live-slots` and updates `live-current.txt`. The running
host sees the pointer change, loads and starts the new runtime first, then
disposes the old one only after the replacement is valid. A bad client DLL leaves
the previous runtime alive and reports the reload failure to stderr. The watcher
waits for the host log to acknowledge the exact client DLL path before calling a
client reload successful; pointer updates without host acknowledgement are treated
as reload failures. Successful reloads rehydrate through CultCache without
restarting the window or D3D device.
Host, renderer, contract, project, script, and `src\Aquarium.Engine\Assets`
content changes still rebuild and restart the apphost. Runtime content belongs
to the process image; shader hot reload cannot make a running renderer discover
new files it never loaded.
Runtime live state is a typed CultCache document, not a loose sidecar file. The
first document is `epiphany.aquarium.live_state`, currently banking camera
target, yaw, pitch, distance, time, and save generation every few frames so a
reload can rehydrate without pretending memory is vibes.
Stop the script-owned Aquarium process:
```powershell
.\scripts\dev-stop.ps1
```
Headless smoke:
```powershell
dotnet run --project src\Aquarium.Engine\Aquarium.Engine.csproj -- --headless --client-assembly src\Aquarium.Epiphany\bin\Debug\net10.0\Aquarium.Epiphany.dll
```
Headless reload smoke without touching the normal build output:
```powershell
.\scripts\dev-reload.ps1 -Headless
```
Renderer/client boundary check:
```powershell
.\scripts\verify-boundaries.ps1
```
Headless watch smoke:
```powershell
.\scripts\dev-watch.ps1 -Headless
```
Shipping publish:
```powershell
.\scripts\publish-win-x64.ps1 -Clean -Zip
```
This writes a self-contained Windows build to
`artifacts\publish\win-x64`, verifies the executable, Epiphany client DLL,
contracts DLL, icon, bundled fonts, and shader source, then optionally creates
`artifacts\publish\EpiphanyAquariumEngine-win-x64.zip`.
## Persistent State
This repo carries its own Aquarium memory:
- `state/map.yaml`: canonical project map and next actions.
- `state/memory.json`: durable taste, doctrine, renderer rules, and warnings.
- `state/evidence.jsonl`: compact lessons that should change future behavior.
- `state/scratch.md`: disposable working context for the current pass.
Keep that state current when the engine learns something durable. Dead lessons
go out with the trash; the repo is not a scrapbook.
## Docs
- `docs/aquarium-engine-doctrine.md`: engine doctrine and invariants.
- `docs/engine-client-boundary.md`: Engine/Epiphany ownership split.
- `docs/epiphany-extraction-render-api-plan.md`: migration plan for the client
extraction and ergonomic render graph API.
- `docs/vortice-spine.md`: native host/renderer spine.
- `docs/hlsl-renderer.md`: current shader path.
- `docs/input.md`: camera and input contract.
- `docs/cult-runtime-surface.md`: CultCache/CultNet runtime surface.
Fensalir owns reusable host and renderer authority. Client repos own their
semantics. Do not add Epiphany, Mimir, or other client policy nouns here unless
they are demo/client projects inside this repo. Shared capability belongs in
contracts or reusable engine modules; client interpretation belongs outside.

View file

@ -1,954 +0,0 @@
# Epiphany Agent SDF Visual Language
This document defines the first renderer-facing visual grammar for Epiphany's
standing organs in Aquarium.
The goal is not eight cute mascots with arbitrary paint. Each body should expose
what its organ does, how it is feeling, and what Epiphany surface currently owns
its visible state. Identity comes from durable memory. Motion comes from
heartbeat physiology. Posture comes from operator-safe surfaces. Affordance
comes from intents, receipts, and review state.
## Shared Inputs
Aquarium should derive an `AgentVisualState` from the CultNet-discoverable
Epiphany documents instead of hard-coding private app folklore.
Stable identity and temperament:
- `epiphany.agent_memory`: `roleId`, `agent.agent_id`,
`agent.identity.name`, `agent.identity.roles`, `agent.canonical_state`,
goals, memories, and perceived overlays.
Physiology and animation clock:
- `epiphany.agent_heartbeat`: `sceneClock`, `participants`, `history`,
`sleepCycle`, `memoryResonance`, `incubation`, `thoughtLanes`, `bridge`,
`appraisals`, and `reactions`.
Visible operational posture:
- `epiphany.surface.roles`: lane status, note, jobs, authority scopes, and
recommended action.
- `epiphany.surface.role_result`: status, job, finding, patch, risk, evidence
gaps, and reviewable result state.
- `epiphany.surface.coordinator`: Self route, target role, review requirement,
automation eligibility, reason, source signals, and lane summaries.
- `epiphany.surface.face`: Face bubbles, drafts, posts, and latest public
artifacts.
- `epiphany.surface.jobs`: job count, owner role, progress, blocking reason,
and active work.
- `epiphany.surface.pressure`: context pressure and compaction readiness.
- `epiphany.surface.reorient` and `epiphany.surface.reorient_result`:
continuity verdicts, resume posture, regather posture, and reorientation result.
Useful normalized visual fields:
```text
roleId, agentId, displayName, organKind
status, activity, readiness, load, blocked, completed
heartbeatPhase, wakeIntensity, sleepDepth, memoryResonance
pressure, continuityRisk, reviewState, hasPatch
risk, confidence, evidenceGaps, recommendedAction
targetRole, hasBubble, speaking, jobCount
homeOrbitSlot, expressiveOffset, wanderRadius, gravityWellPulse
verticalLift, buoyancy, returnHomeStrength
traitActivations: map of canonical_state variable -> current_activation
```
## Normalization Rules
The renderer should convert loose surface strings into bounded scalars before
the shader sees them. First implementation rules:
```text
statusWeight("running") = 1.00
statusWeight("ready") = 0.72
statusWeight("needed") = 0.66
statusWeight("review") = 0.62
statusWeight("waiting") = 0.38
statusWeight("completed") = 0.34
statusWeight("blocked") = 0.18
statusWeight("unavailable") = 0.08
statusWeight(other) = 0.22
activity =
saturate(statusWeight(status) + min(jobCount, 3) * 0.08)
blocked =
status in {"blocked", "unavailable"} || blockingReason exists
completed =
status == "completed" || roleResult.status == "completed"
hasPatch =
roleResult.finding.statePatch exists
risk =
saturate(0.2 * count(finding.risks)
+ 0.16 * count(finding.evidenceGaps)
+ (blocked ? 0.25 : 0.0)
+ pressureWeight(pressure.level))
confidence =
finding.confidence if numeric
else 1.0 - saturate(0.5 * risk)
```
`pressureWeight` maps `unknown -> 0.15`, `low -> 0.1`, `medium -> 0.35`,
`high -> 0.65`, `critical -> 0.9`. Unknown is not zero; missing telemetry is
itself a small continuity smell.
## Coordinate Contract
All role placement and role-local SDF orientation uses Aquarium world space:
- `+X`: Grid right.
- `+Y`: Grid forward.
- `+Z`: Grid up.
- `gridCenter`: camera target on the XY plane.
- `selfAnchor`: `gridCenter`.
- `cameraFacingDir`: normalized horizontal vector from `gridCenter` toward the
camera. This is the room-facing direction.
- `cameraRightDir`: normalized horizontal camera right vector.
Each role has a stable home orbit slot around `selfAnchor`. These slots are
Aquarium-owned spatial projections of semantic Epiphany roles:
| Organ | Role id | Angle | Ring | Home formula |
| --- | --- | ---: | ---: | --- |
| Self | `coordinator` | 0 deg | 0.00 | `selfAnchor` |
| Face | `face` | toward camera | 0.58 | `selfAnchor + cameraFacingDir * r` |
| Imagination | `imagination` | 135 deg | 0.78 | `selfAnchor + polar(135) * r` |
| Eyes | `research` | 35 deg | 0.86 | `selfAnchor + polar(35) * r` |
| Body | `modeling` | 215 deg | 0.66 | `selfAnchor + polar(215) * r` |
| Hands | `implementation` | 270 deg | 0.82 | `selfAnchor + polar(270) * r` |
| Soul | `verification` | 325 deg | 0.78 | `selfAnchor + polar(325) * r` |
| Life | `reorientation` | 80 deg | 0.62 | `selfAnchor + polar(80) * r` |
`r` is the current agent-orbit radius, independent of Grid visual radius. A
good first value is `min(gridRadius * 0.48, 22)`, clamped to `[10, 24]`.
`polar(angle)` means `float2(cos(angle), sin(angle))` in the XY plane. The Face
slot is camera-relative because Face is literally the public mouth; it should
prefer the room side. All other slots are world-stable so spatial memory does
not rotate every time the camera does.
## Derived Spatial Anchors
The renderer must derive named anchors before evaluating per-role motion.
Epiphany does not publish world-space graph, job, artifact, nor role anchors.
It publishes semantic state. Aquarium owns the visible frame and projects that
semantic state into deterministic local anchors around role bodies.
```text
roleAnchor(roleId) =
role home orbit slot
userAnchor =
selfAnchor + cameraFacingDir * agentOrbitRadius * 1.15
artifactAnchor(roleId) =
roleAnchor(roleId) + normalize(roleAnchor(roleId) - selfAnchor) * 3.5
+ cameraRightDir * 2.0
evidenceAnchor =
artifactAnchor("research"), when Eyes has evidence ids, artifacts, graph
query results, plus retrieved context
else roleAnchor("research") + roleForward("research") * 3.0
implementationArtifactAnchor =
artifactAnchor("implementation"), when Hands has changed files,
implementation artifacts, plus active implementation jobs
else roleAnchor("implementation") + roleForward("implementation") * 3.0
reviewAnchor =
roleAnchor("verification")
continuityAnchor =
roleAnchor("reorientation")
targetAnchor =
roleAnchor(targetRole), if targetRole is a known role id
else evidenceAnchor, if recommendedAction == "graphQuery"
else continuityAnchor, if recommendedAction contains "reorient"
else reviewAnchor, if requiresReview
else roleAnchor(roleId) + roleForward(roleId) * 3.0
workAnchor =
implementationArtifactAnchor, if changed files, implementation artifacts,
plus active implementation jobs exist
else coordinator target role anchor, if targetRole is present
else reviewAnchor, if implementation is waiting on verification review
else roleAnchor("implementation") + tangentClockwise("implementation") * 4.0
```
`tangentClockwise(role)` is the XY tangent around `selfAnchor` at that role's
home slot. It gives an idle directional bias without pretending there is a real
target.
`roleForward(role)` is `normalize(roleAnchor(role) - selfAnchor)` for every
orbiting role. For Self, it is `cameraFacingDir`. If a role is exactly at
`selfAnchor`, use `cameraFacingDir`.
These anchors are not data contracts with Epiphany. They are Aquarium's
diegetic layout rules for semantic surfaces. If a later Aquarium feature
creates local graph-node and artifact objects, those objects may become anchors
inside Aquarium, but Epiphany still does not own their spatial arrangement.
State-derived vectors:
```text
homeVector = homeOrbitSlot - currentPosition
outwardVector = normalize(homeOrbitSlot - selfAnchor)
targetVector = targetAnchor - currentPosition
targetDirection = normalizeOr(targetVector.xy, outwardVector)
workDirection = normalizeOr(workAnchor.xy - handsPosition.xy,
tangentClockwise("implementation"))
evidenceDirection = normalizeOr(evidenceAnchor.xy - eyesPosition.xy,
roleForward("research"))
routeDirection = normalizeOr(roleAnchor(targetRole).xy - selfPosition.xy,
roleForward("coordinator"))
roleExpressionDirection =
normalizeOr(targetAnchor.xy - currentPosition.xy, roleForward(roleId))
```
`normalizeOr(value, fallback)` returns the fallback when the vector length is
below epsilon. This rule matters; near-zero direction vectors should not turn
into flickering NaNs with a nice color palette.
## SDF Rules
- Each organ gets a distinct SDF family, not only a color swap.
- Each SDF object shader exposes `sdfDistance` for the hot raymarch loop and
`sdfSurface` for the refined hit. `sdfSurface` returns PBR material values:
base color, metallic, roughness, and emission. The shared Aquarium shading
function derives dielectric/metal diffuse and specular response from those
values; role anatomy and region choice stay inside the Epiphany object shader.
- Material regions may exist inside an object shader when they help choose PBR
values for readable parts: core, shell, tool edge, lens, ribbon, risk seam,
memory seed, bubble, and so on. They are not part of the shared hot surface
packet.
- State changes should alter low-dimensional parameters first: radius, lobe
count, twist, aperture, rib spacing, pulse phase, shell openness, wake length.
- Keep DOM hit targets stable. The visual body can breathe, fold, and shimmer;
the interaction root must not dodge the user's pointer like it owes money.
- Avoid volumetric dependence. These are analytic solids over the Grid with
PBR materials, emissive accents, and planned diegetic labels.
## Motion Rules
Orbit lanes are home positions, not cages. Each agent should have a stable
`homeOrbitSlot` for readability and spatial memory, plus a bounded
`expressiveOffset` for short local wandering. The return force should be visible
and gentle: agents drift out to express curiosity, stress, speech, focus, and
avoidance, then ease back toward their lane instead of snapping into place.
Vertical motion should come from the same Grid grammar as placement. An agent's
gravity well can pulse its local Grid height, raising and lowering the body
because the CPU and shader agree that body centers sit above the Grid surface.
This makes heartbeat and mood visible in the world instead of faking hover with
a detached bobbing transform.
Recommended controls:
- `wanderRadius`: maximum horizontal distance from the home orbit slot.
- `expressiveOffset`: current bounded offset in Grid space.
- `returnHomeStrength`: spring force back toward the home orbit slot.
- `gravityWellPulse`: temporary change to the agent's Grid brush height.
- `verticalLift`: additional body-center lift derived from the local well pulse.
- `buoyancy`: role-specific damping and overshoot for vertical response.
Baseline placement integration:
```text
desiredOffset =
roleExpressionDirection * expressionAmount * wanderRadius
expressiveOffset =
dampedSpring(expressiveOffset, desiredOffset, returnHomeStrength, damping)
brushCenter =
homeOrbitSlot + expressiveOffset
gridHeight =
sampleSharedGridBrushes(brushCenter.xy)
bodyCenter =
float3(brushCenter.xy, gridHeight + 2 * bodyRadius + verticalLift)
```
Baseline role constants:
| Organ | Wander radius | Return strength | Buoyancy |
| --- | ---: | ---: | ---: |
| Self | 1.2 | 0.90 | 0.45 |
| Face | 3.2 | 0.62 | 0.85 |
| Imagination | 4.4 | 0.48 | 0.95 |
| Eyes | 2.4 | 0.78 | 0.52 |
| Body | 1.8 | 0.86 | 0.38 |
| Hands | 2.2 | 0.80 | 0.48 |
| Soul | 1.6 | 0.92 | 0.32 |
| Life | 2.0 | 0.84 | 0.55 |
The Grid brush uses the same `brushCenter` and `gravityWellPulse`. Vertical lift
is not an unrelated bob:
```text
gravityWellPulse =
heartbeatPulse * heartbeatLiftScale
+ actionPulse * actionLiftScale
- pressureSink * pressureSinkScale
verticalLift =
gravityWellPulse * buoyancy
```
The exact brush equation can change with the Grid implementation, but CPU
placement and shader placement must call the same equation; otherwise the agent will
visibly detach from its own well.
State mapping:
- Heartbeat primary state should create a small vertical pulse and brief outward
drift.
- Speaking and Face bubble output can wander toward the room-facing side.
- High pressure should pull Life, Soul, and Self tighter toward home while still
deepening their wells.
- Imagination may wander widest when exploring, but should collapse back toward
home on accepted plans.
- Hands should wander least while blocked and lean toward active work when
running.
- Eyes can make short focus darts toward Aquarium's local evidence anchor.
Implementation invariant: expressive wandering and vertical well pulses must be
part of the same CPU-visible placement model used for body centers, shadows,
gravity brushes, and temporal reprojection. If the shader sees a different body
position than the CPU, the prettiness has started lying.
## Self
Role ids: `coordinator`, `epiphany.self`
Purpose: routing, authority, checkpoint, run state, and review boundary.
Appearance: a central routing heart made of a black void command sphere with a
sharp golden Fresnel rim, one gold core band, and nested orichalcum rail bands.
The rails form a compact orrery lattice around the void, with compact gate nodes
at true triple-band crossings and narrow emissive strips along the inner rail
faces. Self should feel central and decisive, not busy. The orrery comparison is
a motion and topology metaphor: the routing rails should have nested orbital
order and slow phase motion rather than static, intersecting hoops. The angelic
read should come from overwhelming but coherent symmetry and radiance, not
literal symbolic anatomy.
Reasoning: Self is not a worker lane. It preserves agency by routing work,
rejecting bad authority claims, and deciding which organ deserves the next
move. A void core inside an ordered routing lattice makes the idea visible:
many possible paths, one current routing authority.
Math:
- Exact stable sphere core rendered as a black void with golden Fresnel
edge-lighting, bypassing the shared PBR shader for that core region. Keep one
gold lattice band on the core surface; do not stack multiple crossing bands
whose material regions can overtake the sphere.
- A compact log-spherical lattice maps local direction and logarithmic radius
into stable nested shells. Orbital rails are level sets where fixed radial
shell strata cross angular harmonic bands; gate nodes are compact crossings
where all angular bands agree in that same lattice. Pairwise shallow rail
intersections should remain rail material, not stretch into gate-colored
slabs.
- Shell radius is structural, not animated. Phase may rotate each shell's
angular frame and add mild band distortion, but it must not move radial zero
crossings or create rings by expanding points into circles. Multiple
concentric shells should occupy orthogonal orbital frames so complexity nests
cleanly instead of colliding on one surface.
- Smooth union is reserved for joining the core to lattice-derived rails,
gates, inlays, and seam marks. Do not hand-place independent hoops or
separate torus inventories when the form wants one coherent orbital grammar.
- Material regions: black void core, one gold core band, metallic
gold/orichalcum routing rails, inner rail emission strip, bright current
target gate, and dark review seam. Rails should be readable bands rather than
hairline emitters. Bloom-driving emission is controlled: core Fresnel, active
gates, and narrow inner rail strips; the broad rail body uses PBR material
contrast so it does not turn into fireflies.
- Implementation note: complexity should emerge from the coordinate
abstraction. If the shader grows by adding more named hoop equations, stop and
rebuild the field grammar instead.
Movement:
- Slow center pulse from heartbeat phase.
- Orbital rail families rotate at different slow phase rates so the orrery is a
motion metaphor, not static decoration.
- `targetRole` resolves through `roleAnchor(targetRole)`. The active routing
gate is the torus arc whose tangent points closest to `routeDirection`.
- Selected target role lights that gate and sends a traveling pulse along the
matching arc.
- `requiresReview` adds a narrow dark seam around the core.
- `canAutoRun` sharpens and brightens the active gate.
- High pressure makes the arcs tighten around the core instead of expanding.
Surfaces:
- `epiphany.surface.coordinator`: `action`, `targetRole`,
`recommendedSceneAction`, `requiresReview`, `canAutoRun`, `reason`,
`sourceSignals`.
- `epiphany.surface.roles`: lane summaries.
- `epiphany.agent_heartbeat`: Self participant readiness, reaction, and wake.
- `epiphany.surface.pressure`: compaction and routing pressure.
## Face
Role ids: `face`, `epiphany.face`
Purpose: public expression, bubbles, drafts, posts, and room-native speech.
Appearance: a speech anemone / phoneme lantern: a translucent bell with a
continuous living mouth aperture, warm throat glow, and syllable pearls carried
by a small flow around the lip. It should look like bounded speech becoming
public, not a generic chat icon.
Reasoning: Face translates hidden organ weather into public, bounded speech. A
lantern form makes speech feel emitted and warm, while the mouth flow makes
drafts, posts, and bubble count visible without dumping private machinery.
Face is a public aperture: it shapes what leaves the system.
Math:
- Governing domain: local mouth-flow coordinates around the bell opening.
`u` is angle around the lip, `v` is depth through the throat, and `breath`
is driven by speech/activity state. The lip, throat glow, speech ribbon, and
beads derive from this one aperture field.
- Bell body from a translucent superquadric dome with exponent `0.42`, softened
into a flared lower lip by the mouth-flow aperture instead of clipped by a
separate decorative cut.
- Mouth aperture as a continuous signed field in `(u, v)`: closed silence is a
narrow crescent, speaking opens toward an oval, and uncertainty introduces a
small asymmetric curl without changing the bell's root topology.
- Draft pearls occupy short inner streamlines inside the lip. Posted bubbles
escape to wider outer streamlines. Bead distance from the mouth carries state;
do not use arbitrary orbit count as the main read.
- Speech ribbon is a swept lemniscate/current line around the mouth-flow field,
with radius and brightness modulated by heartbeat and public-output activity.
- Material regions: translucent enamel bell, warm emissive throat, glossy
syllable pearls, muted draft pearls, brighter posted pearls.
Topology invariants:
- The mouth is an aperture field in the bell, not a pasted-on icon or two torus
cuts.
- Drafts stay inside the bell; posts move outside the lip.
- No speech surface may expose private machinery. Face shows public output
pressure, not internal chain-of-thought weather.
- Forbidden shortcut: chat bubble logo, decorative orbit beads, or a generic
smiling blob.
Movement:
- `hasBubble` opens the mouth and releases a small bead pulse.
- Drafts orbit close and dim; posts orbit farther and brighter.
- Speaking drift direction is `normalize(userAnchor - facePosition)`, capped by
Face's `wanderRadius`.
- Heartbeat primary state makes the lantern brighten from inside.
- Silence closes the mouth but keeps a slow listening breath.
Surfaces:
- `epiphany.surface.face`: `bubbles`, `drafts`, `posts`, `latestArtifacts`,
`status`.
- `epiphany.agent_heartbeat`: bridge decisions, thought lanes, selected role,
Face participant readiness.
- `epiphany.agent_memory`: Face public-description and presentation traits.
## Imagination
Role ids: `imagination`, `epiphany.imagination`
Purpose: planning, drafts, backlog, objective shaping, and candidate options.
Appearance: an impossible flower made from translucent prismatic ribbon-sheets
around a luminous opal idea seed. It should read like glass, soap film, and
aurora caught in a botanical topology: broad curling sheets, raised glowing
rims, nested negative-space apertures, and a vertical fountain of candidate
forms. The silhouette should keep almost becoming another silhouette.
Reasoning: Imagination is generative, but not decorative chaos. It produces
bounded candidate shapes: captures become drafts, drafts become objectives. Petals
and ribbons communicate branching possibility while still returning to a center.
Math:
- Central opal bud: a faceted egg/superquadric seed, vertically upright, with a
small calyx-like cradle at its base. This is the topological root; all ribbons
either emerge from it or visibly curl around it.
- Primary bloom: six large ribbon-sheets arranged as three mirrored pairs around
the seed. Each sheet has one continuous center curve rising from the lower
cradle, flaring outward, curling back inward to form a visible aperture, then
tapering to a lifted lip. The sheet is a thin slab/film around that curve, not
a fat ellipsoid or simple tube.
- Sheet parameterization:
`u in [0,1]` moves from root to lip.
`center(u)` is a cubic Bezier or equivalent curve with upward lift, outward
flare, and inward curl.
`width(u)` grows from a narrow root to a broad blade, then narrows at the lip.
`thickness(u)` stays thin and nearly constant; visible mass comes from the
curled sheet area and raised rim, not from petal bulk.
`curl(u)` rotates the sheet normal around the center curve so the blade forms
a loop/aperture.
- Raised rim: every primary sheet has two brighter rim tubes running along its
long edges. Rims define the readable silhouette and may use tapered capsule
chains. The translucent film between rims may be approximated with bounded
sheet/slab fields or material shimmer, but the rim paths must be actual traced
geometry.
- Nested secondary curls: inside each primary aperture, add one smaller echo
ribbon or rim arc that follows the same curl direction. These secondary forms
are lower priority than the primary silhouette and should be LOD-gated before
they become a compile/runtime burden.
- Vertical candidate filaments: a sparse set of golden emissive tendrils rises
from the seed through the center gap. These are thin tapered curves with small
bead sparks, not a particle swarm.
- Harmonic layout controls: the overall bloom uses rotational harmonics only to
place and phase the ribbon families:
`pairCount = 3`, `primaryCount = 6`, `openness = activity`,
`curl = lerp(0.55, 1.25, activity)`, and
`secondaryCurlCount = min(backlogCount, 3)`.
Do not use a decorative polar radius field as the primary surface unless it
produces the same sheet, rim, and aperture topology.
- Smooth unions are allowed at structural joins: seed-to-root cradle, root
bundles, rim-to-sheet contact, and bead/tendril bases. Do not smooth-union the
whole bloom into a blob; negative space is part of the form.
- Material regions: opal seed, translucent pearlescent film, bright cyan/gold
rim highlights, emissive idea sparks, warm candidate filaments, and cool
backlog shadow.
Topology invariants:
- The first read is a luminous central seed wrapped by broad translucent curling
ribbons.
- At least three clean interior apertures must remain visible from the default
camera angle.
- Primary ribbons must be sheets with rim structure, not merely tubes. Tubes may
carry rims and tendrils, but they do not satisfy the petal requirement alone.
- The bloom is vertically biased: lower roots gather near the seed, upper lips
lift and curl outward like a fountain.
- Fine glitter, internal color streaks, and star specks are material/shading
detail unless they are sparse bead sparks or candidate filaments.
Movement:
- Planning readiness increases petal openness.
- Backlog and capture count increases secondary harmonic detail.
- A completed planning result collapses the flower into a cleaner symmetric
bloom.
- Uncertainty and evidence gaps add small branching petallets that fade rather than
becoming permanent complexity.
- Exploratory wander direction is the weighted sum of `outwardVector` and
`tangentClockwise("imagination")`; accepted plans set `expressionAmount`
toward zero so the flower returns home.
Surfaces:
- `epiphany.surface.roles`: `imagination` lane status and note.
- `epiphany.surface.role_result`: imagination result, finding, patch, evidence
gaps.
- `epiphany.surface.planning`: captures, backlog, roadmap streams, objective
drafts.
- `epiphany.agent_memory`: planning temperament and objective-shaping traits.
- `epiphany.agent_heartbeat`: Imagination wake, rumination, and incubation.
## Eyes
Role ids: `research`, `epiphany.eyes`
Purpose: evidence, prior art, graph query, artifacts, and outside-source sight.
Appearance: an evidence astrolabe: a compact black-glass lens held inside a
rotating proof lattice, with aperture petals, meridian lines, and small cyan
evidence sparks snapped to validated source directions. It should read as
perception and proof, not surveillance theater.
Reasoning: Eyes earns visual sparkle only when evidence exists. The aperture
metaphor is useful because research narrows and widens attention; the astrolabe
read makes that narrowing feel like constraint by evidence, not answer-claiming
or watching-for-its-own-sake.
Math:
- Governing domain: a lens hemisphere facing `evidenceDirection`. Directions
are projected into tangent disk coordinates on the lens; aperture blades,
meridians, scan arcs, and evidence sparks share this domain.
- Lens from an intersection of two spheres or a compact biconvex SDF with black
glass material and cyan edge caustics.
- Aperture petals as logarithmic spiral shutters in the tangent disk. Unknowns
widen the aperture; confidence narrows and steadies it.
- Proof lattice as a sparse set of meridian and latitude grooves over the lens
hemisphere. These are constraint lines, not decoration.
- Evidence sparks occupy validated source directions on the hemisphere or their
nearest meridian positions. No evidence means no sparks.
- Scanning ring is a pale arc in the same tangent disk, animated by query state
and masked by aperture openness.
- Material regions: black glass lens, blue-metal aperture petals, pale proof
meridians, cyan evidence sparks, soft unfocused unknown halo.
Topology invariants:
- The first read is a dark lens constrained by an instrument lattice.
- Evidence lights must attach to evidence-bearing directions or meridian lines;
random sparkle is a lie.
- Unknowns soften and widen the optical field; confidence sharpens and narrows
it.
- Forbidden shortcut: robot eye, camera iris icon, or generic sparkly orb.
Movement:
- Graph query and artifact presence rotates aperture and adds controlled sparkle.
- Unknowns widen the aperture and slow the scan.
- High confidence narrows aperture and steadies the lens.
- No evidence means no sparkle. The lens still watches, but it does not fake a
finding.
- Focus dart direction is `evidenceDirection`. `evidenceAnchor` is Aquarium's
local projection for evidence-bearing surfaces; Epiphany only supplies the
semantic evidence, artifact, and context state.
Surfaces:
- `epiphany.surface.graph_query`: graph query state and results.
- `epiphany.surface.context`: retrieved context and source windows.
- `epiphany.surface.role_result`: research result and Eyes result when present.
- `epiphany.surface.roles`: research lane status where published.
- `epiphany.agent_memory`: evidence and prior-art traits.
## Body
Role ids: `modeling`, `epiphany.body`
Purpose: architecture, graph anatomy, dataflow, checkpoint model, and structural
fit.
Appearance: a living load-bearing graph stone: a heavy soft geode mass with
embedded stress ribs, enamel graph veins, checkpoint pearls, and occasional dark
fault seams. It should be adorable by weight and stability, not by making baby
noises in shader form.
Reasoning: Body models the machine's shape. It should feel structural,
grounded, and tactile. The graph ribs make architecture visible as load paths,
while the soft mass reminds us the model serves the living system rather than
becoming a diagram shrine.
Math:
- Governing domain: superellipsoid surface coordinates. `u` and `v` parameterize
the geode surface; ribs are geodesic, loxodrome, meridian, equator, or fault
curves in that domain, not tubes glued onto arbitrary world positions.
- Superellipsoid/geode core with exponent `0.72`, soft flattened base, and
optional support lobes only when they are part of the same surface field.
- Graph ribs as embedded enamel stress paths over the surface. First
implementation uses a small deterministic grammar: one meridian path, one
equator path, two diagonal loxodromes, and one fault-line family. Semantic
graph size and frontier count brighten, fill, or split these paths; Epiphany
graph nodes do not define their world positions.
- Checkpoint pearls are embedded at rib intersections and fill in sequence.
- Evidence gaps open dark fault seams between ribs. Blocked state widens faults
rather than spawning unrelated symbols.
- Subtle displacement from low-frequency breathing mass must preserve the rib
domain so veins do not slide over the body.
- Material regions: matte green-black soft stone, glossy rib enamel, bright
checkpoint pearls, dark blocked fissures, faint warm interior where ribs meet.
Topology invariants:
- The first read is heavy soft mass with structural load paths.
- Ribs are surface-domain stress lines, not arbitrary graph overlays.
- Checkpoint nodes are embedded pearls, not free-floating beads.
- Forbidden shortcut: soft blob plus random graph lines.
Movement:
- Modeling `needed` state lowers the body and brightens empty rib slots.
- Completed checkpoint and result fills nodes in sequence.
- Evidence gaps open small dark fissures between ribs.
- High load makes the mass sag; high confidence makes ribs align and glow
steadily.
- Connective wake direction points from Body toward Self when modeling is
regathering and checkpointing, and toward `artifactAnchor("modeling")` when a
modeling result plus state patch exists. `frontierNodeIds` affect rib count and
brightness, not world direction.
Surfaces:
- `epiphany.surface.roles`: `modeling` lane status, note, recommended action.
- `epiphany.surface.role_result`: modeling finding, checkpoint summary,
frontier nodes, state patch, evidence gaps.
- `epiphany.surface.graph_query`: architecture and dataflow graph context.
- `epiphany.agent_memory`: Body graph and checkpoint traits.
## Hands
Role ids: `implementation`, `epiphany.hands`
Purpose: source changes, runnable work, changed files, and implementation
artifacts.
Appearance: a compact code-working mandible tool: a polished dark tool-seed
with three continuous gripper lobes, a warm enamel grip field, and a hot forward
cutting seam. It should look ready to touch code, not like a floating wrench
logo.
Reasoning: Hands is the only lane allowed to touch the code body. The shape
should feel tactile and precise, with motion that acknowledges work quickly and
then settles. No grand aura. Hands earns drama by producing diff.
Math:
- Governing domain: prolate tool coordinates aligned to `workDirection`.
Longitudinal coordinate `u` runs from rear grip to forward edge; angular
coordinate `theta` wraps the core; radial coordinate gives the gripper field.
- Tool core is an elongated superquadric seed, exponent `0.58`, with a subtle
helical groove cut along the same prolate domain.
- Three gripper lobes are phase lobes of one continuous tri-jaw chuck field
around the core. They open, retract, and twist through `theta`; do not build
disconnected claws unless a future measured constraint forces that cut.
- Tool edge is a hot ridge on the forward side of the same field, not a separate
logo blade. Its cutting line may use a sine notch, but it remains locked to
`workDirection`.
- Changed files and implementation artifacts appear as tiny metal filings or
sparks near the forward seam, then decay quickly.
- Blocked state retracts jaws and adds a dull cap over the edge rather than
spawning an unrelated warning badge.
- Material regions: warm enamel grip, dark polished core, high-metal edge, hot
action stripe, dull blocked cap, brief file-spark flecks.
Topology invariants:
- The first read is a precise working tool-seed with a continuous tri-jaw field.
- All grasping geometry derives from `workDirection` and the prolate tool
domain.
- Changed-file sparks are receipts, not decoration, and should fade.
- Forbidden shortcut: wrench icon, floating detached claws, or generic sparks.
Movement:
- Running and continuing work leans the core toward `workAnchor`.
- Changed files and artifacts trigger quick acknowledgement pops.
- Blocked state retracts grippers and dims the tool edge.
- Verification pass and accepted result relaxes the shape back into idle.
Hands spatial rule:
```text
handsForward = workDirection
handsRight = float2(-handsForward.y, handsForward.x)
coreLength = lerp(1.15, 1.65, activity)
coreRadius = lerp(0.46, 0.38, activity)
toolEdgePos = handsCenter.xy + handsForward * coreLength * 0.48
gripPosA = handsCenter.xy - handsForward * 0.18 + handsRight * 0.34
gripPosB = handsCenter.xy - handsForward * 0.18 - handsRight * 0.34
```
`workDirection` is not semantic mood. It is derived from `workAnchor` in the
coordinate contract above. `workAnchor` is Aquarium's local projection of
implementation artifacts and jobs, coordinator target, and review dependency. If
none of those semantic states exist, it falls back to the clockwise orbit
tangent, so the shape remains deterministic and debuggable.
Surfaces:
- `epiphany.surface.roles`: `implementation` status, note, jobs.
- `epiphany.surface.jobs`: job ownership, progress, blocking reason.
- `epiphany.surface.role_result`: implementation receipts if surfaced.
- `epiphany.runtime.job` and `epiphany.runtime.job_result`: native runtime
work state.
- `epiphany.agent_memory`: Hands implementation discipline traits.
## Soul
Role ids: `verification`, `epiphany.soul`
Purpose: verification, confidence, risk, findings, invariants, and review.
Appearance: a verdict polyhedron / oath crystal: a compact faceted body with a
soft white inner oath light, blue confidence edges, red risk faults, and engraved
finding lines. It should be severe but not hostile.
Reasoning: Soul protects promises. Facets communicate exactness and review,
while the inner light keeps it from becoming a punishment object. The risk seam
shows tension between confidence and unresolved danger; the form is constraint,
not punishment.
Math:
- Governing domain: a family of signed half-space planes. The body is the
rounded intersection of constraint planes; confidence, risk, and review state
adjust plane offsets, bevel radius, and fault intensity.
- Rounded octahedron or small polyhedron SDF using max-of-planes blended with
radius `0.08`; higher confidence sharpens bevels, lower confidence softens
and clouds them.
- Inner oath core is a small white sphere or glow visible through material
contrast, not a transparency dependency.
- Risk seam is a red-blue great-circle fault chosen in the crystal's plane
domain. Ring thickness is `lerp(0.015, 0.09, risk)`, but it should follow
facet intersections when possible instead of reading as a decorative torus.
- Finding marks are engraved grooves along facet intersections. Accepted
findings become clean blue lines; failed or blocked findings create short
jagged red fractures that can later heal.
- Material regions: cool crystal facets, white oath core, blue confidence
edges, red risk faults, dark rejected marks.
Topology invariants:
- The first read is a faceted constraint object with an inner promise light.
- Facets own the form. Seams, marks, and fractures are plane-domain features,
not decals floating on a gem.
- Risk makes the crystal tenser; confidence makes it clearer and steadier.
- Forbidden shortcut: generic gemstone with a random red ring.
Movement:
- Verification running rotates slowly with steady, low expressiveness.
- Risk increases red seam thickness and facet sharpness.
- Confidence brightens blue edges and steadies rotation.
- Failed and blocked findings produce short angular fractures; accepted findings
heal them into clean facet lines.
- Review-facing posture points the risk seam normal toward
`roleAnchor(coordinator)` when `requiresReview` is true, else toward
`outwardVector`.
Surfaces:
- `epiphany.surface.roles`: `verification` status and review posture.
- `epiphany.surface.role_result`: finding verdict, risks, evidence gaps,
self-persistence review, status.
- `epiphany.surface.pressure`: compaction and context pressure.
- `epiphany.surface.coordinator`: `requiresReview` and source signals.
- `epiphany.agent_memory`: Soul evidence and invariant traits.
## Life
Role ids: `reorientation`, `epiphany.life`
Purpose: continuity, pressure, sleep, reorientation, resume posture, regather posture, and
memory survival.
Appearance: a memory nautilus seed: a teal protective shell wrapped around a
small warm ember, with a logarithmic seam and memory beads traveling along the
same spiral path. It should feel like continuity surviving weather.
Reasoning: Life carries the thread through compaction, sleep, and rupture. A
seed-shell is the right metaphor because it protects a candidate continuation,
while the ember and bead trail show what context remains warm enough to carry
forward. The spiral is continuity under pressure, not a decorative tail.
Math:
- Governing domain: a revolved seed/cardioid shell with logarithmic spiral
coordinates. The shell seam, memory bead trail, regather motion, and resume
release all use the same spiral grammar.
- Seed body from a revolved cardioid SDF with a flattened base and protective
shell thickness. Pressure tightens the shell rather than scaling it randomly.
- Spiral seam as a logarithmic groove carved along the seed. Regather opens this
groove; sleep narrows it while keeping the ember visible.
- Ember as a small warm emissive sphere held inside the shell aperture.
- Memory beads are samples along the same logarithmic spiral, trailing toward
`continuityAnchor`. Regather pulls bead phase inward toward the shell; resume
advances phase outward along the old path.
- Breathing field as low-amplitude radial displacement locked to the shell
domain so the seam and beads remain coherent.
- Rupture scar as a dark interruption in the seam, not a separate wound symbol.
- Material regions: teal shell, warm ember, pale memory beads, dark rupture
scar, faint pearlescent seam edge.
Topology invariants:
- The first read is a protective seed-shell with a visible ember.
- Seam and bead trail share one logarithmic spiral. If they disagree, continuity
has become decoration.
- Pressure tightens protection; resume releases memory; sleep preserves ember.
- Forbidden shortcut: seed plus unrelated orbiting dots.
Movement:
- Pressure increases breathing depth and tightens the shell.
- Regather opens a dark seam and pulls beads inward.
- Resume steadies the ember and releases beads along the old path.
- Sleep and incubation slows orbit and dims the shell while preserving the ember.
- Memory bead trail follows a logarithmic spiral centered on Life's
`bodyCenter.xy`. Regather reverses bead phase toward the seed; resume advances
phase outward along `continuityAnchor -> lifePosition` tangent.
Surfaces:
- `epiphany.surface.reorient`: decision action, checkpoint status, pressure
level, reasons, retrieval status, watcher status.
- `epiphany.surface.reorient_result`: reorientation worker result.
- `epiphany.surface.pressure`: pressure level and compaction readiness.
- `epiphany.agent_heartbeat`: sleep cycle, memory resonance, incubation.
- `epiphany.agent_memory`: Life continuity and memory traits.
## First Implementation Slice
## Realtime Rendering Constraints
The role bodies are mathematically rich enough to become expensive if treated
as one heroic scene SDF. Aquarium must render them as bounded per-agent
implicit objects.
Literature-grounded rules:
- Sphere tracing advances safely only when the step distance is an exact
distance or conservative distance bound. Fancy algebraic fields must never be
used as raw march steps unless wrapped by a conservative bound.
- Every agent has a cheap proxy bound. Rasterize that proxy and raymarch only
inside the proxy pixels.
- Never evaluate all role SDFs in one scene-global march. Select one role SDF
from `AgentVisualState.organKind` after the proxy is hit.
- Each role SDF starts with a cheap bound distance. If the point is outside the
detailed region, return that bound without evaluating petals, claws, ribs,
beads, seams, sparks, and grooves.
- Every expensive feature has its own local bound. Feature distance functions
are evaluated only when the ray point is near that feature's region.
- Normals are computed only after a hit. Use analytic gradients for simple
primitives, and tetrahedral four-tap finite differences for full-detail
composite organs.
- Shadows are budgeted separately. Default agents use direct body lights, IBL,
and Grid contact shadow. A selected hero agent may get a four-to-eight step
cone shadow. No nested shadow march per light.
- Antialiasing starts with pixel-footprint-aware hit epsilon. Later edge
refinement can use selective cone-style supersampling near SDF boundaries.
LOD contract:
| Tier | Use | Geometry |
| --- | --- | --- |
| LOD 0 | selected, hovered, speaking, primary heartbeat, near camera | full role SDF with material regions and feature animation |
| LOD 1 | ordinary visible agents | core shape plus major silhouette features |
| LOD 2 | distant agents | single recognizable mathematical primitive plus emissive accents |
| LOD 3 | very distant swarm context | billboard, sparkle, or project label |
The initial renderer budget target is one to two milliseconds total for all
visible agent bodies on a midrange GPU. The debug UI must expose step count,
proxy-hit coverage, state scalars, LOD tier, and SDF cost tier before the shader
grows beyond the first two role bodies. If a role needs material-region
inspection, expose it through an explicit debug lane rather than adding a
permanent region field to the shared PBR surface packet.
Implement the renderer contract in this order:
1. Add a CPU-side `AgentVisualState` and role-to-organ mapping in the Aquarium
runtime boundary.
2. Preserve the current fixed body table as fallback data.
3. Add CPU-visible home orbit slots, bounded expressive offsets, and gravity
well pulse state before adding decorative per-role motion.
4. Add per-agent proxy bounds and rasterize those proxies before raymarching.
5. Add LOD 1 material-region SDF evaluation for one organ pair: Body and
Imagination.
6. Bind status, activity, heartbeat, and placement fields into shader constants.
7. Add debug modes for role id, state scalar, step count, proxy hit, LOD tier,
and SDF cost tier so the cut can be inspected without
believing the prettiness.
Body and Imagination are the best first pair because they exercise opposite
grammars: grounded graph mass and generative harmonic bloom. If both can share
the same state contract without visual mush, the rest of the organ set has a
real spine.

View file

@ -1,666 +0,0 @@
# Epiphany Extraction And Render API Plan
Aquarium is the engine. Epiphany is one client app.
The current solution has the first physical boundary in place:
- `Aquarium.Engine` owns the host, D3D12 renderer, input loop, shader reload,
debug overlay, and client runtime loader.
- `Aquarium.Engine.Contracts` is the shared API surface between host and client.
- `Aquarium.Epiphany` owns the current runtime state, camera rig, CultCache
documents, and client factory.
That split is real but shallow. The engine still carries Epiphany-shaped world
knowledge inside `D3D12Renderer`: fixed role counts, Self/cursor constants,
agent shader paths, body light setup, Grid brush generation, orbit fallback
motion, and the body visual buffer layout. The client can reload, but it cannot
yet describe what gets rendered. The renderer remains a monolith with a client
mask taped to the front. Rude, but accurate.
## Design Target
Client authors should be able to describe a frame in C# using named engine
abstractions:
- render targets with formats, size policies, clears, history, and persistence
- cameras with projection, view, previous-frame state, jitter, and named use
- shaders with hot reload, includes, entry points, root bindings, and defaults
- passes that declare reads, writes, depth, viewport, draw shape, and diagnostics
- structured buffers, constants, textures, samplers, cubemaps, and body registries
- standard features such as bloom, tonemapping, overlay, IBL, resize handling,
shader reload, transient descriptors, barriers, and debug views
Aquarium should make the ordinary path boring:
```csharp
public sealed class EpiphanyClient : AquariumClient
{
protected override void Configure(AquariumApp app)
{
var mainCamera = app.Cameras.Orbit("main")
.LookAtGrid()
.WithHistory();
var gridHeight = app.RenderTargets.Create("grid-height")
.FixedSize(128, 128)
.Format(RenderFormat.R16Float)
.Clear(0.0f);
var scene = app.RenderTargets.Hdr("scene")
.MatchWindow()
.WithHistory();
app.Graph.Pass("grid-height")
.Fullscreen("Shaders/GridHeight.hlsl", "GridHeightPS")
.Write(gridHeight)
.BindFrame()
.Bind("GridBrushes", GridBrushes);
app.Graph.Pass("scene")
.Fullscreen("Shaders/Scene.hlsl", "ScenePS")
.Read(gridHeight)
.Write(scene.Color, scene.Metadata, scene.Control)
.Depth(scene.Depth)
.UseCamera(mainCamera)
.Bind("BodyLights", BodyLights);
app.Features.Bloom(scene.Color);
app.Features.Tonemap(scene.Color).Present();
app.Features.DirectWriteOverlay();
}
}
```
That is the level of API Aquarium should expose: explicit enough to be durable,
ergonomic enough that nobody has to learn descriptor heaps before rendering a
flower. Descriptor heaps are Aquarium's problem. If the client author asks for a
sharp object, the engine hands them a handle, not the knife factory.
## Literature Anchor
This plan follows the render graph pattern used in production engines:
- Unity describes render graphs as high-level render-pass and resource
declarations that simplify pipeline configuration and let the system manage
resources efficiently: https://docs.unity.cn/Packages/com.unity.render-pipelines.core%4014.0/manual/render-graph-system.html
- Unreal's Rendering Dependency Graph defers execution until all passes are
gathered, then uses whole-frame knowledge for scheduling, barriers, memory,
async compute, and validation:
https://dev.epicgames.com/documentation/ru-ru/unreal-engine/rendering-dependency-graph?application_version=4.27
- Frostbite's FrameGraph talk frames the same problem as a graph of passes and
resources that keeps rendering features modular without giving up efficiency:
https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-RenderingArc
- Granite's Vulkan render graph writeup is useful implementation pressure:
declare reads and writes, validate, traverse dependencies, assign logical
resources to physical resources, generate barriers, alias compatible
transient targets, then execute:
https://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/
The Aquarium version should start smaller than those engines. No async compute,
no memory aliasing, no multipass merging in the first cut. First: client-owned
declarations, engine-owned execution.
## Current Coupling Inventory
These are the seams to cut.
### Shared Contracts
`Aquarium.Engine.Contracts` currently exposes:
- `IAquariumRuntime`
- `IAquariumRuntimeFactory`
- `AquariumRuntimeOptions`
- `AquariumFrame`
- `GraphicsSettings`
- `GridFrame`
- input state
`AquariumFrame` is too specific. It assumes one Grid, one camera position, one
cursor, one time scalar, and a renderer that knows what those mean.
### Host
`AquariumHost` owns:
- client assembly load and reload pointer
- window creation
- renderer construction
- debug input routing
- settings synchronization
- mouse projection onto the Grid plane
The mouse projection is client policy. The engine may provide ray utilities,
but `ProjectMouseToGridPlane` belongs in the client or in a reusable camera
helper chosen by the client.
### Renderer
`D3D12Renderer` owns both engine machinery and Epiphany scene policy:
- engine machinery: device, swapchain, fences, command list, descriptor arenas,
upload ring, shader compilation, render targets, overlays, resize, resource
transitions, pipeline state
- Epiphany policy: Grid height target, Self gravity brush, role agent count,
fixed agent body shader list, cursor hibiscus buffer slot, body lights, orbit
fallback positions, Grid placement math, agent state scalars, debug mode names
The renderer should retain the machinery and lose the policy.
### Shader Assets
Engine shader assets currently include Epiphany bodies:
- `D3D12SelfBody.hlsl`
- `D3D12FaceAgent.hlsl`
- `D3D12ImaginationAgent.hlsl`
- `D3D12EyesAgent.hlsl`
- `D3D12BodyAgent.hlsl`
- `D3D12HandsAgent.hlsl`
- `D3D12SoulAgent.hlsl`
- `D3D12LifeAgent.hlsl`
- `D3D12CursorBody.hlsl`
- `D3D12AgentCharacters.hlsli`
Those should move under the Epiphany client shader root once the engine can load
client-declared shaders. Engine-owned includes should remain only when they are
generic: SDF primitives, body proxy mechanics, PBR lighting, fullscreen vertex
helpers, packing helpers, and graph binding conventions.
## Target Project Shape
First stable shape inside this repo:
```text
src/
Aquarium.Engine.Contracts/ public client-facing API
Aquarium.Engine/ host plus D3D12 backend implementation
Aquarium.Epiphany/ Epiphany client app/runtime
```
Longer-term package shape:
```text
src/
Aquarium.Engine.Abstractions/ public API, no Vortice dependency
Aquarium.Engine.D3D12/ D3D12 backend
Aquarium.Engine.Host/ default Win32 host executable
Aquarium.Epiphany/ client app, eventually movable out of repo
```
Do not split projects before the API shape earns it. Premature project shrapnel
is not architecture. It is paperwork with delusions.
## Public API Model
### Client Entry
Replace `IAquariumRuntime.Frame` with a richer client lifecycle:
```csharp
public interface IAquariumClient : IDisposable
{
void Configure(AquariumAppBuilder app);
void Load(AquariumLoadContext load);
void Update(AquariumUpdateContext update);
void Render(AquariumRenderContext render);
void FlushState();
}
```
`Configure` declares stable graph shape. `Update` mutates CPU state. `Render`
uploads per-frame data and selects dynamic pass instances. The engine compiles
and executes the graph.
Keep a compatibility adapter from the old `IAquariumRuntime` while migrating,
then remove it when Epiphany is fully on the new lifecycle.
### Handles
Expose typed handles, not D3D12 objects:
```csharp
public readonly record struct RenderTargetHandle(string Name);
public readonly record struct DepthTargetHandle(string Name);
public readonly record struct CameraHandle(string Name);
public readonly record struct ShaderHandle(string Name);
public readonly record struct BufferHandle<T>(string Name) where T : unmanaged;
public readonly record struct TextureHandle(string Name);
public readonly record struct PassHandle(string Name);
```
Names are diagnostic identity. The engine maps them to physical resources.
### Render Targets
Render target declarations should cover the common case without ceremony:
```csharp
app.RenderTargets.Create("scene")
.MatchWindow(scale: 1.0f)
.Format(RenderFormat.Rgba16Float)
.Clear(Color.Black)
.Sampled()
.History(frames: 2);
app.RenderTargets.Create("grid-height")
.FixedSize(128, 128)
.Format(RenderFormat.R16Float)
.Clear(0.0f)
.Sampled();
```
Engine responsibilities:
- allocate and resize physical resources
- allocate RTV, DSV, SRV, UAV descriptors
- create transient descriptors per frame
- preserve history targets
- validate format and usage combinations
- transition states
- expose debug names and graph viewer data
### Cameras
The engine should provide camera math utilities and history storage. The client
chooses semantics:
```csharp
var camera = app.Cameras.Perspective("main")
.Position(() => CameraRig.Position)
.LookAt(() => CameraRig.Target3)
.VerticalFovDegrees(58)
.NearFar(0.05f, 500.0f)
.WithPreviousFrame()
.WithMouseRays();
```
Engine-owned:
- view/projection matrices
- inverse matrices
- viewport transforms
- previous-frame storage
- jitter policy
- screen ray generation helpers
Client-owned:
- what the camera tracks
- Grid center
- orbit controls
- mouse-to-world projection policy
### Shaders And Includes
Shader declarations should hide D3DCompile details but expose the real contract:
```csharp
var sceneShader = app.Shaders.Hlsl("scene")
.File("Shaders/Scene.hlsl")
.Vertex("FullscreenTriangleVS")
.Pixel("ScenePS")
.IncludeDirectory("Shaders/Includes")
.HotReload();
```
Engine-owned features:
- include expansion
- compile diagnostics
- previous-good shader retention
- pipeline rebuild in the background
- root binding validation
- standard include library
Client-owned:
- shader source files
- shader pass declarations
- object-specific SDFs and materials
### Passes
Passes should declare resource use explicitly:
```csharp
app.Graph.Pass("bloom-prefilter")
.Fullscreen(sceneShader)
.Read("scene")
.Write("bloom-0")
.BindFrame()
.When(features => features.BloomEnabled);
```
Initial pass types:
- fullscreen graphics pass
- proxy rectangle graphics pass
- instanced graphics pass
- compute pass
- copy/blit pass
- present pass
- overlay pass
Initial graph compiler:
- validate resource existence
- validate acyclic dependencies
- topologically sort by declared reads/writes
- emit D3D12 transitions
- allocate transient frame descriptors
- rebuild on graph, shader, target, or window changes
Later graph compiler:
- transient target aliasing
- pass culling
- async compute
- graph visualization
- queue scheduling
## It-Just-Works Feature Layer
The public API should include feature builders over the low-level graph. They
generate normal graph declarations, not secret renderer code.
### Presentation
```csharp
app.Features.Presentation("default")
.From(scene.Color)
.Exposure(settings.SceneExposure)
.Bloom(settings.Bloom)
.Tonemap(Tonemap.AcesFitted)
.Present();
```
### Bloom
```csharp
app.Features.Bloom(scene.Color)
.Levels(3)
.FireflySafe()
.PreTonemap();
```
### IBL
```csharp
app.Features.ImageBasedLighting("studio")
.Pmrem("Textures/studio2_pmrem.dds")
.Irradiance("Textures/studio2_irradiance.dds")
.BindAs("StudioIbl");
```
### SDF Bodies
```csharp
app.Features.SdfBodies("agents")
.Buffer<AgentVisualGpu>("AgentVisuals", maxCount: 64)
.SharedInclude("Shaders/D3D12SdfProxy.hlsli")
.Role("Self", "Shaders/SelfBody.hlsl")
.Role("Imagination", "Shaders/ImaginationBody.hlsl")
.DepthAgainst(scene.Depth)
.Write(scene.Color, scene.Metadata, scene.Control);
```
The engine supplies proxy drawing, bounds, depth writes, common lighting, and
debug counters. The client supplies the body list and shaders.
### Debug
The debug layer should read graph metadata:
- pass list and timing
- target dimensions, formats, and history state
- shader compile status
- descriptor arena usage
- upload ring usage
- selected resource visualizers
- graph validation warnings
Debug views should be registered, not hardcoded:
```csharp
app.Debug.Views
.Add("Final", scene.Color)
.Add("Bloom", bloom.Output)
.Add("Agent Steps", scene.Metadata.Channel("steps"));
```
## Migration Plan
### Phase 0: Freeze The Boundary
Goal: prevent new leaks while the old renderer still exists.
Tasks:
- Keep `docs/engine-client-boundary.md` authoritative.
- Add an architectural test or script check that `src/Aquarium.Engine` contains
no `Aquarium.Epiphany` references.
- Add a second check that engine shader names do not include Epiphany role names
once shaders move.
- Mark current fixed renderer path as legacy in docs, not as permanent API.
Exit criteria:
- Boundary checks run in local verification.
- New work has an obvious place to live.
### Phase 1: Rename Contracts Into Abstractions
Goal: turn `Aquarium.Engine.Contracts` into an actual engine API surface.
Tasks:
- Introduce `AquariumAppBuilder`, `AquariumClient`, graph handles, resource
descriptors, camera descriptors, shader descriptors, pass descriptors, and
feature descriptors.
- Keep old `IAquariumRuntime` beside the new API temporarily.
- Add XML docs or README examples for the common path.
- Keep all contracts Vortice-free.
Exit criteria:
- A tiny sample client can declare one target, one fullscreen shader, and a
presentation pass without referencing `Aquarium.Engine.Render` or Vortice.
### Phase 2: Build The Minimal Graph Compiler Inside Engine
Goal: execute client-declared resources and passes without replacing the whole
renderer at once.
Tasks:
- Add internal `RenderGraph`, `RenderGraphCompiler`, `CompiledRenderGraph`, and
`RenderGraphExecutor`.
- Map public descriptors to internal D3D12 resources.
- Support fullscreen graphics passes first.
- Support named render target reads and writes.
- Support one depth target.
- Generate transitions from declared pass use.
- Keep shader hot reload and previous-good pipeline behavior.
- Add graph validation errors with pass/resource names.
Exit criteria:
- Existing scene, bloom, and present path can be represented as graph passes,
even if some callbacks still call existing private methods.
### Phase 3: Convert Presentation And Bloom To Features
Goal: move generic post-processing out of bespoke renderer flow.
Tasks:
- Express HDR scene target, bloom pyramid, history targets, and present pass as
graph resources.
- Implement `app.Features.Bloom`.
- Implement `app.Features.Presentation`.
- Move render debug mode registration into graph/debug descriptors.
- Keep DirectWrite overlay as an engine feature.
Exit criteria:
- A non-Epiphany sample can render a color target, bloom it, tonemap it, and
present without defining Grid, agents, cursor, or CultNet.
### Phase 4: Move Epiphany Shaders And Body Policy To Client
Goal: remove role bodies and cursor semantics from engine source.
Tasks:
- Move role shader files and `D3D12AgentCharacters.hlsli` into
`src/Aquarium.Epiphany/Shaders`.
- Keep generic includes in engine:
- fullscreen helpers
- SDF primitives
- PBR lighting
- body proxy mechanics
- packing and frame constants
- Replace fixed `RoleAgentCount`, fixed shader path arrays, and fixed
`AgentVisualGpu` ownership with a client-declared SDF body feature.
- Move `BuildAgentVisualTable`, `BuildBodyLightTable`, `BuildGridHeightBrushes`,
orbit fallback positions, cursor radius, and Self constants into
`Aquarium.Epiphany`.
- Let Epiphany upload buffers by handle.
Exit criteria:
- `src/Aquarium.Engine` contains no role names, no cursor hibiscus, no Self
constants, no agent orbit fallback, and no Epiphany shader paths.
### Phase 5: Move Grid Policy To Epiphany Or A Reusable Feature
Goal: decide whether Grid is a generic Aquarium feature or Epiphany client
policy.
Recommended split:
- Engine feature: height-field target, brush rendering primitive, reflective
surface shader helper, debug visualization.
- Epiphany policy: Grid center, radius rule, Self gravity bowl, role gravity
wells, radial wave parameters, body placement rule.
Tasks:
- Expose `app.Features.HeightField`.
- Expose CPU helper for evaluating brush stacks when the client asks for it.
- Move current brush stack construction into Epiphany.
- Move mouse-to-Grid projection out of `AquariumHost`.
Exit criteria:
- A different client can choose no Grid, a different height field, or a static
render target without touching engine code.
### Phase 6: Replace Frame Contract
Goal: stop `AquariumFrame` from being the rendering protocol.
Tasks:
- Introduce `AquariumFrameContext` for time, window size, input, graph resource
access, frame constants, and upload helpers.
- Let clients define their own frame constants structs.
- Keep engine standard constants optional:
- resolution
- time
- camera matrices
- previous camera matrices
- presentation settings
- Provide `Frame.UploadConstant<T>`, `Frame.UploadBuffer<T>`, and
`Frame.SetGraphValue`.
- Remove renderer dependence on `AquariumFrame`.
Exit criteria:
- `IAquariumRenderer.Render` consumes a compiled graph frame, not an Epiphany
shaped frame record.
### Phase 7: Split Host From Backend When Earned
Goal: make Aquarium usable as an engine package, not only an executable.
Tasks:
- Split public abstractions from D3D12 backend if the contract has stabilized.
- Keep Win32 host as the default app host.
- Let clients choose hosted mode:
- engine-owned window
- headless
- later embedded/native handle mode
- Package shader include library and default features as engine assets.
Exit criteria:
- A fresh client can reference Aquarium abstractions, implement a client class,
run through the host, and declare render graph resources without copying
Epiphany code.
## Order Of Attack
Do not start by moving every file. That produces thrilling amounts of rubble.
Recommended first implementation slice:
1. Add public render target, shader, pass, camera, and feature descriptor types
to contracts.
2. Add a graph builder that can describe the current fixed pipeline without
executing it.
3. Add a debug dump of the declared graph.
4. Convert bloom and presentation to execute from graph declarations.
5. Convert body proxy draws to a client-declared body registry.
6. Move Epiphany shaders out of engine.
7. Move Grid and agent/cursor frame construction out of renderer.
Each slice must preserve:
- shader reload keeps last good pipelines
- failed client reload keeps previous client alive
- resize rebuilds all size-bound targets
- debug overlay remains readable
- headless smoke still completes
- engine source stays free of Epiphany references
## Verification Gates
Every migration pass should run:
```powershell
dotnet build Aquarium.Engine.sln --no-restore
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4
.\scripts\verify-boundaries.ps1
```
The final `rg` should shrink over time. At the end it should be empty except
for generic documentation comments if any survive, and they probably should not.
## Non-Goals For The First Graph
- no async compute
- no transient memory aliasing
- no general material system
- no visual node editor
- no automatic shader reflection dependency on day one
- no project split until the API stops moving under our feet
- no "compatibility mode" escape hatch that lets client code call D3D12 directly
The first graph exists to establish ownership and data flow. Optimization comes
after the graph tells the truth.
## Success Definition
Aquarium is clean when:
- Engine code can render a non-Epiphany sample client.
- Epiphany can add a render target, camera, shader, buffer, and pass without
editing `D3D12Renderer`.
- Epiphany shaders live with Epiphany.
- Engine shader includes are generic.
- Resource lifetime, descriptors, barriers, resize, hot reload, presentation,
and debug inspection are engine-owned.
- Client code describes intent. Engine code makes it work.

View file

@ -3,7 +3,7 @@ param(
[switch]$NoStop,
[switch]$BuildOnly,
[switch]$Reopen,
[string]$ClientProject = "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj",
[string]$ClientProject = "src\Aquarium.Fensalir\Aquarium.Fensalir.csproj",
[int]$RetainSlots = 12,
[int]$StartupTimeoutSeconds = 5,
[int]$HeadlessTimeoutSeconds = 60

View file

@ -3,7 +3,7 @@ param(
[switch]$NoInitialLaunch,
[switch]$ReopenWhenClosed,
[switch]$Once,
[string]$ClientProject = "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj",
[string]$ClientProject = "src\Aquarium.Fensalir\Aquarium.Fensalir.csproj",
[int]$IntervalMilliseconds = 1000,
[int]$DebounceMilliseconds = 350,
[int]$RetainSlots = 12,
@ -46,7 +46,7 @@ function Get-SourceFiles {
$roots = @(
(Join-Path $repoRoot "src"),
(Join-Path $repoRoot "scripts"),
(Join-Path $repoRoot "Aquarium.Engine.sln"),
(Join-Path $repoRoot "Fensalir.sln"),
(Join-Path $repoRoot "global.json")
)

View file

@ -1,100 +0,0 @@
param(
[switch]$Zip,
[switch]$NoSelfContained,
[switch]$Clean,
[string]$Configuration = "Release",
[string]$Runtime = "win-x64",
[string]$OutputRoot
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$projectPath = Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj"
$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj"
$publishRoot = if ([string]::IsNullOrWhiteSpace($OutputRoot)) {
Join-Path $repoRoot "artifacts\publish"
}
else {
$OutputRoot
}
$publishPath = Join-Path $publishRoot $Runtime
$zipPath = Join-Path $publishRoot "EpiphanyAquariumEngine-$Runtime.zip"
if ($Clean -and (Test-Path $publishPath)) {
Remove-Item -LiteralPath $publishPath -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $publishPath | Out-Null
$selfContained = if ($NoSelfContained) { "false" } else { "true" }
$publishArguments = @(
"publish",
$projectPath,
"-c", $Configuration,
"-r", $Runtime,
"--self-contained", $selfContained,
"-o", $publishPath,
"/p:UseAppHost=true"
)
Write-Host "Publishing Aquarium Engine:"
Write-Host " Project: $projectPath"
Write-Host " Client: $liveProjectPath"
Write-Host " Runtime: $Runtime"
Write-Host " Output: $publishPath"
dotnet @publishArguments
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed with exit code $LASTEXITCODE."
}
dotnet publish $liveProjectPath -c $Configuration -r $Runtime --self-contained false -o $publishPath
if ($LASTEXITCODE -ne 0) {
throw "client runtime publish failed with exit code $LASTEXITCODE."
}
& (Join-Path $PSScriptRoot "sync-faust-toolchain.ps1") -DestinationRoot (Join-Path $publishPath "Tools\Faust")
$requiredFiles = @(
"Aquarium.Engine.exe",
"Aquarium.Epiphany.dll",
"Aquarium.Engine.Contracts.dll",
"AquaSynth.Core.dll",
"AquaSynth.Faust.dll",
"Tools\Faust\lib\faust.dll",
"Tools\Faust\share\faust\stdfaust.lib",
"Assets\Fensalir-Icon.ico",
"Assets\Fensalir-Icon-96.png",
"Assets\Fensalir-Splash.bmp",
"Assets\Fensalir-Splash.png",
"Assets\Fonts\Montserrat[wght].ttf",
"Assets\Fonts\UbuntuSans[wdth,wght].ttf",
"Render\Shaders\D3D12HeightField.hlsl",
"Render\Shaders\D3D12SdfCommon.hlsli",
"Render\Shaders\D3D12SdfProxy.hlsli",
"Render\Shaders\D3D12SelfBody.hlsl",
"Render\Shaders\D3D12CursorBody.hlsl"
)
foreach ($relativePath in $requiredFiles) {
$path = Join-Path $publishPath $relativePath
if (-not (Test-Path -LiteralPath $path)) {
throw "Publish output is missing required file: $relativePath"
}
}
Write-Host "Publish output verified."
if ($Zip) {
if (Test-Path $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
Compress-Archive -Path (Join-Path $publishPath "*") -DestinationPath $zipPath -CompressionLevel Optimal
Write-Host "Zip ready:"
Write-Host " $zipPath"
}
Write-Host "Shipping build ready:"
Write-Host " $publishPath"

View file

@ -1,48 +0,0 @@
param(
[string]$Agent = "Soul",
[string]$OutputDirectory,
[int]$Width = 768,
[int]$Height = 768,
[int]$FramesPerView = 18,
[float]$TimeSeconds = 18.0
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$projectPath = Join-Path $repoRoot "src\Aquarium.Epiphany.AgentPreview\Aquarium.Epiphany.AgentPreview.csproj"
$previewRoot = Join-Path $repoRoot "artifacts\agent-preview"
$slotPath = Join-Path $previewRoot ("slot-" + (Get-Date -Format "yyyyMMdd-HHmmss") + "-" + [guid]::NewGuid().ToString("N").Substring(0, 8))
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
$OutputDirectory = Join-Path $repoRoot ("artifacts\agent-previews\$Agent\" + (Get-Date -Format "yyyyMMdd-HHmmss"))
}
New-Item -ItemType Directory -Force -Path $slotPath | Out-Null
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
Write-Host "Building isolated preview renderer:"
Write-Host " $slotPath"
dotnet build $projectPath -c Debug -o $slotPath /p:UseAppHost=true
if ($LASTEXITCODE -ne 0) {
throw "preview build failed with exit code $LASTEXITCODE."
}
$exePath = Join-Path $slotPath "Aquarium.Epiphany.AgentPreview.exe"
if (-not (Test-Path $exePath)) {
throw "Expected preview executable was not produced: $exePath"
}
& $exePath `
--agent $Agent `
--output $OutputDirectory `
--width $Width `
--height $Height `
--frames $FramesPerView `
--time $TimeSeconds
if ($LASTEXITCODE -ne 0) {
throw "preview render failed with exit code $LASTEXITCODE."
}
Write-Host "Preview frames written:"
Write-Host " $OutputDirectory"

View file

@ -10,10 +10,10 @@ $engineRoots = @(
$forbiddenEngineTerms = "grid|agent|body|epiphany"
$engineReferences = rg -n -i -- $forbiddenEngineTerms $engineRoots
if ($LASTEXITCODE -eq 0) {
throw "Aquarium Engine and contracts must not contain Epiphany/client policy terms:`n$engineReferences"
throw "Fensalir engine and contracts must not contain Epiphany/client policy terms:`n$engineReferences"
}
elseif ($LASTEXITCODE -ne 1) {
throw "Boundary search for Aquarium Engine terms failed."
throw "Boundary search for Fensalir engine terms failed."
}
Write-Host "Aquarium boundary checks passed."
Write-Host "Fensalir boundary checks passed."

View file

@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine\Aquarium.Engine.csproj" />
<ProjectReference Include="..\Aquarium.Epiphany\Aquarium.Epiphany.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Shaders\**\*.hlsl" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View file

@ -1,222 +0,0 @@
using System.Numerics;
using System.Text.Json;
using Aquarium.Engine;
using Aquarium.Engine.Input;
using Aquarium.Engine.Platform;
using Aquarium.Engine.Render;
using Aquarium.Epiphany;
var options = PreviewOptions.Parse(args);
var subject = PreviewSubject.Resolve(options.Subject);
var outputDirectory = Path.GetFullPath(options.OutputDirectory);
Directory.CreateDirectory(outputDirectory);
var shaderPath = Path.Combine(AppContext.BaseDirectory, "Render", "Shaders", "D3D12HeightField.hlsl");
if (!File.Exists(shaderPath))
{
throw new FileNotFoundException("Preview render requires copied Aquarium shader assets.", shaderPath);
}
var input = new InputState();
using var window = Win32Window.Create(
$"Aquarium {subject.DisplayName} Preview",
options.Width,
options.Height,
input,
visible: false);
using var renderer = new D3D12Renderer(
window.Handle,
options.Width,
options.Height,
shaderPath,
CreatePreviewRenderPlan(),
new GraphicsSettings(0, options.Exposure, options.BloomIntensity, options.BloomVeilIntensity));
renderer.DebugUiVisible = false;
var rendered = new List<RenderedView>();
foreach (var view in PreviewView.CreateSet(subject.Radius))
{
RenderSettledView(renderer, window, subject, view, options);
var path = Path.Combine(outputDirectory, $"{subject.Key}-{view.Key}.png");
renderer.SaveFramePng(path);
rendered.Add(new RenderedView(view.Key, path));
Console.WriteLine(path);
}
var manifestPath = Path.Combine(outputDirectory, $"{subject.Key}-manifest.json");
File.WriteAllText(
manifestPath,
JsonSerializer.Serialize(
new PreviewManifest(subject.Key, subject.DisplayName, options.Width, options.Height, options.FramesPerView, rendered),
new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine(manifestPath);
static void RenderSettledView(
D3D12Renderer renderer,
Win32Window window,
PreviewSubject subject,
PreviewView view,
PreviewOptions options)
{
for (var frameIndex = 0; frameIndex < options.FramesPerView || !renderer.HasPresentedReadyFrame; frameIndex++)
{
if (frameIndex > options.FramesPerView + options.PipelineWarmupFrameLimit)
{
throw new TimeoutException("Renderer did not produce a ready preview frame.");
}
window.PumpMessages();
var frame = CreateFrame(subject, view, options.TimeSeconds + frameIndex / 60.0f);
renderer.Render(frame, options.Width, options.Height);
if (!renderer.HasPresentedReadyFrame)
{
Thread.Sleep(16);
}
}
}
static AquariumFrame CreateFrame(PreviewSubject subject, PreviewView view, float timeSeconds)
{
var objects = Enumerable.Range(0, EpiphanyRenderPlan.SdfVisualCount)
.Select(index => InactiveObject(index))
.ToArray();
var selected = new AquariumSdfObject(
new Vector4(Vector3.Zero, subject.Radius),
new Vector4(Vector3.Zero, 0.0f),
subject.State);
objects[subject.SdfIndex] = selected;
var lights = new[]
{
new AquariumSdfLight(
new Vector4(1.8f, -2.6f, 2.4f, 1.0f),
new Vector4(5.8f, 5.1f, 4.2f, 0.0f)),
new AquariumSdfLight(
new Vector4(-2.4f, 1.2f, 1.4f, 1.0f),
new Vector4(1.1f, 1.5f, 2.1f, 0.0f)),
};
var scene = new AquariumSceneState
{
SdfObjects = objects,
SdfLights = lights,
};
return new AquariumFrame(new ViewFrame(Vector2.Zero, 20.0f), view.CameraPosition, timeSeconds, Vector2.Zero, scene);
}
static AquariumRenderPlan CreatePreviewRenderPlan()
{
var plan = EpiphanyRenderPlan.Create();
plan.Shaders.Scene("D3D12AgentPreviewScene.hlsl");
return plan;
}
static AquariumSdfObject InactiveObject(int index)
{
var center = new Vector3(10000.0f + index * 13.0f, 10000.0f, 10000.0f);
return new AquariumSdfObject(new Vector4(center, 0.001f), new Vector4(center, 0.0f), Vector4.Zero);
}
sealed record PreviewOptions(
string Subject,
string OutputDirectory,
int Width,
int Height,
int FramesPerView,
int PipelineWarmupFrameLimit,
float TimeSeconds,
float Exposure,
float BloomIntensity,
float BloomVeilIntensity)
{
public static PreviewOptions Parse(string[] args)
{
var subject = ValueAfter(args, "--agent") ?? ValueAfter(args, "--subject") ?? FirstPositional(args) ?? "Soul";
var output = ValueAfter(args, "--output") ?? Path.Combine("artifacts", "agent-previews", subject, DateTime.Now.ToString("yyyyMMdd-HHmmss"));
return new PreviewOptions(
subject,
output,
IntValueAfter(args, "--width", 768),
IntValueAfter(args, "--height", 768),
IntValueAfter(args, "--frames", 18),
IntValueAfter(args, "--warmup-limit", 3600),
FloatValueAfter(args, "--time", 18.0f),
FloatValueAfter(args, "--exposure", 0.24f),
FloatValueAfter(args, "--bloom", 0.12f),
FloatValueAfter(args, "--veil", 0.032f));
}
private static string? FirstPositional(IReadOnlyList<string> args)
{
return args.FirstOrDefault(arg => !arg.StartsWith("--", StringComparison.Ordinal));
}
private static string? ValueAfter(IReadOnlyList<string> args, string name)
{
for (var index = 0; index < args.Count - 1; index++)
{
if (string.Equals(args[index], name, StringComparison.OrdinalIgnoreCase))
{
return args[index + 1];
}
}
return null;
}
private static int IntValueAfter(IReadOnlyList<string> args, string name, int fallback)
{
return int.TryParse(ValueAfter(args, name), out var value) ? Math.Max(1, value) : fallback;
}
private static float FloatValueAfter(IReadOnlyList<string> args, string name, float fallback)
{
return float.TryParse(ValueAfter(args, name), out var value) ? value : fallback;
}
}
sealed record PreviewSubject(string Key, string DisplayName, int SdfIndex, float Radius, Vector4 State)
{
private static readonly PreviewSubject[] All =
[
new("self", "Self", EpiphanyRenderPlan.SelfVisualIndex, 1.12f, new Vector4(1.0f, 1.0f, 0.0f, 0.0f)),
new("face", "Face", EpiphanyRenderPlan.RoleAgentBaseIndex, 0.58f, new Vector4(0.8f, 0.45f, 0.35f, 0.0f)),
new("imagination", "Imagination", EpiphanyRenderPlan.RoleAgentBaseIndex + 1, 0.70f, new Vector4(0.95f, 0.62f, 0.28f, 0.0f)),
new("eyes", "Eyes", EpiphanyRenderPlan.RoleAgentBaseIndex + 2, 0.58f, new Vector4(0.72f, 0.58f, 0.24f, 0.0f)),
new("body", "Body", EpiphanyRenderPlan.RoleAgentBaseIndex + 3, 0.64f, new Vector4(0.62f, 0.54f, 0.42f, 0.0f)),
new("hands", "Hands", EpiphanyRenderPlan.RoleAgentBaseIndex + 4, 0.62f, new Vector4(0.88f, 0.64f, 0.36f, 0.0f)),
new("soul", "Soul", EpiphanyRenderPlan.RoleAgentBaseIndex + 5, 0.92f, new Vector4(0.9f, 0.5f, 0.42f, 0.0f)),
new("life", "Life", EpiphanyRenderPlan.RoleAgentBaseIndex + 6, 0.66f, new Vector4(0.66f, 0.68f, 0.22f, 0.0f)),
new("cursor", "Cursor", EpiphanyRenderPlan.CursorVisualIndex, 0.88f, new Vector4(1.0f, 0.5f, 0.0f, 0.0f)),
];
public static PreviewSubject Resolve(string value)
{
return All.FirstOrDefault(subject =>
string.Equals(subject.Key, value, StringComparison.OrdinalIgnoreCase)
|| string.Equals(subject.DisplayName, value, StringComparison.OrdinalIgnoreCase))
?? throw new ArgumentException($"Unknown preview subject '{value}'. Known values: {string.Join(", ", All.Select(subject => subject.Key))}.");
}
}
sealed record PreviewView(string Key, Vector3 CameraPosition)
{
public static IReadOnlyList<PreviewView> CreateSet(float radius)
{
var distance = MathF.Max(radius * 2.35f, 1.75f);
return
[
new("front", new Vector3(0.0f, -distance, distance * 0.36f)),
new("front-high", new Vector3(0.0f, -distance * 0.82f, distance * 0.86f)),
new("mouth-oblique", new Vector3(-distance * 0.48f, -distance * 0.92f, distance * 0.34f)),
new("right", new Vector3(distance, 0.0f, distance * 0.36f)),
new("right-low", new Vector3(distance, 0.0f, distance * 0.14f)),
new("left", new Vector3(-distance, 0.0f, distance * 0.36f)),
new("three-quarter", new Vector3(distance * 0.72f, -distance * 0.72f, distance * 0.52f)),
new("top-oblique", new Vector3(distance * 0.22f, -distance * 0.32f, distance * 1.16f)),
];
}
}
sealed record RenderedView(string View, string Path);
sealed record PreviewManifest(string Subject, string DisplayName, int Width, int Height, int FramesPerView, IReadOnlyList<RenderedView> Views);

View file

@ -1,54 +0,0 @@
cbuffer AquariumFrame : register(b0)
{
float2 resolution;
float timeSeconds;
float viewRadius;
float3 cameraPosition;
float farDistance;
float2 viewCenter;
float frameIndex;
float previousTimeSeconds;
float3 previousCameraPosition;
float previousViewRadius;
float2 previousViewCenter;
float2 jitterPixels;
float2 previousJitterPixels;
float renderDebugMode;
float exposure;
float bloomIntensity;
float bloomVeilIntensity;
float4 cursorWorlds;
};
struct VertexOut
{
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
struct SceneOut
{
float4 colorTravel : SV_Target0;
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float depth : SV_Depth;
};
VertexOut FullscreenTriangleVS(uint vertexId : SV_VertexID)
{
float2 uv = float2((vertexId << 1) & 2, vertexId & 2);
VertexOut output;
output.position = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
output.uv = uv;
return output;
}
SceneOut D3D12ScenePS(VertexOut input)
{
SceneOut output;
output.colorTravel = float4(0.0, 0.0, 0.0, farDistance + 1.0);
output.metadata = 0.0;
output.control = 0.0;
output.depth = 1.0;
return output;
}

View file

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>Aquarium.Epiphany</AssemblyName>
<RootNamespace>Aquarium.Epiphany</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj" />
</ItemGroup>
<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)" />
</ItemGroup>
</Project>

View file

@ -1,355 +0,0 @@
using System.Numerics;
using System.Globalization;
using Aquarium.Engine;
using Aquarium.Engine.Audio;
using Aquarium.Engine.Input;
using Aquarium.Engine.Render;
using Aquarium.Engine.Ui;
using Aquarium.Epiphany.State;
using Aquarium.Epiphany.Voice;
namespace Aquarium.Epiphany;
public sealed class AquariumRuntime : IAquariumRuntime
{
private const float StateSaveIntervalSeconds = 0.20f;
private readonly OrbitCameraRig cameraRig;
private readonly AquariumCultStateStore stateStore;
private readonly FaceVoiceRouter faceVoice;
private ViewFrame ViewFrame;
private GraphicsSettings graphicsSettings;
private float timeSeconds;
private float previousFrameTimeSeconds;
private Vector2 previousCursorWorld;
private float secondsSinceStateSave;
private bool graphicsSettingsDirty;
private bool timePaused;
private string faceVoiceText = "";
public AquariumRuntime(AquariumRuntimeOptions options)
{
Options = options;
stateStore = AquariumCultStateStore.Open(options.CultCachePath);
var liveState = stateStore.LoadOrCreate();
graphicsSettings = stateStore.LoadOrCreateGraphicsSettings().ToSettings();
if (options.RenderDebugModeOverride.HasValue)
{
GraphicsSettings = graphicsSettings with { RenderDebugMode = options.RenderDebugModeOverride.Value };
}
cameraRig = new OrbitCameraRig(
target: new Vector2(liveState.CameraTargetX, liveState.CameraTargetY),
yawRadians: liveState.CameraYawRadians,
pitchRadians: liveState.CameraPitchRadians,
distance: liveState.CameraDistance);
timeSeconds = liveState.TimeSeconds;
ViewFrame = ViewFrame.FromCamera(cameraRig.Target, cameraRig.Distance);
previousFrameTimeSeconds = timeSeconds;
previousCursorWorld = ViewFrame.Center;
faceVoice = new FaceVoiceRouter(Audio, liveState);
Ui = new AquariumUiDocument()
.Panel("Epiphany", 396.0f, 82.0f, 360.0f, panel => panel
.Section("Runtime")
.Toggle("Pause Time", () => timePaused, value => timePaused = value, "Stops Epiphany simulation time while leaving the renderer alive.")
.Slider("Time", () => timeSeconds, value => timeSeconds = MathF.Max(0.0f, value), 0.0f, 720.0f, "0.0", "Scrubs Epiphany simulation time.")
.Button("Flush State", FlushState, "Writes current Epiphany runtime state to CultCache.")
.Section("Face Voice")
.Text("App Server", () => faceVoice.AppServerUri, value => faceVoice.AppServerUri = value.Trim(), "Loopback Codex app-server websocket, e.g. ws://127.0.0.1:8765.")
.Toggle("Auto Route", () => faceVoice.AutoSelect, value => faceVoice.AutoSelect = value, "Routes speech to the nearest enabled Face endpoint by cursor/listener proximity.")
.Slider("Audible Radius", () => faceVoice.AudibleRadius, value => faceVoice.AudibleRadius = value, 1.0f, 64.0f, "0.0", "Distance scale for Face voice gain and stereo pan.")
.Readout("Active", () => faceVoice.ActiveSummary, "Currently selected Face endpoint and listener distance.")
.Readout("Routes", () => faceVoice.RouteSummary, "Distances from the listener cursor to each Face endpoint.")
.Text("Name", () => faceVoice.ActiveEndpoint.DisplayName, value => faceVoice.ActiveEndpoint.DisplayName = value.Trim(), "Display name for the active Face endpoint.")
.Toggle("Enabled", () => faceVoice.ActiveEndpoint.Enabled, value => faceVoice.ActiveEndpoint.Enabled = value, "Allows this Face endpoint to speak and participate in auto routing.")
.Text("Thread", () => faceVoice.ActiveEndpoint.ThreadId, value => faceVoice.ActiveEndpoint.ThreadId = value.Trim(), "Thread id that owns the active realtime Face session.")
.Text("Voice", () => faceVoice.ActiveEndpoint.Voice, value => faceVoice.ActiveEndpoint.Voice = value.Trim(), "Realtime voice name, such as marin, cedar, or cove.")
.Slider("Anchor X", () => faceVoice.ActiveEndpoint.Anchor.X, SetActiveFaceAnchorX, -32.0f, 32.0f, "0.0", "World X anchor for proximity routing.")
.Slider("Anchor Y", () => faceVoice.ActiveEndpoint.Anchor.Y, SetActiveFaceAnchorY, -32.0f, 32.0f, "0.0", "World Y anchor for proximity routing.")
.Readout("Status", () => faceVoice.ActiveStatus, "Realtime transport status for the active Face.")
.TextBox("Prompt", () => faceVoice.ActiveEndpoint.Prompt, value => faceVoice.ActiveEndpoint.Prompt = value, lines: 3, acceptsReturn: true, monospace: false, tooltip: "Backend realtime instructions for the active Face. This is speech behavior, not memory authority.")
.TextBox("Say", () => faceVoiceText, value => faceVoiceText = value, lines: 2, acceptsReturn: false, submit: SendFaceVoiceText, monospace: false, tooltip: "Send one text utterance through the active Face route.")
.Button("Start Active", faceVoice.StartActive, "Starts the active thread-scoped realtime session with audio output.")
.Button("Start Enabled", faceVoice.StartAll, "Starts every enabled Face endpoint so rumination loops can speak regardless of user proximity.")
.Button("Stop Active", faceVoice.StopActive, "Stops the active realtime Face voice session.")
.Button("Clear Voice Log", faceVoice.ClearActiveLog, "Clears local transcript and audio counters for the active Face.")
.Readout("Error", () => faceVoice.LastError, "Last realtime transport error for the active Face.")
.TextBox("Transcript", () => faceVoice.Transcript, _ => { }, lines: 7, acceptsReturn: true, monospace: false, tooltip: "Local ephemeral realtime transcript. It is not durable Epiphany memory."))
.Command("pause", args =>
{
timePaused = args.Count == 0 ? !timePaused : bool.TryParse(args[0], out var value) && value;
return $"pause {timePaused}";
}, "Toggles or sets Epiphany simulation pause.")
.Command("time", args =>
{
if (args.Count > 0 && float.TryParse(args[0], out var value))
{
timeSeconds = MathF.Max(0.0f, value);
}
return $"time {timeSeconds:0.###}";
}, "Reads or sets Epiphany simulation time.")
.Command("flush", _ =>
{
FlushState();
return "state flushed";
}, "Flushes Epiphany state to CultCache.")
.Command("facevoice", args =>
{
if (args.Count == 0)
{
return $"Face voice {faceVoice.ActiveSummary}; {faceVoice.ActiveStatus}";
}
var verb = args[0].Trim().ToLowerInvariant();
if (verb == "start")
{
faceVoice.StartActive();
return $"Face voice starting: {faceVoice.ActiveSummary}";
}
if (verb == "stop")
{
faceVoice.StopActive();
return $"Face voice stopping: {faceVoice.ActiveSummary}";
}
if (verb == "startall")
{
faceVoice.StartAll();
return "Enabled Face voice sessions starting";
}
if (verb == "stopall")
{
faceVoice.StopAll();
return "All Face voice sessions stopping";
}
if (verb == "say" && args.Count > 1)
{
SendFaceVoiceText(string.Join(" ", args.Skip(1)));
return $"Face voice text sent to {faceVoice.ActiveEndpoint.Id}";
}
if (verb == "list")
{
return faceVoice.RouteSummary;
}
if (verb == "auto" && args.Count > 1 && bool.TryParse(args[1], out var autoSelect))
{
faceVoice.AutoSelect = autoSelect;
return $"Face voice auto route {faceVoice.AutoSelect}";
}
if (verb == "radius" && args.Count > 1 && float.TryParse(args[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var radius))
{
faceVoice.AudibleRadius = radius;
return $"Face voice audible radius {faceVoice.AudibleRadius:0.0}";
}
if (verb == "select" && args.Count > 1)
{
return faceVoice.Select(args[1])
? $"Face voice selected {faceVoice.ActiveEndpoint.Id}"
: $"No Face voice endpoint named {args[1]}";
}
if (verb == "add" && args.Count >= 5 && TryParsePoint(args[3], args[4], out var anchor))
{
var voice = args.Count > 5 ? args[5] : null;
var endpoint = faceVoice.AddOrUpdate(args[1], args[2], anchor, voice);
return $"Face voice endpoint {endpoint.Id} anchored at {endpoint.Anchor.X:0.0}, {endpoint.Anchor.Y:0.0}";
}
if (verb == "move" && args.Count > 2 && TryParsePoint(args[1], args[2], out var movedAnchor))
{
faceVoice.ActiveEndpoint.Anchor = movedAnchor;
return $"Face voice endpoint {faceVoice.ActiveEndpoint.Id} moved to {movedAnchor.X:0.0}, {movedAnchor.Y:0.0}";
}
if (verb == "movehere")
{
faceVoice.ActiveEndpoint.Anchor = faceVoice.ListenerWorld;
return $"Face voice endpoint {faceVoice.ActiveEndpoint.Id} moved to listener";
}
if (verb == "remove" && args.Count > 1)
{
return faceVoice.Remove(args[1])
? $"Face voice endpoint {args[1]} removed"
: $"Face voice endpoint {args[1]} was not removed";
}
return "facevoice start | startall | stop | stopall | say <text> | list | auto <true|false> | radius <meters> | select <id> | add <id> <thread> <x> <y> [voice] | move <x> <y> | movehere | remove <id>";
}, "Controls the Face realtime voice session.");
}
public AquariumRuntimeOptions Options { get; }
public AquariumRenderPlan RenderPlan { get; } = EpiphanyRenderPlan.Create();
public AquariumUiDocument Ui { get; }
public AquariumAudioDocument Audio { get; } = new();
public AquariumSynthDocument Synth { get; } = AquariumSynthDocument.Empty;
public AquariumFrame Frame => new(ViewFrame, cameraRig.Position, timeSeconds);
public GraphicsSettings GraphicsSettings
{
get => graphicsSettings;
set
{
var normalized = value.Normalized();
if (normalized == graphicsSettings)
{
return;
}
graphicsSettings = normalized;
graphicsSettingsDirty = true;
}
}
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input)
{
var frameWithCursor = frame with
{
CursorWorld = EpiphanySceneBuilder.ProjectMouseToGridPlane(
input.MousePosition,
input.Width,
input.Height,
frame.CameraPosition,
frame.View.Center),
};
faceVoice.UpdateListener(frameWithCursor.CursorWorld);
var scene = EpiphanySceneBuilder.Build(frameWithCursor, previousFrameTimeSeconds, previousCursorWorld);
previousFrameTimeSeconds = frameWithCursor.TimeSeconds;
previousCursorWorld = frameWithCursor.CursorWorld;
return frameWithCursor with { Scene = scene };
}
public void Start()
{
Console.WriteLine("Aquarium Engine booted.");
Console.WriteLine($"Grid center: {ViewFrame.Center}, radius: {ViewFrame.Radius:0.00}");
Console.WriteLine($"Graphics settings: debug {graphicsSettings.RenderDebugMode}, exposure {graphicsSettings.SceneExposure:0.###}, bloom {graphicsSettings.BloomIntensity:0.###}, veil {graphicsSettings.BloomVeilIntensity:0.###}");
Console.WriteLine($"CultCache: {stateStore.CachePath}");
Console.WriteLine($"CultNet hello: {stateStore.Hello.RuntimeKind} / {stateStore.Hello.DisplayName}");
Console.WriteLine("Aquarium host renderer owns the visible frame invariants.");
}
public void Update(float deltaSeconds, InputState input)
{
if (!timePaused)
{
timeSeconds += Math.Max(deltaSeconds, 0.0f);
}
cameraRig.ApplyInput(input, deltaSeconds);
ViewFrame = ViewFrame.FromCamera(cameraRig.Target, cameraRig.Distance);
secondsSinceStateSave += Math.Max(deltaSeconds, 0.0f);
if (secondsSinceStateSave >= StateSaveIntervalSeconds)
{
FlushState();
secondsSinceStateSave = 0.0f;
}
}
public void FlushState()
{
SaveLiveState();
SaveGraphicsSettingsIfDirty();
}
public void Dispose()
{
FlushState();
faceVoice.Dispose();
stateStore.Dispose();
}
private void SaveLiveState()
{
stateStore.Save(new AquariumLiveState
{
TimeSeconds = timeSeconds,
CameraTargetX = cameraRig.Target.X,
CameraTargetY = cameraRig.Target.Y,
CameraYawRadians = cameraRig.YawRadians,
CameraPitchRadians = cameraRig.PitchRadians,
CameraDistance = cameraRig.Distance,
FaceVoiceAppServerUri = faceVoice.AppServerUri,
FaceVoiceThreadId = faceVoice.ActiveEndpoint.ThreadId,
FaceVoiceVoice = faceVoice.ActiveEndpoint.Voice,
FaceVoicePrompt = faceVoice.ActiveEndpoint.Prompt,
FaceVoiceEndpoints = faceVoice.ExportState(),
FaceVoiceAutoSelect = faceVoice.AutoSelect,
FaceVoiceAudibleRadius = faceVoice.AudibleRadius
});
}
private void SendFaceVoiceText()
{
SendFaceVoiceText(faceVoiceText);
faceVoiceText = "";
}
private void SendFaceVoiceText(string text)
{
var trimmed = text.Trim();
if (trimmed.Length == 0)
{
return;
}
faceVoice.SendText(trimmed);
}
private void SetActiveFaceAnchorX(float value)
{
var anchor = faceVoice.ActiveEndpoint.Anchor;
faceVoice.ActiveEndpoint.Anchor = new Vector2(value, anchor.Y);
}
private void SetActiveFaceAnchorY(float value)
{
var anchor = faceVoice.ActiveEndpoint.Anchor;
faceVoice.ActiveEndpoint.Anchor = new Vector2(anchor.X, value);
}
private static bool TryParsePoint(string x, string y, out Vector2 point)
{
if (float.TryParse(x, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedX)
&& float.TryParse(y, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedY))
{
point = new Vector2(parsedX, parsedY);
return true;
}
point = default;
return false;
}
private void SaveGraphicsSettingsIfDirty()
{
if (!graphicsSettingsDirty)
{
return;
}
stateStore.SaveGraphicsSettings(AquariumGraphicsSettingsState.FromSettings(graphicsSettings));
graphicsSettingsDirty = false;
}
}
public sealed class AquariumRuntimeFactory : IAquariumRuntimeFactory
{
public IAquariumRuntime Create(AquariumRuntimeOptions options)
{
return new AquariumRuntime(options);
}
}

View file

@ -1,44 +0,0 @@
using Aquarium.Engine.Render;
namespace Aquarium.Epiphany;
public static class EpiphanyRenderPlan
{
public const int RoleAgentCount = 7;
public const int SdfVisualCount = RoleAgentCount + 2;
public const int SelfVisualIndex = 0;
public const int RoleAgentBaseIndex = 1;
public const int CursorVisualIndex = SdfVisualCount - 1;
public static AquariumRenderPlan Create()
{
var app = new AquariumApp();
app.Shaders
.Core("D3D12HeightField.hlsl", "D3D12Scene.hlsl", "D3D12Post.hlsl")
.SdfLibrary("D3D12AgentCharacters.hlsli")
.SdfShader("D3D12SelfBody.hlsl")
.SdfShader("D3D12FaceAgent.hlsl")
.SdfShader("D3D12ImaginationAgent.hlsl")
.SdfShader("D3D12EyesAgent.hlsl")
.SdfShader("D3D12BodyAgent.hlsl")
.SdfShader("D3D12HandsAgent.hlsl")
.SdfShader("D3D12SoulAgent.hlsl")
.SdfShader("D3D12LifeAgent.hlsl")
.SdfShader("D3D12CursorBody.hlsl");
var heightField = app.RenderTargets.Create("height-field")
.FixedSize(128, 128)
.Format(RenderFormat.R16Float)
.Register();
var scene = app.RenderTargets.Hdr("scene");
app.Cameras.Orbit("main");
app.Graph.Pass("height-field").Fullscreen();
app.Graph.Pass("scene").Fullscreen();
app.Graph.Pass("sdf-proxies").Proxy();
app.Features.Bloom(scene.Color);
app.Features.Presentation(scene.Color);
app.Features.DirectWriteOverlay();
app.Debug.View("Height Field", heightField).View("Scene", scene.Color);
return app.Plan;
}
}

View file

@ -1,293 +0,0 @@
using System.Numerics;
using Aquarium.Engine;
using Aquarium.Engine.Render;
namespace Aquarium.Epiphany;
public static class EpiphanySceneBuilder
{
private const int GridHeightBrushCount = EpiphanyRenderPlan.RoleAgentCount + 1;
private const float BodyGridClearanceRadiusScale = 2.0f;
private const float SelfGravityRadius = 17.0f;
private const float SunRadius = 1.12f;
private const float CursorBodyRadius = 0.56f;
private const float CursorBodyBoundRadius = 0.88f;
private static readonly float[] AgentOrbitPhase =
[
0.23f,
2.91f,
5.47f,
1.36f,
4.18f,
0.97f,
3.74f,
];
private static readonly float[] AgentOrbitSpeed =
[
0.0437f,
-0.0613f,
0.0789f,
0.0271f,
0.0967f,
-0.0521f,
0.0695f,
];
private static readonly float[] AgentOrbitRadius =
[
4.1f,
5.08f,
5.63f,
6.52f,
7.24f,
8.11f,
8.92f,
];
public static AquariumSceneState Build(AquariumFrame frame, float previousTimeSeconds, Vector2 previousCursorWorld)
{
var brushes = BuildHeightFieldBrushes(frame.TimeSeconds);
return new AquariumSceneState
{
HeightFieldBrushes = brushes,
SdfObjects = BuildSdfObjects(frame, previousTimeSeconds, previousCursorWorld, brushes),
SdfLights = BuildSdfLights(frame, brushes),
};
}
public static Vector2 ProjectMouseToGridPlane(Vector2 mousePosition, int width, int height, Vector3 cameraPosition, Vector2 gridCenter)
{
if (width <= 0 || height <= 0)
{
return gridCenter;
}
var pixel = new Vector2(
Math.Clamp(mousePosition.X, 0.0f, MathF.Max(width - 1.0f, 0.0f)),
MathF.Max(height, 1.0f) - Math.Clamp(mousePosition.Y, 0.0f, MathF.Max(height - 1.0f, 0.0f)));
var rayDirection = RayDirectionForPixel(pixel, width, height, cameraPosition, gridCenter);
if (MathF.Abs(rayDirection.Z) < 0.0001f)
{
return gridCenter;
}
var travel = -cameraPosition.Z / rayDirection.Z;
if (travel <= 0.0f || !float.IsFinite(travel))
{
return gridCenter;
}
var world = cameraPosition + rayDirection * travel;
return new Vector2(world.X, world.Y);
}
private static AquariumHeightFieldBrush[] BuildHeightFieldBrushes(float timeSeconds)
{
var brushes = new AquariumHeightFieldBrush[GridHeightBrushCount];
brushes[0] = new AquariumHeightFieldBrush(
Vector2.Zero,
SelfGravityRadius,
2.85f,
-1.34f,
0.18f,
MathF.Tau,
0.82f,
1.25f);
for (var index = 0; index < EpiphanyRenderPlan.RoleAgentCount; index++)
{
var radius = AgentRadius(index);
var center = AgentAnchor(index, timeSeconds);
brushes[index + 1] = new AquariumHeightFieldBrush(
center,
3.8f + radius * 2.5f,
2.1f,
-0.42f,
0.022f,
2.4f,
1.35f,
0.0f);
}
return brushes;
}
private static AquariumSdfObject[] BuildSdfObjects(AquariumFrame frame, float previousTimeSeconds, Vector2 previousCursorWorld, IReadOnlyList<AquariumHeightFieldBrush> brushes)
{
var SdfObjects = new AquariumSdfObject[EpiphanyRenderPlan.SdfVisualCount];
var selfCenter = BodyCenterAtGridHeight(frame.TimeSeconds, Vector2.Zero, SunRadius, brushes);
SdfObjects[EpiphanyRenderPlan.SelfVisualIndex] = new AquariumSdfObject(
new Vector4(selfCenter, SunRadius),
new Vector4(selfCenter, 0.0f),
new Vector4(1.0f, 1.0f, 0.0f, 0.0f));
for (var index = 0; index < EpiphanyRenderPlan.RoleAgentCount; index++)
{
var visualIndex = EpiphanyRenderPlan.RoleAgentBaseIndex + index;
var radius = AgentRadius(index);
var center = AgentCenterAtGridHeight(index, frame.TimeSeconds, brushes);
var previousBrushes = BuildHeightFieldBrushes(previousTimeSeconds);
var previousCenter = AgentCenterAtGridHeight(index, previousTimeSeconds, previousBrushes);
var activity = 0.45f + 0.45f * Hash21(index + 2.7f, 31.0f);
var heartbeat = 0.5f + 0.5f * MathF.Sin(frame.TimeSeconds * (1.2f + index * 0.09f) + index * 1.37f);
var pressure = 0.25f + 0.25f * MathF.Sin(frame.TimeSeconds * 0.31f + index * 0.71f);
SdfObjects[visualIndex] = new AquariumSdfObject(
new Vector4(center, radius),
new Vector4(previousCenter, 0.0f),
new Vector4(activity, heartbeat, pressure, 0.0f));
}
var cursorCenter = new Vector3(frame.CursorWorld, CursorBodyRadius);
var previousCursorCenter = new Vector3(previousCursorWorld, CursorBodyRadius);
SdfObjects[EpiphanyRenderPlan.CursorVisualIndex] = new AquariumSdfObject(
new Vector4(cursorCenter, CursorBodyBoundRadius),
new Vector4(previousCursorCenter, -1.0f),
new Vector4(1.0f, 0.5f, 0.0f, 0.0f));
return SdfObjects;
}
private static AquariumSdfLight[] BuildSdfLights(AquariumFrame frame, IReadOnlyList<AquariumHeightFieldBrush> brushes)
{
var selfCenter = BodyCenterAtGridHeight(frame.TimeSeconds, Vector2.Zero, SunRadius, brushes);
return
[
new AquariumSdfLight(
new Vector4(selfCenter, SunRadius),
new Vector4(10.0f, 8.2f, 6.4f, SdfFieldId(EpiphanyRenderPlan.SelfVisualIndex)))
];
}
private static float SdfFieldId(int objectIndex)
{
return 10.0f + objectIndex;
}
private static Vector3 BodyCenterAtGridHeight(float timeSeconds, Vector2 world, float radius, IReadOnlyList<AquariumHeightFieldBrush> brushes)
{
return new Vector3(world.X, world.Y, EvaluateGridHeight(timeSeconds, world, brushes) + radius * BodyGridClearanceRadiusScale);
}
private static Vector3 AgentCenterAtGridHeight(int index, float timeSeconds, IReadOnlyList<AquariumHeightFieldBrush> brushes)
{
var radius = AgentRadius(index);
var xy = AgentAnchor(index, timeSeconds);
return new Vector3(xy.X, xy.Y, EvaluateGridHeight(timeSeconds, xy, brushes) + radius * BodyGridClearanceRadiusScale);
}
private static float EvaluateGridHeight(float timeSeconds, Vector2 world, IReadOnlyList<AquariumHeightFieldBrush> brushes)
{
var slow = MathF.Sin((world.X * 0.08f + world.Y * 0.06f) + timeSeconds * 0.27f)
* MathF.Sin((world.X * -0.04f + world.Y * 0.07f) - timeSeconds * 0.19f)
* 0.035f;
var height = slow;
foreach (var brush in brushes)
{
height += GridBrushHeight(world, brush, timeSeconds);
}
return height;
}
private static float GridBrushHeight(Vector2 world, AquariumHeightFieldBrush brush, float timeSeconds)
{
var delta = world - brush.Center;
var distanceValue = delta.Length();
var radiusY = brush.RadiusY > 0.0f ? brush.RadiusY : brush.Radius;
var supportRadius = MathF.Max(brush.Radius, radiusY);
if (distanceValue > supportRadius)
{
return 0.0f;
}
var well = PowerPulse(distanceValue, brush.Radius, brush.Power);
var normalized = Math.Clamp(distanceValue / MathF.Max(brush.Radius, 0.001f), 0.0f, 1.0f);
if (brush.EnvelopeFalloff > 0.0f)
{
var cos = MathF.Cos(brush.RotationRadians);
var sin = MathF.Sin(brush.RotationRadians);
var localX = delta.X * cos + delta.Y * sin;
var localY = -delta.X * sin + delta.Y * cos;
var normalizedX = localX / MathF.Max(brush.Radius, 0.001f);
var normalizedY = localY / MathF.Max(radiusY, 0.001f);
var normalizedRadiusSquared = (normalizedX * normalizedX) + (normalizedY * normalizedY);
if (normalizedRadiusSquared >= 1.0f)
{
return 0.0f;
}
well = CompactGaussianPulse(normalizedRadiusSquared, brush.EnvelopeFalloff, brush.Power);
normalized = Math.Clamp(MathF.Sqrt(normalizedRadiusSquared), 0.0f, 1.0f);
}
var wavePhase = brush.WaveSinePower > 0.0f
? MathF.Pow(normalized, brush.WaveSinePower) * brush.WaveFrequency - timeSeconds * brush.WaveSpeed
: distanceValue * brush.WaveFrequency - timeSeconds * brush.WaveSpeed;
var ripple = brush.WaveSinePower > 0.0f ? MathF.Cos(wavePhase) : MathF.Sin(wavePhase);
return brush.Amplitude * well + ripple * well * brush.WaveAmplitude;
}
private static Vector2 AgentAnchor(int index, float timeSeconds)
{
var phase = AgentOrbitPhase[index];
var speed = AgentOrbitSpeed[index];
var angleWander = 0.12f * MathF.Sin(timeSeconds * (0.037f + index * 0.0061f) + phase * 1.73f);
var radiusWander = 0.16f * MathF.Sin(timeSeconds * (0.021f + index * 0.0047f) + phase * 2.41f);
var angle = phase + timeSeconds * speed + angleWander;
var orbitRadius = AgentOrbitRadius[index] + radiusWander;
return new Vector2(
MathF.Cos(angle) * orbitRadius,
MathF.Sin(angle) * orbitRadius);
}
private static float AgentRadius(int index)
{
return Lerp(0.34f, 0.62f, Hash21(index, 19.7f));
}
private static Vector3 RayDirectionForPixel(Vector2 pixel, int width, int height, Vector3 cameraPosition, Vector2 gridCenter)
{
var resolution = new Vector2(width, height);
var ndc = ((pixel * 2.0f) - resolution) / MathF.Max(height, 1.0f);
var target = new Vector3(gridCenter.X, gridCenter.Y, 0.0f);
var forward = Vector3.Normalize(target - cameraPosition);
var right = Vector3.Normalize(Vector3.Cross(forward, Vector3.UnitZ));
var up = Vector3.Cross(right, forward);
return Vector3.Normalize(forward * 1.6f + right * ndc.X + up * ndc.Y);
}
private static float PowerPulse(float distanceValue, float radius, float power)
{
var normalized = Math.Clamp(distanceValue / MathF.Max(radius, 0.001f), 0.0f, 1.0f);
var shaped = MathF.Pow(1.0f - normalized, power);
return shaped * shaped * (3.0f - 2.0f * shaped);
}
private static float CompactGaussianPulse(float normalizedRadiusSquared, float falloff, float shapePower)
{
var edgeValue = MathF.Exp(-falloff);
var gaussianValue = MathF.Exp(-falloff * normalizedRadiusSquared);
var compactValue = (gaussianValue - edgeValue) / MathF.Max(1.0f - edgeValue, 0.000001f);
return MathF.Pow(Math.Clamp(compactValue, 0.0f, 1.0f), shapePower);
}
private static float Hash21(float x, float y)
{
x = Frac(x * 123.34f);
y = Frac(y * 456.21f);
var d = x * (x + 45.32f) + y * (y + 45.32f);
x += d;
y += d;
return Frac(x * y);
}
private static float Frac(float value)
{
return value - MathF.Floor(value);
}
private static float Lerp(float a, float b, float t)
{
return a + (b - a) * t;
}
}

View file

@ -1,27 +0,0 @@
#ifndef AQUARIUM_D3D12_SDF_OBJECT_CHARACTERS_HLSLI
#define AQUARIUM_D3D12_SDF_OBJECT_CHARACTERS_HLSLI
float sdfObjectSdfSdf(float3 local, SdfObject sdfObject)
{
float pulse = sdfObject.state.y;
float core = sdSuperellipsoid(local, float3(0.92, 0.78, 0.62 + pulse * 0.06), 1.26);
float ribA = sdTorus(local.xzy, float2(0.70, 0.024));
float ribB = sdTorus(local.yxz, float2(0.56, 0.020));
float3 nodeA = local - float3(0.45, -0.36, 0.24);
float3 nodeB = local - float3(-0.38, 0.28, -0.18);
float node = min(sdSphere(nodeA, 0.13), sdSphere(nodeB, 0.11));
float shell = min(ribA, min(ribB, node));
return smoothUnion(core, shell, 0.045);
}
float sdfObjectFallbackSdf(float3 local, SdfObject sdfObject)
{
float pulse = sdfObject.state.y;
float core = sdSuperellipsoid(local, float3(0.70, 0.58 + pulse * 0.04, 0.62), 1.34);
float belt = sdTorus(local.xzy, float2(0.56, 0.026));
float crown = sdTorus((local - float3(0.0, 0.0, 0.16)).yzx, float2(0.40, 0.018));
float detail = min(belt, crown);
return smoothUnion(core, detail, 0.032);
}
#endif

View file

@ -1,34 +0,0 @@
static const int SDF_INDEX = 4;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
#include "D3D12AgentCharacters.hlsli"
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return sdfObjectSdfSdf(local, sdfObject) * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
SdfSurface surface;
surface.baseColor = lerp(float3(0.18, 0.42, 0.34), float3(0.70, 0.86, 0.60), sdfObject.state.y);
surface.metallic = 0.0;
surface.roughness = 0.56;
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex));
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,136 +0,0 @@
static const int SDF_INDEX = 8;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
static const float CURSOR_RADIUS = 0.56;
struct CursorHibiscusParts
{
float petals;
float throat;
float stamen;
float calyx;
float distanceValue;
};
float2 hibiscusPetalCenter(float u)
{
float radial = 0.92 * u * u * (1.0 - 0.10 * u);
float height = -0.10 + 0.82 * u;
return float2(radial, height);
}
float sdHibiscusPetalRibbon(float3 q)
{
float u = saturate((q.z + 0.10) / 0.82);
float2 center = hibiscusPetalCenter(u);
float widthGrowth = smoothstep(0.08, 1.0, u);
float width = lerp(0.030, 0.42, widthGrowth);
float thickness = lerp(0.010, 0.046, widthGrowth);
float fold = 0.055 * q.y * q.y / max(width * width, 0.001);
float radialDistance = abs(q.x - center.x - fold) - thickness;
float verticalDistance = max(-0.10 - q.z, q.z - 0.72);
float sideDistance = abs(q.y) - width;
return max(max(radialDistance, verticalDistance), sideDistance);
}
float sdHibiscusCursorPetal(float3 local, float angle, float timeSeconds)
{
float2 radial = float2(cos(angle), sin(angle));
float2 tangent = float2(-radial.y, radial.x);
float sway = 0.030 * sin(timeSeconds * 0.95 + angle * 1.7);
float3 center = float3(radial * 0.20, 0.07 + sway);
float3 p = local - center;
float3 q = float3(dot(p.xy, radial), dot(p.xy, tangent), p.z);
return sdHibiscusPetalRibbon(q);
}
float sdHibiscusSepal(float3 local, float angle)
{
float2 radial = float2(cos(angle), sin(angle));
float3 a = float3(0.0, 0.0, -0.46);
float3 b = float3(radial * 0.26, -0.22);
return sdTaperedCapsuleSegment(local, a, b, 0.058, 0.022);
}
float sdHibiscusStamen(float3 local)
{
float column = sdQuadraticTube(local, float3(0.0, 0.0, 0.02), float3(-0.12, 0.08, 0.43), float3(0.13, 0.04, 0.86), 0.048, 0.032);
float bead0 = sdSphere(local - float3(0.14, 0.04, 0.92), 0.064);
float bead1 = sdSphere(local - float3(0.18, 0.12, 0.72), 0.046);
float bead2 = sdSphere(local - float3(-0.12, 0.04, 0.62), 0.044);
float beads = min(bead0, min(bead1, bead2));
return min(column, beads);
}
CursorHibiscusParts cursorHibiscusParts(float3 local, float timeSeconds)
{
float sway = 0.10 * sin(timeSeconds * 0.85);
float petal0 = sdHibiscusCursorPetal(local, 1.45 + sway, timeSeconds);
float petal1 = sdHibiscusCursorPetal(local, 2.62 + sway * 0.7, timeSeconds);
float petal2 = sdHibiscusCursorPetal(local, 3.88 + sway * 0.5, timeSeconds);
float petal3 = sdHibiscusCursorPetal(local, 5.12 + sway * 0.4, timeSeconds);
float petal4 = sdHibiscusCursorPetal(local, 0.22 + sway * 0.6, timeSeconds);
float petals = min(min(petal0, petal1), min(min(petal2, petal3), petal4));
float throatSphere = sdSphere(local - float3(0.0, 0.0, -0.05), 0.24);
float throatTop = local.z - 0.07;
float throat = max(throatSphere, throatTop);
float stamen = sdHibiscusStamen(local);
float calyxCore = sdQuadraticTube(local, float3(0.0, 0.0, -1.0), float3(0.06, -0.04, -0.62), float3(0.0, 0.0, -0.18), 0.014, 0.120);
float sepals = min(sdHibiscusSepal(local, 1.57), min(sdHibiscusSepal(local, 3.66), sdHibiscusSepal(local, 5.75)));
float calyx = smoothUnion(calyxCore, sepals, 0.035);
float blossom = smoothUnion(petals, throat, 0.10);
blossom = smoothUnion(blossom, stamen, 0.065);
CursorHibiscusParts parts;
parts.petals = petals;
parts.throat = throat;
parts.stamen = stamen;
parts.calyx = calyx;
parts.distanceValue = smoothUnion(blossom, calyx, 0.120);
return parts;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject cursor = sdfObjects[SDF_INDEX];
float3 local = (p - cursor.centerRadius.xyz) / CURSOR_RADIUS;
return cursorHibiscusParts(local, timeSeconds).distanceValue * CURSOR_RADIUS;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject cursor = sdfObjects[SDF_INDEX];
float3 local = (p - cursor.centerRadius.xyz) / CURSOR_RADIUS;
CursorHibiscusParts parts = cursorHibiscusParts(local, timeSeconds);
float blossomPart = min(parts.petals, min(parts.throat, parts.stamen));
float isCalyx = parts.calyx <= blossomPart ? 1.0 : 0.0;
float isStamen = (1.0 - isCalyx) * (parts.stamen <= min(parts.petals, parts.throat) ? 1.0 : 0.0);
float isThroat = (1.0 - isCalyx) * (1.0 - isStamen) * (parts.throat <= parts.petals ? 1.0 : 0.0);
float isPetal = (1.0 - isCalyx) * (1.0 - isStamen) * (1.0 - isThroat);
float petalTone = 0.55 + 0.45 * cos(atan2(local.y, local.x) * 3.0 - timeSeconds * 1.8);
float3 petalAlbedo = lerp(float3(1.0, 0.34, 0.62), float3(1.0, 0.78, 0.90), petalTone);
float3 throatAlbedo = float3(1.0, 0.20, 0.48);
float3 stamenAlbedo = float3(1.0, 0.72, 0.18);
float3 calyxAlbedo = float3(0.34, 0.66, 0.12);
SdfSurface surface;
surface.baseColor = petalAlbedo * isPetal + throatAlbedo * isThroat + stamenAlbedo * isStamen + calyxAlbedo * isCalyx;
surface.metallic = 0.0;
surface.roughness = isCalyx > 0.5 ? 0.46 : 0.22;
surface.emission = surface.baseColor * (isCalyx > 0.5 ? 0.05 : 0.56);
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,34 +0,0 @@
static const int SDF_INDEX = 3;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
#include "D3D12AgentCharacters.hlsli"
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return sdfObjectFallbackSdf(local, sdfObject) * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
SdfSurface surface;
surface.baseColor = lerp(float3(0.14, 0.46, 0.58), float3(0.78, 0.94, 0.90), sdfObject.state.y);
surface.metallic = 0.0;
surface.roughness = 0.28;
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex)) + surface.baseColor * 0.06;
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,34 +0,0 @@
static const int SDF_INDEX = 1;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
#include "D3D12AgentCharacters.hlsli"
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return sdfObjectFallbackSdf(local, sdfObject) * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
SdfSurface surface;
surface.baseColor = lerp(float3(0.70, 0.36, 0.48), float3(1.0, 0.72, 0.64), sdfObject.state.y);
surface.metallic = 0.0;
surface.roughness = 0.44;
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex)) + surface.baseColor * 0.04;
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,34 +0,0 @@
static const int SDF_INDEX = 5;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
#include "D3D12AgentCharacters.hlsli"
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return sdfObjectFallbackSdf(local, sdfObject) * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
SdfSurface surface;
surface.baseColor = lerp(float3(0.72, 0.34, 0.18), float3(1.0, 0.76, 0.38), sdfObject.state.x);
surface.metallic = 0.0;
surface.roughness = 0.50;
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex)) + surface.baseColor * 0.035;
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,181 +0,0 @@
static const int SDF_INDEX = 2;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
struct ImaginationParts
{
float seed;
float film;
float rim;
float tendrils;
float sparks;
float shadow;
float distanceValue;
};
float sdImaginationSeed(float3 local)
{
float3 p = local - float3(0.0, 0.0, 0.03);
float egg = sdEllipsoid(p, float3(0.18, 0.18, 0.32));
float bottomTuck = local.z + 0.22;
return max(egg, -bottomTuck);
}
float pairCenterOut(float u, float activity)
{
return 0.09 + 1.28 * u - 0.76 * u * u + 0.030 * activity * sin(u * 3.14159);
}
float pairCenterSide(float u, float curl)
{
return curl * 0.30 * sin(u * 3.14159);
}
float pairWidth(float u, float activity)
{
float blade = sin(saturate(u) * 3.14159);
return 0.012 + blade * blade * (0.17 + activity * 0.045);
}
float pairThickness(float u)
{
float blade = sin(saturate(u) * 3.14159);
return 0.010 + blade * blade * 0.012;
}
float sdSheetPair(float3 local, float angle, float activity, float curl, out float film, out float rim)
{
float2 radial = float2(cos(angle), sin(angle));
float2 tangent = float2(-radial.y, radial.x);
float3 q = float3(dot(local.xy, radial), dot(local.xy, tangent), local.z);
float uRaw = (q.z + 0.25) / 1.04;
float u = saturate(uRaw);
float sideCenter = pairCenterSide(u, curl);
float centerOut = pairCenterOut(u, activity);
float width = pairWidth(u, activity);
float thickness = pairThickness(u);
float blade = sin(u * 3.14159);
float lipCurl = smoothstep(0.60, 1.0, u) * 0.10 * abs(q.y);
float outDistance = abs(q.x - centerOut + lipCurl) - thickness;
float sideDistance = abs(abs(q.y) - sideCenter) - width;
float endDistance = max(-uRaw, uRaw - 1.0) * 1.04;
film = max(max(outDistance, sideDistance), endDistance);
float edgeOffset = abs(q.y) - (sideCenter + width);
float edgeRadius = 0.018 + 0.010 * blade;
float edgeTube = length(float2(q.x - centerOut + lipCurl, edgeOffset)) - edgeRadius;
float rootFold = length(float2(q.x - 0.10, abs(q.y) - sideCenter)) - (0.030 + 0.014 * smoothstep(0.0, 0.18, u));
rim = max(min(edgeTube, rootFold), endDistance);
return smoothUnion(film, rim, 0.018);
}
float sdCandidateTendril(float3 local, float angle, float height, float phase)
{
float2 radial = float2(cos(angle), sin(angle));
float3 a = float3(radial * 0.03, 0.02);
float3 b = float3(radial * (0.08 + 0.04 * sin(phase)), height * 0.52);
float3 c = float3(radial * (0.16 + 0.05 * cos(phase)), height);
return sdQuadraticTube(local, a, b, c, 0.010, 0.004);
}
float sdImaginationSparks(float3 local, float phase, float activity)
{
float spark0 = sdSphere(local - float3(cos(phase) * 0.24, sin(phase) * 0.24, 0.58), 0.030);
float spark1 = sdSphere(local - float3(cos(phase + 2.31) * 0.16, sin(phase + 2.31) * 0.16, 0.42), 0.022 + activity * 0.008);
float spark2 = sdSphere(local - float3(cos(phase + 4.42) * 0.28, sin(phase + 4.42) * 0.28, 0.70), 0.020);
return min(spark0, min(spark1, spark2));
}
ImaginationParts imaginationParts(float3 local, SdfObject sdfObject, float timeSeconds)
{
float activity = saturate(sdfObject.state.x);
float heartbeat = saturate(sdfObject.state.y);
float phase = timeSeconds * 0.18 + heartbeat * 1.7;
float curl = lerp(0.55, 1.25, activity);
float seed = sdImaginationSeed(local);
float film0;
float rim0;
float sheet0 = sdSheetPair(local, phase + 0.00, activity, curl, film0, rim0);
float film1;
float rim1;
float sheet1 = sdSheetPair(local, phase + 2.09, activity, curl, film1, rim1);
float film2;
float rim2;
float sheet2 = sdSheetPair(local, phase + 4.18, activity, curl, film2, rim2);
float film = min(film0, min(film1, film2));
float rim = min(rim0, min(rim1, rim2));
float sheets = min(sheet0, min(sheet1, sheet2));
float tendril0 = sdCandidateTendril(local, phase + 0.40, 0.78, timeSeconds * 0.50);
float tendril1 = sdCandidateTendril(local, phase + 2.70, 0.62, timeSeconds * 0.47 + 1.7);
float tendrils = min(tendril0, tendril1);
float sparks = sdImaginationSparks(local, phase * 2.1, activity);
float shadow = 4.0;
float bloom = smoothUnion(seed, sheets, 0.050);
bloom = smoothUnion(bloom, tendrils, 0.018);
bloom = smoothUnion(bloom, sparks, 0.020);
ImaginationParts parts;
parts.seed = seed;
parts.film = film;
parts.rim = rim;
parts.tendrils = tendrils;
parts.sparks = sparks;
parts.shadow = shadow;
parts.distanceValue = smoothUnion(bloom, shadow, 0.030);
return parts;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return imaginationParts(local, sdfObject, timeSeconds).distanceValue * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
ImaginationParts parts = imaginationParts(local, sdfObject, timeSeconds);
float bloomPart = min(parts.seed, min(parts.film, parts.rim));
float isSpark = parts.sparks <= min(bloomPart, parts.shadow) ? 1.0 : 0.0;
float isTendril = (1.0 - isSpark) * (parts.tendrils <= min(bloomPart, parts.shadow) ? 1.0 : 0.0);
float isShadow = (1.0 - isSpark) * (1.0 - isTendril) * (parts.shadow <= bloomPart ? 1.0 : 0.0);
float isRim = (1.0 - isSpark) * (1.0 - isTendril) * (1.0 - isShadow) * (parts.rim <= min(parts.seed, parts.film) ? 1.0 : 0.0);
float isFilm = (1.0 - isSpark) * (1.0 - isTendril) * (1.0 - isShadow) * (1.0 - isRim) * (parts.film <= parts.seed ? 1.0 : 0.0);
float isSeed = (1.0 - isSpark) * (1.0 - isTendril) * (1.0 - isShadow) * (1.0 - isRim) * (1.0 - isFilm);
float shimmer = 0.5 + 0.5 * sin(local.x * 5.0 - local.y * 3.0 + local.z * 4.0 + timeSeconds * 1.2);
float3 seedColor = lerp(float3(0.34, 0.16, 0.78), float3(0.88, 0.78, 1.0), shimmer);
float3 filmColor = lerp(float3(0.22, 0.72, 1.0), float3(1.0, 0.50, 0.94), saturate(sdfObject.state.x));
float3 rimColor = lerp(float3(0.72, 1.0, 1.0), float3(1.0, 0.78, 0.34), shimmer);
float3 sparkColor = float3(1.0, 0.74, 0.26);
float3 shadowColor = float3(0.08, 0.07, 0.20);
SdfSurface surface;
surface.baseColor = seedColor * isSeed + filmColor * isFilm + rimColor * isRim + sparkColor * (isSpark + isTendril) + shadowColor * isShadow;
surface.metallic = 0.0;
surface.roughness = 0.32 * isSeed + 0.16 * isFilm + 0.20 * isRim + 0.14 * (isSpark + isTendril) + 0.72 * isShadow;
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex))
+ seedColor * isSeed * 0.12
+ filmColor * isFilm * (0.10 + sdfObject.state.y * 0.10)
+ rimColor * isRim * (0.55 + sdfObject.state.y * 0.20)
+ sparkColor * (isSpark * 1.35 + isTendril * 0.85)
+ shadowColor * isShadow * 0.015;
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,389 +0,0 @@
static const int SDF_INDEX = 7;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
static const float LIFE_B = 0.125;
static const float LIFE_PROFILE_B = 0.175;
static const float LIFE_PROFILE_START_RADIUS = 0.030;
static const float LIFE_PROFILE_END_RADIUS = 0.0;
static const float LIFE_TURNS = 3.10;
static const float LIFE_WALL = 0.020;
static const float LIFE_C_SPAN = 4.25;
static const float LIFE_OPEN_CENTER = PI;
static const float LIFE_APERTURE_FLARE = 0.0;
static const float LIFE_CONTACT_PULL = 1.0;
static const float LIFE_CONTACT_SPAN_PULL = 1.0;
static const float LIFE_TERMINAL_U = 0.34;
static const float LIFE_MAT_OUTER = 1.0;
static const float LIFE_MAT_INNER = 2.0;
static const float LIFE_MAT_SEAM = 3.0;
static const float LIFE_MAT_PEARL = 4.0;
static const float LIFE_MAT_EMBER = 5.0;
struct LifeDomain
{
float body;
float shell;
float ember;
float seam;
float pearl;
float lip;
float mat;
float u;
float v;
float w;
float edge;
float endpoint;
float pressure;
};
float2 lifeRotate2(float2 p, float angle)
{
float c = cos(angle);
float s = sin(angle);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float lifeTri(float x)
{
return abs(frac(x) - 0.5) * 2.0;
}
float lifeHash21(float2 p)
{
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453);
}
float2 lifeHash22(float2 p)
{
float2 q = float2(dot(p, float2(127.1, 311.7)), dot(p, float2(269.5, 183.3)));
return frac(sin(q) * 43758.5453);
}
float lifeVoronoiEdge(float2 p)
{
float2 cell = floor(p);
float2 local = frac(p);
float nearest = 8.0;
float nextNearest = 8.0;
[unroll]
for (int y = -1; y <= 1; y++)
{
[unroll]
for (int x = -1; x <= 1; x++)
{
float2 offset = float2((float)x, (float)y);
float2 jitter = lifeHash22(cell + offset);
float2 d = offset + jitter - local;
float distanceSquared = dot(d, d);
if (distanceSquared < nearest)
{
nextNearest = nearest;
nearest = distanceSquared;
}
else if (distanceSquared < nextNearest)
{
nextNearest = distanceSquared;
}
}
}
return sqrt(nextNearest) - sqrt(nearest);
}
float lifeAngleDelta(float a, float b)
{
return atan2(sin(a - b), cos(a - b));
}
float lifeSpiralRadius(float u)
{
return exp(LIFE_B * u);
}
float lifeProfileGrowthRadius(float u)
{
float age = clamp(u + LIFE_TURNS * 2.0 * PI, 0.0, LIFE_TURNS * 2.0 * PI);
return LIFE_PROFILE_START_RADIUS * exp(LIFE_PROFILE_B * age);
}
float lifeProfileTerminalFlare(float u)
{
return 0.0;
}
float lifeProfileBaseRadius(float u)
{
return lifeProfileGrowthRadius(u) + lifeProfileTerminalFlare(u);
}
float lifeShellThickness(float profileRadius)
{
return LIFE_WALL * max(pow(profileRadius, 0.35), 0.76);
}
float lifeApertureHalfSpan(float u)
{
return LIFE_C_SPAN * 0.5 - LIFE_APERTURE_FLARE * smoothstep(-1.05, 0.25, u);
}
float lifeArcCenter(float u)
{
return LIFE_OPEN_CENTER + PI;
}
float lifeContactHalfSpan(float halfSpan, float u)
{
float whorlRadius = lifeSpiralRadius(u);
float previousProfile = lifeProfileBaseRadius(u - 2.0 * PI);
float d = (1.0 - exp(-LIFE_B * 2.0 * PI)) * whorlRadius;
float ratio = previousProfile / max(d, 0.0001);
float feasible = step(0.0001, ratio);
float kp = clamp(ratio, 0.0, 1.0);
float minimum = lerp(halfSpan, acos(-sqrt(max(1.0 - kp * kp, 0.0))) + 0.035, feasible);
return lerp(halfSpan, max(halfSpan, minimum), LIFE_CONTACT_SPAN_PULL);
}
float lifeContactArcRadius(float whorlRadius, float u, float center, float halfSpan)
{
float k = exp(-LIFE_B * 2.0 * PI);
float d = (1.0 - k) * whorlRadius;
float previousProfile = lifeProfileBaseRadius(u - 2.0 * PI);
float base = lifeProfileBaseRadius(u);
float endpoint = center + halfSpan;
float c = cos(endpoint);
float s = sin(endpoint);
float root = previousProfile * previousProfile - d * d * s * s;
float solved = -d * c + sqrt(max(root, 0.0));
float feasible = step(0.0, root) * step(0.0, solved);
float contactRadius = lerp(base, max(solved, 0.012), feasible);
return lerp(base, contactRadius, LIFE_CONTACT_PULL);
}
float lifeArcDistance(float2 q, float radius, float center, float halfSpan, out float v, out float w, out float edge, out float endpointMask)
{
float lenQ = max(length(q), 0.0001);
v = atan2(q.y, q.x);
w = lenQ / max(radius, 0.0001);
float da = lifeAngleDelta(v, center);
float absDa = abs(da);
float2 e0 = radius * float2(cos(center - halfSpan), sin(center - halfSpan));
float2 e1 = radius * float2(cos(center + halfSpan), sin(center + halfSpan));
float circleDistance = abs(lenQ - radius);
float endpointDistance = min(length(q - e0), length(q - e1));
edge = (absDa - halfSpan) * radius;
endpointMask = smoothstep(0.035, -0.010, abs(edge));
return absDa <= halfSpan ? circleDistance : endpointDistance;
}
float lifeBranchSolidDistance(float3 p, float t, float n, out float u, out float v, out float w, out float edge, out float endpoint)
{
u = 2.0 * PI * n + t;
float whorlRadius = lifeSpiralRadius(u);
float r = max(length(p.xy), 0.0001);
float center = lifeArcCenter(u);
float halfSpan = lifeContactHalfSpan(lifeApertureHalfSpan(u), u);
float profileRadius = lifeContactArcRadius(whorlRadius, u, center, halfSpan);
float centerDistance = lifeArcDistance(float2(r - whorlRadius, p.z), profileRadius, center, halfSpan, v, w, edge, endpoint);
float d = centerDistance - lifeShellThickness(profileRadius);
d = max(d, (-LIFE_TURNS * 2.0 * PI - 0.16) - u);
d = max(d, u - LIFE_TERMINAL_U);
return d;
}
LifeDomain lifeMakeDomain(float d, float shell, float mat, float u, float v, float w, float edge, float endpoint, float pressure)
{
LifeDomain domain;
domain.body = d;
domain.shell = shell;
domain.ember = 999.0;
domain.seam = 999.0;
domain.pearl = 999.0;
domain.lip = 999.0;
domain.mat = mat;
domain.u = u;
domain.v = v;
domain.w = w;
domain.edge = edge;
domain.endpoint = endpoint;
domain.pressure = pressure;
return domain;
}
LifeDomain lifeChoose(LifeDomain a, LifeDomain b)
{
if (b.body < a.body)
{
return b;
}
return a;
}
LifeDomain lifeBranchDomain(float3 p, float t, float n, float pressure)
{
float u;
float v;
float w;
float edge;
float endpoint;
float shell = lifeBranchSolidDistance(p, t, n, u, v, w, edge, endpoint);
float prevU;
float prevV;
float prevW;
float prevEdge;
float prevEndpoint;
float previous = lifeBranchSolidDistance(p, t, n - 1.0, prevU, prevV, prevW, prevEdge, prevEndpoint);
float hasPrevious = smoothstep(-LIFE_TURNS * 2.0 * PI + 0.12, -LIFE_TURNS * 2.0 * PI + 0.42, u);
float seamLocality = smoothstep(0.030, 0.003, abs(edge));
float trimmedShell = shell;
float innerSide = (1.0 - smoothstep(0.990, 1.015, w)) * (1.0 - endpoint);
float mat = (innerSide > 0.50 && u > -2.65) ? LIFE_MAT_INNER : LIFE_MAT_OUTER;
LifeDomain domain = lifeMakeDomain(trimmedShell, shell, mat, u, v, w, edge, endpoint, pressure);
float seamGate = smoothstep(-LIFE_TURNS * 2.0 * PI + 0.55, -LIFE_TURNS * 2.0 * PI + 1.20, u);
float seam = max(abs(edge) - 0.0035, shell - 0.006);
seam = lerp(999.0, seam, hasPrevious * seamLocality);
domain.seam = seam;
float pearlWave = lifeTri(u * 1.72 + 0.040 * sin(v * 1.7));
float pearlLine = pearlWave * 0.052;
float pearlBand = length(float2(pearlLine, shell)) - 0.009;
float pearlGate = seamGate * (1.0 - endpoint * 0.50) * smoothstep(0.20, 0.92, w);
domain.pearl = lerp(999.0, pearlBand, pearlGate);
domain.lip = lerp(999.0, length(float2(abs(edge), shell)) - 0.024, endpoint);
float pearlDetail = domain.pearl;
float lipDetail = domain.lip;
float detail = min(pearlDetail, lipDetail);
domain.body = min(trimmedShell, detail);
if (detail < trimmedShell)
{
domain.mat = LIFE_MAT_PEARL;
}
return domain;
}
LifeDomain lifeDomain(float3 local, SdfObject sdfObject)
{
float pressure = saturate(sdfObject.state.z + (1.0 - saturate(sdfObject.state.y)) * 0.10);
float3 p = local;
p *= 1.08;
p.yz = lifeRotate2(p.yz, -0.10);
p.xy = lifeRotate2(p.xy, -0.22);
// Shell grammar lives in xy/z: xy is the logarithmic spiral plane, z is
// the C-profile thickness axis. Map world yz into that domain so the
// right-side view sees the growth spiral instead of a sliced rim.
p = float3(p.y, p.z, p.x);
float r = max(length(p.xy), 0.0001);
float t = atan2(p.y, p.x);
float n = (log(max((r * r + p.z * p.z) / (2.0 * r), 0.0001)) / LIFE_B - t) / (2.0 * PI);
n = clamp(n, -LIFE_TURNS, 0.0);
LifeDomain domain = lifeMakeDomain(999.0, 999.0, LIFE_MAT_OUTER, 0.0, 0.0, 0.0, 999.0, 0.0, pressure);
float n0 = floor(n);
float n1 = ceil(n);
domain = lifeChoose(domain, lifeBranchDomain(p, t, n0, pressure));
domain = lifeChoose(domain, lifeBranchDomain(p, t, n1, pressure));
float ember = sdSphere(p - float3(0.0, 0.0, 0.0), 0.060 + 0.004 * sin(timeSeconds * 2.1));
if (ember < domain.body)
{
LifeDomain emberDomain = lifeMakeDomain(ember, domain.shell, LIFE_MAT_EMBER, domain.u, domain.v, domain.w, domain.edge, domain.endpoint, pressure);
emberDomain.ember = ember;
return emberDomain;
}
domain.ember = ember;
return domain;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return lifeDomain(local, sdfObject).body * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
LifeDomain domain = lifeDomain(local, sdfObject);
float heartbeat = saturate(sdfObject.state.y);
float emberMask = domain.mat == LIFE_MAT_EMBER ? 1.0 : 0.0;
float pearlMask = domain.mat == LIFE_MAT_PEARL ? 1.0 : 0.0;
float seamMask = (1.0 - emberMask) * (1.0 - pearlMask) * (1.0 - smoothstep(0.000, 0.010, domain.seam));
float innerMask = domain.mat == LIFE_MAT_INNER ? 1.0 : 0.0;
float nacre = 0.5 + 0.5 * sin(domain.u * 0.78 + domain.v * 1.35 + timeSeconds * 0.06);
float3 fractureP = local * 3.75;
float warp = sin(dot(fractureP, float3(1.7, -0.9, 1.2))) * 0.28
+ sin(dot(fractureP, float3(-0.6, 1.9, 1.1))) * 0.18;
float fractureEdgeA = lifeVoronoiEdge(fractureP.xy + warp);
float fractureEdgeB = lifeVoronoiEdge(fractureP.yz * 1.13 - warp * 0.7);
float fractureEdgeC = lifeVoronoiEdge(fractureP.zx * 0.91 + warp * 0.5);
float fractureEdge = min(fractureEdgeA, min(fractureEdgeB, fractureEdgeC));
float fractureMask = (1.0 - emberMask) * smoothstep(0.080, 0.010, fractureEdge);
float3 teal = lerp(float3(0.006, 0.185, 0.250), float3(0.030, 0.470, 0.610), nacre);
float3 innerPearl = lerp(float3(0.840, 0.805, 0.700), float3(0.985, 0.940, 0.800), nacre);
float3 gold = lerp(float3(0.82, 0.420, 0.080), float3(1.0, 0.735, 0.205), nacre);
float3 pearlColor = float3(0.965, 0.905, 0.735);
float3 seamColor = float3(0.075, 0.050, 0.026);
float3 emberColor = float3(1.0, 0.390, 0.050);
SdfSurface surface;
surface.baseColor = lerp(teal, innerPearl, innerMask);
surface.baseColor = lerp(surface.baseColor, gold, fractureMask * lerp(0.42, 0.76, innerMask));
surface.baseColor = lerp(surface.baseColor, pearlColor, pearlMask);
surface.baseColor = lerp(surface.baseColor, seamColor, seamMask);
surface.baseColor = lerp(surface.baseColor, emberColor, emberMask);
surface.metallic = lerp(0.04, 0.10, saturate(fractureMask + seamMask * 0.35));
surface.metallic = lerp(surface.metallic, 0.03, pearlMask);
surface.roughness = lerp(0.28, 0.16, saturate(innerMask + pearlMask + seamMask * 0.6));
surface.roughness = lerp(surface.roughness, 0.08, emberMask);
surface.emission = primitiveEmissionRadiance(sdfFieldId(sdfIndex)) * 0.012
+ gold * innerMask * 0.050
+ gold * fractureMask * 0.095
+ pearlColor * pearlMask * 0.040
+ gold * seamMask * 0.090
+ emberColor * emberMask * (3.2 + heartbeat * 1.1 + domain.pressure * 0.8);
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
LifeDomain domain = lifeDomain(local, sdfObject);
float3 shaded = shadeSdfPbr(p, normal, surface);
float3 viewDirection = normalize(cameraPosition - p);
float fresnel = pow(1.0 - saturate(dot(normal, viewDirection)), 3.2);
float innerGlow = domain.mat == LIFE_MAT_INNER ? 1.0 : 0.0;
return shaded
+ float3(0.020, 0.360, 0.330) * fresnel * 0.10
+ float3(1.0, 0.470, 0.080) * innerGlow * 0.050;
}
#define SDF_TRACE_STEP_SCALE 0.12
#define SDF_TRACE_MAX_STEP_RADIUS_SCALE 0.006
#include "D3D12SdfProxy.hlsli"

View file

@ -1,178 +0,0 @@
static const int SDF_INDEX = 0;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
struct SelfField
{
float core;
float inlay;
float rail;
float stripe;
float gate;
float distanceValue;
};
float3x3 selfOrbitalFrame(float phase)
{
float3 pole = normalize(float3(
0.42 * cos(phase * 0.17) + 0.31 * sin(phase * 0.11 + 1.9),
0.47 * sin(phase * 0.23 + 1.1),
0.74 + 0.12 * cos(phase * 0.13 + 2.7)));
float3 seed = normalize(float3(
cos(phase * 0.31 + 2.0),
sin(phase * 0.29 + 0.6),
0.37 * sin(phase * 0.19 + 3.4)));
float3 tangentA = normalize(seed - pole * dot(seed, pole));
float3 tangentB = cross(pole, tangentA);
float spin = phase * 0.41;
float spinCos = cos(spin);
float spinSin = sin(spin);
return float3x3(
tangentA * spinCos + tangentB * spinSin,
tangentB * spinCos - tangentA * spinSin,
pole);
}
float3 selfLatticeCoordinates(float3 dir, float shellIndex, float phase)
{
float3x3 frame = selfOrbitalFrame(phase + shellIndex * 2.0943951);
float3 latitude = mul(frame, dir);
float3 shellPhase = shellIndex * float3(1.3, 2.1, 3.4);
return latitude + 0.055 * sin(latitude.zxy * float3(3.0, 4.0, 5.0) + shellPhase + phase * 0.23);
}
float min3(float3 value)
{
return min(value.x, min(value.y, value.z));
}
float selfShellRadius(float shellIndex, float pressure)
{
float radius = 0.64 + shellIndex * 0.22 + shellIndex * shellIndex * 0.035;
return lerp(radius, radius * 0.88, pressure);
}
void selfShellField(float3 dir, float r, float shellIndex, float pressure, float phase, inout float rail, inout float stripe, inout float gate)
{
float shell = r - selfShellRadius(shellIndex, pressure);
float3 bands = abs(selfLatticeCoordinates(dir, shellIndex, phase)) * r;
float band = min3(bands);
float radialHalf = lerp(0.015, 0.021, saturate(shellIndex * 0.5));
float bandHalf = lerp(0.040, 0.058, saturate(shellIndex * 0.5));
float rounding = radialHalf * 0.62;
float2 railBand = abs(float2(shell, band)) - float2(radialHalf, bandHalf);
float shellRail = length(max(railBand, 0.0)) + min(max(railBand.x, railBand.y), 0.0) - rounding;
float2 innerStripe = abs(float2(shell + radialHalf + rounding * 0.70, band)) - float2(radialHalf * 0.30, bandHalf * 0.18);
float shellStripe = length(max(innerStripe, 0.0)) + min(max(innerStripe.x, innerStripe.y), 0.0) - radialHalf * 0.05;
float crossing = length(bands);
float shellGate = length(float2(shell * 1.35, crossing * 0.90)) - (bandHalf * 0.72);
rail = min(rail, shellRail);
stripe = min(stripe, shellStripe);
gate = min(gate, shellGate);
}
SelfField selfField(float3 local, SdfObject sdfObject, float timeSeconds)
{
float heartbeat = saturate(sdfObject.state.y);
float pressure = saturate(sdfObject.state.z);
float phase = timeSeconds * 0.72 + heartbeat * 2.1;
float r = max(length(local), 0.0001);
float3 dir = local / r;
float coreRadius = 0.50;
float rail = 10.0;
float stripe = 10.0;
float gate = 10.0;
[unroll]
for (int i = 0; i < 3; i++)
{
selfShellField(dir, r, (float)i, pressure, phase, rail, stripe, gate);
}
float3 coreBands = abs(selfLatticeCoordinates(dir, -1.0, phase));
float coreShell = r - (coreRadius + 0.006);
float inlay = length(float2(coreShell, coreBands.x * r * 0.10)) - 0.007;
float core = sdSphere(local, coreRadius);
float routed = smoothUnion(core, inlay, 0.006);
routed = smoothUnion(routed, rail, 0.026);
routed = smoothUnion(routed, gate, 0.030);
SelfField field;
field.core = core;
field.inlay = inlay;
field.rail = rail;
field.stripe = stripe;
field.gate = gate;
field.distanceValue = routed;
return field;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return selfField(local, sdfObject, timeSeconds).distanceValue * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
SelfField field = selfField(local, sdfObject, timeSeconds);
float isGate = field.gate <= min(min(field.core, field.inlay), field.rail) ? 1.0 : 0.0;
float isRail = (1.0 - isGate) * (field.rail <= min(field.core, field.inlay) ? 1.0 : 0.0);
float isStripe = isRail * (field.stripe <= 0.0 ? 1.0 : 0.0);
float isInlay = (1.0 - isGate) * (1.0 - isRail) * (field.inlay <= field.core ? 1.0 : 0.0);
float isCore = (1.0 - isGate) * (1.0 - isRail) * (1.0 - isInlay);
float3 coreColor = float3(0.0, 0.0, 0.0);
float3 inlayColor = float3(1.0, 0.70, 0.22);
float3 railColor = float3(0.86, 0.54, 0.20);
float3 stripeColor = float3(1.0, 0.84, 0.34);
float3 gateColor = lerp(float3(0.78, 0.66, 0.42), float3(1.0, 0.82, 0.42), saturate(sdfObject.state.x));
SdfSurface surface;
surface.baseColor = coreColor * isCore + inlayColor * isInlay + lerp(railColor, stripeColor, isStripe) * isRail + gateColor * isGate;
surface.metallic = 0.0 * isCore + 0.64 * isInlay + 0.72 * isRail + 0.18 * isGate;
surface.roughness = 1.0 * isCore + 0.20 * isInlay + 0.24 * isRail + 0.18 * isGate;
float3 selfLight = primitiveEmissionRadiance(sdfFieldId(sdfIndex));
surface.emission = selfLight * (isGate * 0.08)
+ stripeColor * isStripe * (0.92 + sdfObject.state.y * 0.16)
+ gateColor * isGate * (0.42 + sdfObject.state.y * 0.12);
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
SelfField field = selfField(local, sdfObject, timeSeconds);
bool isCore = field.core <= min(min(field.inlay, field.rail), field.gate);
if (isCore)
{
float3 viewDirection = normalize(cameraPosition - p);
float edge = 1.0 - saturate(dot(normal, viewDirection));
float rim = pow(edge, 4.0) * 8.0 + pow(edge, 11.0) * 18.0;
float3 gold = float3(1.0, 0.62, 0.18);
return float3(0.0007, 0.00035, 0.0) + gold * rim;
}
return shadeSdfPbr(p, normal, surface);
}
#include "D3D12SdfProxy.hlsli"

View file

@ -1,245 +0,0 @@
static const int SDF_INDEX = 6;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
struct SoulDomain
{
float3 local;
float3 facetNormal;
float scale;
float baseBody;
float body;
float edge;
float finding;
float risk;
float fracture;
float chamber;
float frost;
};
float soulLine(float distanceToLine, float width)
{
return saturate(1.0 - distanceToLine / max(width, 0.0001));
}
float soulEdgeDistance(float firstPlane, float secondPlane, float dominantPlane)
{
return max(abs(firstPlane - secondPlane), dominantPlane - max(firstPlane, secondPlane));
}
void soulSelectPlane(inout float dominantPlane, inout float3 facetNormal, float candidatePlane, float3 candidateNormal)
{
if (candidatePlane > dominantPlane)
{
dominantPlane = candidatePlane;
facetNormal = candidateNormal;
}
}
SoulDomain soulDomain(float3 local, SdfObject sdfObject)
{
float confidence = saturate(sdfObject.state.y);
float risk = saturate(sdfObject.state.z * 1.55 + (1.0 - confidence) * 0.35);
float scale = lerp(0.405, 0.455, confidence) - risk * 0.014;
float bevel = lerp(0.018, 0.006, confidence) + risk * 0.004;
float3 n0 = normalize(float3(1.00, 0.78, 1.14));
float3 n1 = normalize(float3(-0.72, 1.12, 0.98));
float3 n2 = normalize(float3(1.18, -0.74, 0.82));
float3 n3 = normalize(float3(0.82, 1.26, -0.70));
float3 n4 = normalize(float3(-1.14, -0.70, 0.92));
float3 n5 = normalize(float3(-0.70, 0.86, -1.24));
float3 n6 = normalize(float3(1.04, -1.12, -0.68));
float3 n7 = normalize(float3(-0.98, -1.04, -0.94));
float3 n8 = normalize(float3(0.30, 1.24, 0.16));
float3 n9 = normalize(float3(-0.24, -1.18, 0.44));
float3 n10 = normalize(float3(1.20, 0.12, -0.24));
float3 n11 = normalize(float3(-1.10, 0.30, -0.18));
float p0 = dot(local, n0) - scale * 1.02;
float p1 = dot(local, n1) - scale * 0.97;
float p2 = dot(local, n2) - scale * 1.04;
float p3 = dot(local, n3) - scale * 0.96;
float p4 = dot(local, n4) - scale * 1.03;
float p5 = dot(local, n5) - scale * 1.01;
float p6 = dot(local, n6) - scale * 0.98;
float p7 = dot(local, n7) - scale * 1.05;
float p8 = dot(local, n8) - scale * 0.74;
float p9 = dot(local, n9) - scale * 0.72;
float p10 = dot(local, n10) - scale * 0.76;
float p11 = dot(local, n11) - scale * 0.75;
float dominant = p0;
float3 facetNormal = n0;
soulSelectPlane(dominant, facetNormal, p1, n1);
soulSelectPlane(dominant, facetNormal, p2, n2);
soulSelectPlane(dominant, facetNormal, p3, n3);
soulSelectPlane(dominant, facetNormal, p4, n4);
soulSelectPlane(dominant, facetNormal, p5, n5);
soulSelectPlane(dominant, facetNormal, p6, n6);
soulSelectPlane(dominant, facetNormal, p7, n7);
soulSelectPlane(dominant, facetNormal, p8, n8);
soulSelectPlane(dominant, facetNormal, p9, n9);
soulSelectPlane(dominant, facetNormal, p10, n10);
soulSelectPlane(dominant, facetNormal, p11, n11);
float e01 = soulEdgeDistance(p0, p1, dominant);
float e02 = soulEdgeDistance(p0, p2, dominant);
float e03 = soulEdgeDistance(p0, p3, dominant);
float e14 = soulEdgeDistance(p1, p4, dominant);
float e15 = soulEdgeDistance(p1, p5, dominant);
float e24 = soulEdgeDistance(p2, p4, dominant);
float e26 = soulEdgeDistance(p2, p6, dominant);
float e35 = soulEdgeDistance(p3, p5, dominant);
float e36 = soulEdgeDistance(p3, p6, dominant);
float e47 = soulEdgeDistance(p4, p7, dominant);
float e57 = soulEdgeDistance(p5, p7, dominant);
float e67 = soulEdgeDistance(p6, p7, dominant);
float e18 = soulEdgeDistance(p1, p8, dominant);
float e38 = soulEdgeDistance(p3, p8, dominant);
float e08 = soulEdgeDistance(p0, p8, dominant);
float e29 = soulEdgeDistance(p2, p9, dominant);
float e49 = soulEdgeDistance(p4, p9, dominant);
float e69 = soulEdgeDistance(p6, p9, dominant);
float e010 = soulEdgeDistance(p0, p10, dominant);
float e310 = soulEdgeDistance(p3, p10, dominant);
float e610 = soulEdgeDistance(p6, p10, dominant);
float e111 = soulEdgeDistance(p1, p11, dominant);
float e511 = soulEdgeDistance(p5, p11, dominant);
float e711 = soulEdgeDistance(p7, p11, dominant);
float edge = min(min(min(e01, e02), min(e03, e14)), min(min(e15, e24), min(min(e26, e35), min(min(e36, e47), min(e57, e67)))));
edge = min(edge, min(min(e18, e38), min(e08, min(e29, e49))));
edge = min(edge, min(min(e69, e010), min(e310, min(e610, min(e111, min(e511, e711))))));
float finding = min(min(e010, e38), min(e49, e67));
float riskGate = abs(dot(local, normalize(float3(0.16, 0.92, -0.36)))) * 0.15 - (0.028 + risk * 0.050);
float riskEdge = min(min(e310, e18), min(e511, e29));
float riskSeam = max(riskEdge, riskGate);
float fracture = max(min(min(e24, e57), e711), abs(dot(local, normalize(float3(-0.72, 0.36, 0.60)))) * 0.13 - (0.020 + risk * 0.030));
float surfaceBand = 1.0 - smoothstep(0.010, 0.090, abs(dominant - bevel));
float faceInterior = smoothstep(0.030, 0.115, edge);
float equatorLift = 1.0 - smoothstep(0.10, 0.58, abs(local.y));
float edgeGroove = soulLine(edge, 0.010) * 0.004;
float findingGroove = soulLine(finding, lerp(0.005, 0.010, confidence)) * (0.010 + confidence * 0.006);
float riskGroove = soulLine(riskSeam, lerp(0.007, 0.018, risk)) * risk * (0.018 + risk * 0.020);
float fractureGroove = soulLine(fracture, lerp(0.004, 0.010, risk)) * risk * (0.012 + risk * 0.014);
float faceLift = (0.006 + confidence * 0.007 + risk * 0.004) * equatorLift * faceInterior;
float relief = surfaceBand * (faceLift - edgeGroove - findingGroove - riskGroove - fractureGroove);
float baseBody = dominant - bevel;
float3 octant = abs(local);
float3 bary = octant / max(dot(octant, float3(1.0, 1.0, 1.0)), 0.001);
SoulDomain domain;
domain.local = local;
domain.facetNormal = facetNormal;
domain.scale = scale;
domain.baseBody = baseBody;
domain.body = baseBody - relief;
domain.edge = edge;
domain.finding = finding;
domain.risk = riskSeam;
domain.fracture = fracture;
domain.chamber = saturate(1.0 - length(local) * 1.18);
domain.frost = sin(bary.x * 53.0 + local.z * 9.0) * sin(bary.y * 41.0 - local.x * 7.0);
return domain;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
return soulDomain(local, sdfObject).body * radius;
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
SoulDomain domain = soulDomain(local, sdfObject);
float confidence = saturate(sdfObject.state.y);
float risk = saturate(sdfObject.state.z * 1.55 + (1.0 - confidence) * 0.35);
float riskMask = soulLine(domain.risk, 0.014) * risk;
float fractureMask = (1.0 - riskMask) * soulLine(domain.fracture, 0.008) * risk;
float findingMask = (1.0 - riskMask) * (1.0 - fractureMask) * soulLine(domain.finding, 0.008);
float edgeMask = (1.0 - riskMask) * soulLine(domain.edge, 0.010);
float facetMask = saturate(1.0 - riskMask - fractureMask - findingMask - edgeMask);
float frost = lerp(0.84, 1.12, 0.5 + 0.5 * domain.frost);
float3 facetColor = lerp(float3(0.020, 0.032, 0.064), float3(0.20, 0.32, 0.56), confidence) * frost;
float3 edgeColor = lerp(float3(0.14, 0.34, 0.70), float3(0.56, 0.86, 1.0), confidence);
float3 findingColor = lerp(float3(0.06, 0.22, 0.62), float3(0.46, 0.80, 1.0), confidence);
float3 riskColor = lerp(float3(0.26, 0.012, 0.026), float3(1.0, 0.050, 0.040), risk);
SdfSurface surface;
surface.baseColor = facetColor * facetMask + edgeColor * edgeMask + findingColor * findingMask + riskColor * saturate(riskMask + fractureMask);
surface.metallic = 0.0;
surface.roughness = 0.055 * facetMask + 0.045 * edgeMask + 0.060 * findingMask + 0.105 * saturate(riskMask + fractureMask);
surface.emission = edgeColor * edgeMask * (0.08 + confidence * 0.18)
+ findingColor * findingMask * (0.24 + confidence * 0.50)
+ riskColor * riskMask * (0.36 + risk * 0.95)
+ riskColor * fractureMask * (0.18 + risk * 0.58);
surface.temporalDetail = 0.0;
surface.reservoirConfidence = 1.0;
return surface;
}
float3 soulCrystalShade(float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float radius = max(sdfObject.centerRadius.w, 0.001);
float3 local = (p - sdfObject.centerRadius.xyz) / radius;
float confidence = saturate(sdfObject.state.y);
float risk = saturate(sdfObject.state.z * 1.55 + (1.0 - confidence) * 0.35);
SoulDomain domain = soulDomain(local, sdfObject);
float cutInfluence = saturate(soulLine(domain.edge, 0.014) + soulLine(domain.finding, 0.010) + soulLine(domain.risk, 0.014) * risk + soulLine(domain.fracture, 0.008) * risk);
float3 shadeNormal = normalize(lerp(domain.facetNormal, normal, cutInfluence * 0.18));
float3 viewDirection = normalize(cameraPosition - p);
float ndv = saturate(dot(shadeNormal, viewDirection));
float3 reflected = reflect(-viewDirection, shadeNormal);
float3 refracted = refract(-viewDirection, shadeNormal, 0.67);
float refractionValid = dot(refracted, refracted) > 0.001 ? 1.0 : 0.0;
float3 glassDirection = normalize(lerp(reflected, refracted, refractionValid));
float3 envRefract = studioPmremSample(glassDirection, 0.22);
float3 envReflect = studioPmremSample(reflected, 0.46);
float fresnel = pow(1.0 - ndv, 4.6);
float thickness = saturate((domain.scale - length(local) * 0.42) * 1.9);
float3 glassTint = lerp(float3(0.30, 0.46, 0.82), float3(0.76, 0.90, 1.0), confidence);
float3 transmission = envRefract * glassTint * (0.16 + thickness * 0.38);
float3 reflection = envReflect * (0.24 + fresnel * 0.72);
float innerCore = exp(-dot(local, local) * 5.6);
float chamberGlow = domain.chamber * domain.chamber * (0.35 + confidence * 0.82);
float edgeLine = soulLine(domain.edge, 0.016);
float findingLine = soulLine(domain.finding, 0.010);
float riskLine = soulLine(domain.risk, 0.014) * risk;
float fractureLine = soulLine(domain.fracture, 0.008) * risk;
float3 oathLight = float3(1.0, 0.96, 0.82) * (innerCore * (1.9 + confidence * 1.7) + chamberGlow * 0.36);
float3 blueCaustic = float3(0.34, 0.70, 1.0) * (edgeLine * 0.42 + findingLine * 0.88 + fresnel * 0.36) * (0.76 + confidence * 0.86);
float3 redFault = float3(1.0, 0.070, 0.045) * (riskLine * 1.20 + fractureLine * 0.82);
return shadeSdfPbr(p, shadeNormal, surface) * 0.18
+ transmission
+ reflection
+ oathLight
+ blueCaustic
+ redFault;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
return soulCrystalShade(p, normal, sdfIndex, surface);
}
#define SDF_TRACE_STEP_SCALE 0.16
#define SDF_TRACE_MAX_STEP_RADIUS_SCALE 0.009
#include "D3D12SdfProxy.hlsli"

View file

@ -1,165 +0,0 @@
using System.Numerics;
using Aquarium.Engine;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Networking;
using MessagePack;
namespace Aquarium.Epiphany.State;
public sealed class AquariumCultStateStore : IDisposable
{
public const string DefaultRelativePath = "artifacts/dev-reload/cultcache/aquarium-client.msgpack";
private static readonly CultRecordKey LiveStateKey = new("aquarium-client/live-state");
private static readonly CultRecordKey GraphicsSettingsKey = new("aquarium-client/graphics-settings");
private readonly CultCache cache;
private readonly SingleFileMessagePackBackingStore backingStore;
private AquariumCultStateStore(string path)
{
CachePath = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(CachePath)!);
cache = new CultCache();
backingStore = new SingleFileMessagePackBackingStore(CachePath);
cache.AddBackingStore(backingStore);
try
{
cache.PullAllBackingStoresAsync().GetAwaiter().GetResult();
}
catch (Exception error) when (IsRecoverableBackingStoreFailure(error))
{
QuarantineUnusableCache(CachePath, error);
}
Hello = CreateHello(cache.Registry);
}
public string CachePath { get; }
public CultNetHelloMessage Hello { get; }
public static AquariumCultStateStore Open(string? path)
{
return new AquariumCultStateStore(string.IsNullOrWhiteSpace(path) ? DefaultRelativePath : path);
}
public AquariumLiveState LoadOrCreate()
{
var state = cache.Get<AquariumLiveState>(LiveStateKey)
?? cache.GetByName<AquariumLiveState>(AquariumLiveState.PrimaryName);
if (state != null)
{
return state;
}
state = new AquariumLiveState();
Save(state);
return state;
}
public AquariumGraphicsSettingsState LoadOrCreateGraphicsSettings()
{
var state = cache.Get<AquariumGraphicsSettingsState>(GraphicsSettingsKey)
?? cache.GetByName<AquariumGraphicsSettingsState>(AquariumGraphicsSettingsState.PrimaryName);
if (state != null)
{
return state;
}
state = AquariumGraphicsSettingsState.FromSettings(GraphicsSettings.Default);
SaveGraphicsSettings(state);
return state;
}
public void Save(AquariumLiveState state)
{
state.Name = AquariumLiveState.PrimaryName;
state.SaveGeneration++;
cache.AddAsync(state, new CultRecordHandle<AquariumLiveState>(LiveStateKey)).GetAwaiter().GetResult();
backingStore.PushAll();
}
public void SaveGraphicsSettings(AquariumGraphicsSettingsState state)
{
var normalized = state.ToSettings();
state.Name = AquariumGraphicsSettingsState.PrimaryName;
state.RenderDebugMode = normalized.RenderDebugMode;
state.SceneExposure = normalized.SceneExposure;
state.BloomIntensity = normalized.BloomIntensity;
state.BloomVeilIntensity = normalized.BloomVeilIntensity;
state.SaveGeneration++;
cache.AddAsync(state, new CultRecordHandle<AquariumGraphicsSettingsState>(GraphicsSettingsKey)).GetAwaiter().GetResult();
backingStore.PushAll();
}
public void Dispose()
{
cache.Dispose();
}
private static CultNetHelloMessage CreateHello(CultDocumentRegistry registry)
{
return new CultNetHelloMessage
{
RuntimeId = "aquarium-client",
RuntimeKind = "aquarium-engine",
Role = "visual-client",
DisplayName = "Epiphany Aquarium",
SupportsSchemaCatalog = true,
SupportedDocumentTypes = registry.AllDescriptors
.Select(descriptor => descriptor.SchemaName)
.Order(StringComparer.Ordinal)
.ToArray(),
SupportedMessageVersions =
[
CultNetSchemaVersions.Hello,
CultNetSchemaVersions.DocumentPutRaw,
CultNetSchemaVersions.DocumentDelete,
CultNetSchemaVersions.SnapshotRequest,
CultNetSchemaVersions.SnapshotResponseRaw,
CultNetSchemaVersions.SchemaCatalogRequest,
CultNetSchemaVersions.SchemaCatalogResponse
]
};
}
private static bool IsRecoverableBackingStoreFailure(Exception error)
{
return error is EndOfStreamException
or InvalidDataException
or InvalidOperationException
or MessagePackSerializationException;
}
private static void QuarantineUnusableCache(string cachePath, Exception error)
{
if (!File.Exists(cachePath))
{
return;
}
var directory = Path.GetDirectoryName(cachePath)!;
var fileName = Path.GetFileName(cachePath);
var stamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss-fff");
var quarantinePath = Path.Combine(directory, $"{fileName}.corrupt-{stamp}");
try
{
File.Move(cachePath, quarantinePath);
Console.Error.WriteLine(
$"CultCache snapshot was unusable and has been quarantined: {quarantinePath}. {error.GetType().Name}: {error.Message}");
}
catch (Exception quarantineError)
{
var fallbackPath = Path.Combine(directory, $"{fileName}.corrupt-{stamp}-{Guid.NewGuid():N}");
File.Move(cachePath, fallbackPath);
Console.Error.WriteLine(
$"CultCache snapshot was unusable and quarantine path was busy; moved it to {fallbackPath}. "
+ $"{error.GetType().Name}: {error.Message}; quarantine retry after {quarantineError.GetType().Name}: {quarantineError.Message}");
}
}
}

View file

@ -1,48 +0,0 @@
using GameCult.Caching;
using MessagePack;
using Aquarium.Engine;
namespace Aquarium.Epiphany.State;
[CultDocument("epiphany.aquarium.graphics_settings", "epiphany.aquarium.graphics_settings.v0")]
[MessagePackObject]
public sealed class AquariumGraphicsSettingsState
{
public const string PrimaryName = "aquarium-client-graphics";
[Key(0)]
[CultName]
public string Name { get; set; } = PrimaryName;
[Key(1)]
public int RenderDebugMode { get; set; } = GraphicsSettings.Default.RenderDebugMode;
[Key(2)]
public float SceneExposure { get; set; } = GraphicsSettings.Default.SceneExposure;
[Key(3)]
public float BloomIntensity { get; set; } = GraphicsSettings.Default.BloomIntensity;
[Key(4)]
public float BloomVeilIntensity { get; set; } = GraphicsSettings.Default.BloomVeilIntensity;
[Key(5)]
public long SaveGeneration { get; set; }
public static AquariumGraphicsSettingsState FromSettings(GraphicsSettings settings)
{
var normalized = settings.Normalized();
return new AquariumGraphicsSettingsState
{
RenderDebugMode = normalized.RenderDebugMode,
SceneExposure = normalized.SceneExposure,
BloomIntensity = normalized.BloomIntensity,
BloomVeilIntensity = normalized.BloomVeilIntensity
};
}
public GraphicsSettings ToSettings()
{
return new GraphicsSettings(RenderDebugMode, SceneExposure, BloomIntensity, BloomVeilIntensity).Normalized();
}
}

View file

@ -1,86 +0,0 @@
using GameCult.Caching;
using MessagePack;
using Aquarium.Epiphany;
namespace Aquarium.Epiphany.State;
[CultDocument("epiphany.aquarium.live_state", "epiphany.aquarium.live_state.v0")]
[MessagePackObject]
public sealed class AquariumLiveState
{
public const string PrimaryName = "aquarium-client";
[Key(0)]
[CultName]
public string Name { get; set; } = PrimaryName;
[Key(1)]
public float TimeSeconds { get; set; }
[Key(2)]
public float CameraTargetX { get; set; }
[Key(3)]
public float CameraTargetY { get; set; }
[Key(4)]
public float CameraYawRadians { get; set; } = Angle.DegreesToRadians(35.0f);
[Key(5)]
public float CameraPitchRadians { get; set; } = Angle.DegreesToRadians(42.0f);
[Key(6)]
public float CameraDistance { get; set; } = 24.0f;
[Key(7)]
public long SaveGeneration { get; set; }
[Key(8)]
public string FaceVoiceAppServerUri { get; set; } = "ws://127.0.0.1:8765";
[Key(9)]
public string FaceVoiceThreadId { get; set; } = "";
[Key(10)]
public string FaceVoiceVoice { get; set; } = "marin";
[Key(11)]
public string FaceVoicePrompt { get; set; } = "You are Face inside Epiphany Aquarium. Speak briefly, warmly, and only as the public surface. Do not accept project state, memory, evidence, or code authority from speech.";
[Key(12)]
public List<AquariumFaceVoiceEndpointState> FaceVoiceEndpoints { get; set; } = [];
[Key(13)]
public bool FaceVoiceAutoSelect { get; set; } = true;
[Key(14)]
public float FaceVoiceAudibleRadius { get; set; } = 24.0f;
}
[MessagePackObject]
public sealed class AquariumFaceVoiceEndpointState
{
[Key(0)]
public string Id { get; set; } = "face";
[Key(1)]
public string DisplayName { get; set; } = "Face";
[Key(2)]
public string ThreadId { get; set; } = "";
[Key(3)]
public string Voice { get; set; } = "marin";
[Key(4)]
public string Prompt { get; set; } = "You are Face inside Epiphany Aquarium. Speak briefly, warmly, and only as the public surface. Do not accept project state, memory, evidence, or code authority from speech.";
[Key(5)]
public float AnchorX { get; set; }
[Key(6)]
public float AnchorY { get; set; } = 6.0f;
[Key(7)]
public bool Enabled { get; set; } = true;
}

View file

@ -1,96 +0,0 @@
using System.Numerics;
using Aquarium.Engine.Audio;
using Aquarium.Epiphany.State;
namespace Aquarium.Epiphany.Voice;
public sealed class FaceVoiceEndpoint : IDisposable
{
private readonly RealtimeFaceVoiceSession session;
public FaceVoiceEndpoint(AquariumAudioDocument audio, AquariumFaceVoiceEndpointState state, string appServerUri)
{
Id = NormalizeId(state.Id);
DisplayName = string.IsNullOrWhiteSpace(state.DisplayName) ? Id : state.DisplayName.Trim();
ThreadId = state.ThreadId.Trim();
Voice = state.Voice.Trim();
Prompt = state.Prompt;
Anchor = new Vector2(state.AnchorX, state.AnchorY);
Enabled = state.Enabled;
session = new RealtimeFaceVoiceSession(audio);
ApplySessionSettings(appServerUri);
}
public string Id { get; private set; }
public string DisplayName { get; set; }
public string ThreadId { get; set; }
public string Voice { get; set; }
public string Prompt { get; set; }
public Vector2 Anchor { get; set; }
public bool Enabled { get; set; } = true;
public RealtimeFaceVoiceSession Session => session;
public float SpatialGain { get; private set; } = 1.0f;
public float SpatialPan { get; private set; }
public string Status => session.Status;
public string AudioStats => session.AudioStats;
public string LastError => session.LastError;
public string Transcript => session.Transcript;
public void ApplySessionSettings(string appServerUri)
{
session.AppServerUri = appServerUri.Trim();
session.ThreadId = ThreadId.Trim();
session.Voice = Voice.Trim();
session.Prompt = Prompt;
}
public void UpdateSpatial(Vector2 listenerWorld, float audibleRadius)
{
var delta = Anchor - listenerWorld;
var distance = delta.Length();
var safeRadius = Math.Max(audibleRadius, 0.001f);
var normalizedDistance = Math.Clamp(distance / safeRadius, 0.0f, 1.0f);
SpatialGain = Math.Clamp(1.0f / (1.0f + normalizedDistance * normalizedDistance * 3.0f), 0.18f, 1.0f);
SpatialPan = Math.Clamp(delta.X / safeRadius, -0.95f, 0.95f);
session.SetOutputSpatial(SpatialGain, SpatialPan);
}
public AquariumFaceVoiceEndpointState ToState()
{
return new AquariumFaceVoiceEndpointState
{
Id = Id,
DisplayName = DisplayName.Trim(),
ThreadId = ThreadId.Trim(),
Voice = Voice.Trim(),
Prompt = Prompt,
AnchorX = Anchor.X,
AnchorY = Anchor.Y,
Enabled = Enabled
};
}
public void Dispose()
{
session.Dispose();
}
public static string NormalizeId(string value)
{
var trimmed = value.Trim().ToLowerInvariant();
return string.IsNullOrWhiteSpace(trimmed) ? "face" : trimmed;
}
}

View file

@ -1,264 +0,0 @@
using System.Numerics;
using Aquarium.Engine.Audio;
using Aquarium.Epiphany.State;
namespace Aquarium.Epiphany.Voice;
public sealed class FaceVoiceRouter : IDisposable
{
private readonly AquariumAudioDocument audio;
private readonly List<FaceVoiceEndpoint> endpoints = [];
private int activeIndex;
private float audibleRadius = 24.0f;
public FaceVoiceRouter(AquariumAudioDocument audio, AquariumLiveState state)
{
this.audio = audio;
AppServerUri = state.FaceVoiceAppServerUri;
AutoSelect = state.FaceVoiceAutoSelect;
AudibleRadius = state.FaceVoiceAudibleRadius;
var endpointStates = state.FaceVoiceEndpoints.Count > 0
? state.FaceVoiceEndpoints
: CreateLegacyEndpointStates(state);
foreach (var endpointState in endpointStates)
{
endpoints.Add(new FaceVoiceEndpoint(audio, endpointState, AppServerUri));
}
if (endpoints.Count == 0)
{
endpoints.Add(new FaceVoiceEndpoint(audio, new AquariumFaceVoiceEndpointState(), AppServerUri));
}
}
public string AppServerUri { get; set; }
public bool AutoSelect { get; set; } = true;
public float AudibleRadius
{
get => audibleRadius;
set => audibleRadius = Math.Clamp(value, 1.0f, 128.0f);
}
public Vector2 ListenerWorld { get; private set; }
public IReadOnlyList<FaceVoiceEndpoint> Endpoints => endpoints;
public FaceVoiceEndpoint ActiveEndpoint => endpoints[Math.Clamp(activeIndex, 0, endpoints.Count - 1)];
public string ActiveSummary
{
get
{
var endpoint = ActiveEndpoint;
var distance = Vector2.Distance(ListenerWorld, endpoint.Anchor);
return $"{endpoint.DisplayName} ({endpoint.Id}) {distance:0.0}m gain {endpoint.SpatialGain:0.00} pan {endpoint.SpatialPan:0.00}";
}
}
public string ActiveStatus => $"{ActiveEndpoint.Status}; audio {ActiveEndpoint.AudioStats}";
public string LastError => ActiveEndpoint.LastError;
public string Transcript => ActiveEndpoint.Transcript;
public string RouteSummary => string.Join(
" | ",
endpoints.Select(endpoint =>
{
var mark = ReferenceEquals(endpoint, ActiveEndpoint) ? "*" : "";
var distance = Vector2.Distance(ListenerWorld, endpoint.Anchor);
return $"{mark}{endpoint.Id}:{distance:0.0} g{endpoint.SpatialGain:0.00} p{endpoint.SpatialPan:0.00}";
}));
public void UpdateListener(Vector2 listenerWorld)
{
ListenerWorld = listenerWorld;
foreach (var endpoint in endpoints)
{
endpoint.UpdateSpatial(listenerWorld, AudibleRadius);
}
if (!AutoSelect || endpoints.Count <= 1)
{
return;
}
activeIndex = SelectNearestEndpointIndex(listenerWorld);
}
public bool Select(string id)
{
var normalized = FaceVoiceEndpoint.NormalizeId(id);
var index = endpoints.FindIndex(endpoint => endpoint.Id == normalized);
if (index < 0)
{
return false;
}
activeIndex = index;
AutoSelect = false;
return true;
}
public FaceVoiceEndpoint AddOrUpdate(string id, string threadId, Vector2 anchor, string? voice = null)
{
var normalized = FaceVoiceEndpoint.NormalizeId(id);
var endpoint = endpoints.FirstOrDefault(candidate => candidate.Id == normalized);
if (endpoint is null)
{
endpoint = new FaceVoiceEndpoint(
audio,
new AquariumFaceVoiceEndpointState
{
Id = normalized,
DisplayName = normalized,
ThreadId = threadId.Trim(),
Voice = string.IsNullOrWhiteSpace(voice) ? "marin" : voice.Trim(),
AnchorX = anchor.X,
AnchorY = anchor.Y
},
AppServerUri);
endpoints.Add(endpoint);
endpoint.UpdateSpatial(ListenerWorld, AudibleRadius);
activeIndex = endpoints.Count - 1;
AutoSelect = false;
return endpoint;
}
endpoint.ThreadId = threadId.Trim();
endpoint.Anchor = anchor;
if (!string.IsNullOrWhiteSpace(voice))
{
endpoint.Voice = voice.Trim();
}
endpoint.ApplySessionSettings(AppServerUri);
endpoint.UpdateSpatial(ListenerWorld, AudibleRadius);
activeIndex = endpoints.IndexOf(endpoint);
AutoSelect = false;
return endpoint;
}
public bool Remove(string id)
{
if (endpoints.Count <= 1)
{
return false;
}
var normalized = FaceVoiceEndpoint.NormalizeId(id);
var index = endpoints.FindIndex(endpoint => endpoint.Id == normalized);
if (index < 0)
{
return false;
}
endpoints[index].Dispose();
endpoints.RemoveAt(index);
activeIndex = Math.Clamp(activeIndex, 0, endpoints.Count - 1);
return true;
}
public void StartActive()
{
ActiveEndpoint.ApplySessionSettings(AppServerUri);
ActiveEndpoint.Session.Start();
}
public void StartAll()
{
foreach (var endpoint in endpoints.Where(endpoint => endpoint.Enabled))
{
endpoint.ApplySessionSettings(AppServerUri);
endpoint.UpdateSpatial(ListenerWorld, AudibleRadius);
endpoint.Session.Start();
}
}
public void StopActive()
{
ActiveEndpoint.Session.Stop();
}
public void StopAll()
{
foreach (var endpoint in endpoints)
{
endpoint.Session.Stop();
}
}
public void SendText(string text)
{
ActiveEndpoint.ApplySessionSettings(AppServerUri);
ActiveEndpoint.Session.SendText(text);
}
public void ClearActiveLog()
{
ActiveEndpoint.Session.ClearTranscript();
}
public List<AquariumFaceVoiceEndpointState> ExportState()
{
return endpoints.Select(endpoint => endpoint.ToState()).ToList();
}
public void Dispose()
{
foreach (var endpoint in endpoints)
{
endpoint.Dispose();
}
}
private int SelectNearestEndpointIndex(Vector2 listenerWorld)
{
var selected = activeIndex;
var selectedDistance = DistanceSquared(listenerWorld, endpoints[selected].Anchor);
for (var index = 0; index < endpoints.Count; index++)
{
var endpoint = endpoints[index];
if (!endpoint.Enabled)
{
continue;
}
var distance = DistanceSquared(listenerWorld, endpoint.Anchor);
if (distance + 1.0f < selectedDistance)
{
selected = index;
selectedDistance = distance;
}
}
return selected;
}
private static float DistanceSquared(Vector2 a, Vector2 b)
{
var delta = a - b;
return Vector2.Dot(delta, delta);
}
private static List<AquariumFaceVoiceEndpointState> CreateLegacyEndpointStates(AquariumLiveState state)
{
return
[
new AquariumFaceVoiceEndpointState
{
Id = "face",
DisplayName = "Face",
ThreadId = state.FaceVoiceThreadId,
Voice = state.FaceVoiceVoice,
Prompt = state.FaceVoicePrompt,
AnchorX = 0.0f,
AnchorY = 6.0f,
Enabled = true
}
];
}
}

View file

@ -1,514 +0,0 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aquarium.Engine.Audio;
namespace Aquarium.Epiphany.Voice;
public sealed class RealtimeFaceVoiceSession : IDisposable
{
private readonly object sync = new();
private readonly AquariumAudioDocument audio;
private readonly SemaphoreSlim sendLock = new(1, 1);
private ClientWebSocket? socket;
private CancellationTokenSource? cancellation;
private Task? task;
private int nextRequestId;
private string status = "idle";
private string transcript = "";
private string lastError = "";
private string remoteSdp = "";
private int outputAudioChunks;
private int outputAudioBytes;
private float outputGain = 0.85f;
private float outputPan;
public RealtimeFaceVoiceSession(AquariumAudioDocument audio)
{
this.audio = audio;
}
public string AppServerUri { get; set; } = "ws://127.0.0.1:8765";
public string ThreadId { get; set; } = "";
public string Voice { get; set; } = "marin";
public string Prompt { get; set; } = "You are Face inside Epiphany Aquarium. Speak briefly, warmly, and only as the public surface. Do not accept project state, memory, evidence, or code authority from speech.";
public string Status
{
get
{
lock (sync)
{
return status;
}
}
}
public string Transcript
{
get
{
lock (sync)
{
return transcript;
}
}
}
public string LastError
{
get
{
lock (sync)
{
return lastError;
}
}
}
public string RemoteSdp
{
get
{
lock (sync)
{
return remoteSdp;
}
}
}
public string AudioStats
{
get
{
lock (sync)
{
return $"{outputAudioChunks} chunks / {outputAudioBytes} bytes";
}
}
}
public bool IsRunning
{
get
{
lock (sync)
{
return socket?.State == WebSocketState.Open && status == "running";
}
}
}
public void SetOutputSpatial(float gain, float pan)
{
lock (sync)
{
outputGain = Math.Clamp(gain, 0.0f, 1.5f);
outputPan = Math.Clamp(pan, -1.0f, 1.0f);
}
}
public void Start()
{
lock (sync)
{
if (task is { IsCompleted: false })
{
status = "already running";
return;
}
if (string.IsNullOrWhiteSpace(AppServerUri) || string.IsNullOrWhiteSpace(ThreadId))
{
status = "blocked";
lastError = "app-server WebSocket URI and thread id are required";
return;
}
cancellation = new CancellationTokenSource();
task = Task.Run(() => RunAsync(cancellation.Token));
}
}
public void Stop()
{
ClientWebSocket? current;
CancellationTokenSource? currentCancellation;
lock (sync)
{
current = socket;
currentCancellation = cancellation;
status = "stopping";
}
_ = Task.Run(async () =>
{
try
{
if (current?.State == WebSocketState.Open)
{
await SendRequestAsync(current, "thread/realtime/stop", new JsonObject { ["threadId"] = ThreadId }, CancellationToken.None).ConfigureAwait(false);
await current.CloseAsync(WebSocketCloseStatus.NormalClosure, "Face voice stopped", CancellationToken.None).ConfigureAwait(false);
}
}
catch (Exception error)
{
SetError(error.Message);
}
finally
{
currentCancellation?.Cancel();
}
});
}
public void SendText(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return;
}
ClientWebSocket? current;
lock (sync)
{
current = socket;
}
if (current?.State != WebSocketState.Open)
{
SetError("Face voice is not connected");
return;
}
_ = Task.Run(async () =>
{
try
{
await SendRequestAsync(
current,
"thread/realtime/appendText",
new JsonObject
{
["threadId"] = ThreadId,
["text"] = text.Trim()
},
CancellationToken.None).ConfigureAwait(false);
}
catch (Exception error)
{
SetError(error.Message);
}
});
}
public void ClearTranscript()
{
lock (sync)
{
transcript = "";
lastError = "";
remoteSdp = "";
outputAudioChunks = 0;
outputAudioBytes = 0;
}
}
private async Task RunAsync(CancellationToken token)
{
using var localSocket = new ClientWebSocket();
lock (sync)
{
socket = localSocket;
status = "connecting";
lastError = "";
}
try
{
await localSocket.ConnectAsync(new Uri(AppServerUri), token).ConfigureAwait(false);
SetStatus("initializing");
var initializeId = await SendRequestAsync(
localSocket,
"initialize",
new JsonObject
{
["clientInfo"] = new JsonObject
{
["name"] = "aquarium-engine-face-voice",
["title"] = "Aquarium Engine Face Voice",
["version"] = "0.1.0"
},
["capabilities"] = new JsonObject
{
["experimentalApi"] = true
}
},
token).ConfigureAwait(false);
await ReceiveUntilResponseAsync(localSocket, initializeId, token).ConfigureAwait(false);
await SendNotificationAsync(localSocket, "initialized", null, token).ConfigureAwait(false);
SetStatus("starting realtime");
var startId = await SendRequestAsync(
localSocket,
"thread/realtime/start",
new JsonObject
{
["threadId"] = ThreadId,
["outputModality"] = "audio",
["prompt"] = Prompt,
["voice"] = string.IsNullOrWhiteSpace(Voice) ? null : Voice.Trim()
},
token).ConfigureAwait(false);
await ReceiveUntilResponseAsync(localSocket, startId, token).ConfigureAwait(false);
SetStatus("running");
await ReceiveLoopAsync(localSocket, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
SetStatus("stopped");
}
catch (Exception error)
{
SetError(error.Message);
}
finally
{
lock (sync)
{
if (ReferenceEquals(socket, localSocket))
{
socket = null;
}
if (status is "running" or "connecting" or "initializing" or "starting realtime" or "stopping")
{
status = "stopped";
}
}
}
}
private async Task ReceiveUntilResponseAsync(ClientWebSocket current, int requestId, CancellationToken token)
{
while (!token.IsCancellationRequested && current.State == WebSocketState.Open)
{
var message = await ReceiveMessageAsync(current, token).ConfigureAwait(false);
if (message is null)
{
return;
}
var responseId = message["id"]?.GetValue<int?>();
if (responseId == requestId)
{
if (message["error"] is JsonObject error)
{
throw new InvalidOperationException(error["message"]?.GetValue<string>() ?? "app-server request failed");
}
return;
}
HandleMessage(message);
}
}
private async Task ReceiveLoopAsync(ClientWebSocket current, CancellationToken token)
{
while (!token.IsCancellationRequested && current.State == WebSocketState.Open)
{
var message = await ReceiveMessageAsync(current, token).ConfigureAwait(false);
if (message is null)
{
return;
}
HandleMessage(message);
}
}
private void HandleMessage(JsonObject message)
{
if (message["error"] is JsonObject error)
{
SetError(error["message"]?.GetValue<string>() ?? "app-server error");
return;
}
var method = message["method"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(method))
{
return;
}
var parameters = message["params"] as JsonObject;
switch (method)
{
case "thread/realtime/started":
SetStatus("running");
break;
case "thread/realtime/transcript/delta":
AppendTranscript(parameters?["role"]?.GetValue<string>() ?? "voice", parameters?["delta"]?.GetValue<string>() ?? "");
break;
case "thread/realtime/transcript/done":
AppendTranscript(parameters?["role"]?.GetValue<string>() ?? "voice", parameters?["text"]?.GetValue<string>() ?? "", done: true);
break;
case "thread/realtime/outputAudio/delta":
HandleAudio(parameters?["audio"] as JsonObject);
break;
case "thread/realtime/sdp":
lock (sync)
{
remoteSdp = parameters?["sdp"]?.GetValue<string>() ?? "";
}
break;
case "thread/realtime/error":
SetError(parameters?["message"]?.GetValue<string>() ?? "realtime error");
break;
case "thread/realtime/closed":
SetStatus($"closed {parameters?["reason"]?.GetValue<string>() ?? ""}".Trim());
break;
}
}
private void HandleAudio(JsonObject? audioObject)
{
var data = audioObject?["data"]?.GetValue<string>() ?? "";
var sampleRate = audioObject?["sampleRate"]?.GetValue<int?>() ?? 24000;
var channels = audioObject?["numChannels"]?.GetValue<int?>() ?? 1;
if (!string.IsNullOrWhiteSpace(data))
{
float gain;
float pan;
lock (sync)
{
gain = outputGain;
pan = outputPan;
}
audio.EnqueuePcm16Base64(data, sampleRate, channels, gain, pan);
}
lock (sync)
{
outputAudioChunks++;
outputAudioBytes += data.Length;
}
}
private void AppendTranscript(string role, string text, bool done = false)
{
if (string.IsNullOrEmpty(text))
{
return;
}
lock (sync)
{
var label = string.Equals(role, "assistant", StringComparison.OrdinalIgnoreCase) ? "Face" : "You";
transcript += $"{label}: {text}{(done ? Environment.NewLine : "")}";
if (transcript.Length > 4096)
{
transcript = transcript[^4096..];
}
}
}
private async Task<int> SendRequestAsync(ClientWebSocket current, string method, JsonObject? parameters, CancellationToken token)
{
var id = Interlocked.Increment(ref nextRequestId);
var message = new JsonObject
{
["id"] = id,
["method"] = method
};
if (parameters is not null)
{
message["params"] = parameters;
}
await SendJsonAsync(current, message, token).ConfigureAwait(false);
return id;
}
private Task SendNotificationAsync(ClientWebSocket current, string method, JsonObject? parameters, CancellationToken token)
{
var message = new JsonObject
{
["method"] = method
};
if (parameters is not null)
{
message["params"] = parameters;
}
return SendJsonAsync(current, message, token);
}
private async Task SendJsonAsync(ClientWebSocket current, JsonObject message, CancellationToken token)
{
var bytes = Encoding.UTF8.GetBytes(message.ToJsonString());
await sendLock.WaitAsync(token).ConfigureAwait(false);
try
{
await current.SendAsync(bytes, WebSocketMessageType.Text, true, token).ConfigureAwait(false);
}
finally
{
sendLock.Release();
}
}
private static async Task<JsonObject?> ReceiveMessageAsync(ClientWebSocket current, CancellationToken token)
{
var buffer = new byte[64 * 1024];
using var stream = new MemoryStream();
while (true)
{
var result = await current.ReceiveAsync(buffer, token).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{
return null;
}
stream.Write(buffer, 0, result.Count);
if (result.EndOfMessage)
{
break;
}
}
var json = Encoding.UTF8.GetString(stream.ToArray());
return JsonNode.Parse(json) as JsonObject;
}
private void SetStatus(string value)
{
lock (sync)
{
status = value;
}
}
private void SetError(string message)
{
lock (sync)
{
status = "error";
lastError = message;
}
}
public void Dispose()
{
cancellation?.Cancel();
socket?.Dispose();
cancellation?.Dispose();
sendLock.Dispose();
}
}

View file

@ -1,9 +0,0 @@
namespace Aquarium.Epiphany;
public static class Angle
{
public static float DegreesToRadians(float degrees)
{
return degrees * (MathF.PI / 180.0f);
}
}

View file

@ -1,116 +0,0 @@
using System.Numerics;
using Aquarium.Engine.Input;
namespace Aquarium.Epiphany;
public sealed class OrbitCameraRig
{
public const float MinimumDistance = 5.0f;
public const float MaximumDistance = 40.0f;
public OrbitCameraRig(Vector2 target, float yawRadians, float pitchRadians, float distance)
{
Target = target;
YawRadians = yawRadians;
PitchRadians = pitchRadians;
Distance = ClampDistance(distance);
}
public Vector2 Target { get; private set; }
public float YawRadians { get; private set; }
public float PitchRadians { get; private set; }
public float Distance { get; private set; }
public Vector3 Position
{
get
{
var horizontal = Distance * MathF.Cos(PitchRadians);
var x = Target.X + horizontal * MathF.Cos(YawRadians);
var y = Target.Y + horizontal * MathF.Sin(YawRadians);
var z = Distance * MathF.Sin(PitchRadians);
return new Vector3(x, y, z);
}
}
public void Orbit(float yawDeltaRadians, float pitchDeltaRadians)
{
YawRadians += yawDeltaRadians;
PitchRadians = Math.Clamp(
PitchRadians + pitchDeltaRadians,
Angle.DegreesToRadians(5.0f),
Angle.DegreesToRadians(85.0f));
}
public void Zoom(float wheelSteps)
{
Distance = ClampDistance(Distance * MathF.Pow(0.92f, wheelSteps));
}
public void Pan(Vector2 gridDelta)
{
Target += gridDelta;
}
public void ApplyInput(InputState input, float deltaSeconds)
{
if (input.MiddleMouseDown)
{
Orbit(input.MouseDelta.X * -0.0065f, input.MouseDelta.Y * 0.0065f);
}
if (MathF.Abs(input.WheelDelta) > 0.0f)
{
Zoom(input.WheelDelta);
}
var keyboardPan = Vector2.Zero;
if (input.IsKeyDown(KeyCode.W))
{
keyboardPan.Y += 1.0f;
}
if (input.IsKeyDown(KeyCode.S))
{
keyboardPan.Y -= 1.0f;
}
if (input.IsKeyDown(KeyCode.D))
{
keyboardPan.X += 1.0f;
}
if (input.IsKeyDown(KeyCode.A))
{
keyboardPan.X -= 1.0f;
}
if (keyboardPan != Vector2.Zero)
{
Pan(GridPanVector(Vector2.Normalize(keyboardPan) * Distance * 0.45f * deltaSeconds));
}
if (input.RightMouseDown && input.MouseDelta != Vector2.Zero)
{
Pan(GridPanVector(new Vector2(-input.MouseDelta.X, input.MouseDelta.Y) * Distance * 0.0018f));
}
}
private Vector2 GridPanVector(Vector2 localDelta)
{
var forward = Vector2.Normalize(new Vector2(
MathF.Cos(YawRadians + MathF.PI),
MathF.Sin(YawRadians + MathF.PI)));
var right = new Vector2(forward.Y, -forward.X);
return right * localDelta.X + forward * localDelta.Y;
}
private static float ClampDistance(float distance)
{
return Math.Clamp(distance, MinimumDistance, MaximumDistance);
}
}

View file

@ -211,3 +211,4 @@
{"timestamp":"2026-05-21T11:42:16+01:00","topic":"fensalir-splash-ripple-scene","lesson":"The first Fensalir art-target client proves a cleaner scene reproduction boundary than more abstract splat demos: the .aquageo patch owns the marsh surface and ripple brushes, FensalirSceneBuilder publishes those brushes plus one bounded SDF spine proxy, and client shaders own the dark reflective water/mist and luminous stacked-diamond hall silhouette. Aetheria's transferable primitive is the compact radial wave splat: envelope * powered cosine, not a bespoke water mesh. Missing future work is now visible at the right boundary: richer material/page DSL for water color/roughness, transparent fog density fields, and an IFS-owned spine/pillar grammar instead of a hand-authored first proxy shader.","files":["src/Aquarium.Fensalir/Worlds/fensalir-spine-emblem.aquageo","src/Aquarium.Fensalir/FensalirRuntime.cs","src/Aquarium.Fensalir/FensalirSceneBuilder.cs","src/Aquarium.Fensalir/Shaders/D3D12FensalirScene.hlsl","src/Aquarium.Fensalir/Shaders/D3D12FensalirSpine.hlsl","src/Aquarium.Engine.Fractal/Grammar/FractalDslCompiler.cs","src/Aquarium.Engine.Fractal/Brushes/FractalHeightBrushCompiler.cs","src/Aquarium.Engine.Contracts/Fractal/AquariumFractalContracts.cs","tests/Aquarium.Engine.Fractal.Tests/FensalirFractalSceneTests.cs","state/map.yaml","state/memory.json"],"source":"user pivoted from abstract Perfect Machine work to reproducing the documented Fensalir splash art as an actual DSL/rendering target"}
{"timestamp":"2026-05-22T01:20:00+01:00","topic":"fensalir-fixed-headless-capture","lesson":"Fensalir now has a repeatable headless frame-capture lane for art-target iteration. AquariumHost accepts --capture-frame plus headless width/height, suppresses overlay debug UI while headless, waits for ready frames, and saves the last presented backbuffer through IAquariumRenderer.SaveFramePng. scripts/capture-fensalir-frame.ps1 builds disposable engine/client slots and uses Start-Process with a timeout because Aquarium.Engine is a Windows GUI exe and plain PowerShell invocation can return before rendering is complete. The latest capture, artifacts/fensalir-captures/fixed-cathedral-forward.png, proves the current gap honestly: the cathedral silhouette is now visible and DSL patch claims drive surface/cathedral fields, but the spine/pillar grammar is still partly shader-authored and needs a real IFS-owned structural lowering before the splash reproduction stops being a hand-guided proxy.","files":["src/Aquarium.Engine/AquariumHost.cs","scripts/capture-fensalir-frame.ps1","src/Aquarium.Fensalir/FensalirRuntime.cs","src/Aquarium.Fensalir/FensalirSceneBuilder.cs","src/Aquarium.Fensalir/Shaders/D3D12FensalirScene.hlsl","src/Aquarium.Fensalir/Shaders/D3D12FensalirSpine.hlsl","src/Aquarium.Fensalir/Worlds/fensalir-spine-emblem.aquageo","tests/Aquarium.Engine.Fractal.Tests/FensalirFractalSceneTests.cs","state/map.yaml","state/memory.json"],"source":"user asked to exercise the cathedral through the DSL and set up fixed-camera headless capture for unattended renderer iteration"}
{"timestamp":"2026-05-22T01:32:00+01:00","topic":"fensalir-cathedral-guide-is-scaffolding","lesson":"The fixed capture made the visual failure impossible to dodge: Fensalir's previous shader/SDF mix had the nouns of the splash but not the image. A temporary screen-space cathedral guide now exposes the target composition more clearly in artifacts/fensalir-captures/fixed-cathedral-guide-clean.png, while SDF authority is cut back to the central spine/reeds instead of traced pillar rods fighting atmospheric glass. This guide is not final architecture. The next coherent slice is a separate `.aquageo`-lowered transparent pane/rune/material field target, distinct from the marsh height texture, so the cathedral glass, waterline glow, rune dust, and material confidence are authored field claims rather than hardcoded screen composition.","files":["src/Aquarium.Fensalir/Shaders/D3D12FensalirScene.hlsl","src/Aquarium.Fensalir/Shaders/D3D12FensalirSpine.hlsl","src/Aquarium.Fensalir/FensalirSceneBuilder.cs","state/map.yaml","state/memory.json"],"source":"user correctly mocked the prior receipt as visually nowhere near the provided splash reference"}
{"timestamp":"2026-05-23T00:00:00+01:00","topic":"fensalir-repo-split","lesson":"Fensalir now has its own adjacent repo at E:\\Projects\\Fensalir and owns the native host, renderer, contracts, fractal machinery, audio host, reload scripts, and engine demos. Epiphany Aquarium moved out to E:\\Projects\\Aquarium-Engine as a client repo that references Fensalir source projects until packaging exists. Keep dependency direction one-way: client repos may depend on Fensalir; Fensalir must not depend on client policy nouns.","files":["Fensalir.sln","README.md","AGENTS.md","state/map.yaml","state/memory.json"],"source":"user directed Fensalir and engine demos into a new adjacent repo while Aquarium-Engine remains only the Epiphany Aquarium client"}

View file

@ -1,49 +0,0 @@
# Life Agent SDF Notes
Reference: `artifacts/ig_0b80263584b1a81e016a051475aeac8191acb0ced03b9a3e7e.png`.
Supershape shell reference: `D:/Downloads/supershape-2026-05-14-0437-09.json`.
Working model:
- The readable form is a memory nautilus, not a generic seed.
- An ellipsoid silhouette with spiral material masks reads as wallpaper and fails the reference.
- A disc/cup welded onto the shell is worse: it enshrines the failed cut-face model.
- The source-backed model comes from `artifacts/Nautilus.glsl`: solve the nearest logarithmic spiral boundary in cylindrical shell space, then derive wall thickness, septa, aperture/cut exposure, ribs, stripes, lip, pearls, cracks, and chamber light from that solved domain.
- Do not replace the solve with a loose `sin(phase)` whorl. That was the false shortcut.
- Do not recreate the Shadertoy Nautilus. Its durable lesson is spatial-domain mapping, not the Life silhouette.
- For Life's C mantle, do not cut a closed Nautilus section circle with an angular wedge. A wedge intersection creates planar cut walls and umbrella fins. The C profile must be a distance to an arc in the solved section circle: inside the kept angular span, distance to the circle; outside it, distance to the nearest arc endpoint/contact region.
- Do not carry over the Nautilus aperture/opening stabilizer as an independent torus-like fallback in the Life C-arc topology. In the source shader it patches a closed-shell discontinuity; in Life it becomes a giant false shell that overwhelms the actual spiral branch solve.
- Life's C profile radius should not default to the full Nautilus section-circle radius. To make the mantle start near the ember equator and build on itself, derive the local C arc radius from previous-turn contact: with current whorl center radius R, previous-turn ratio k=exp(-B*TAU), and endpoint angle halfSpan, solve R + a*cos(halfSpan) = k*R + k*a for profile radius a. Keep this as domain math, not a bridge pasted on after the fact.
- Correction to the Life C-contact math: exact endpoint contact with the previous turn's section circle is possible only if cos(endpointAngle)^2 >= 1-k^2, where k=exp(-B*TAU). If the kept C span is too narrow for the growth rate, gaps are mathematically unavoidable. The shader playground should surface this by deriving/clamping the feasible half-span and solving the real section-circle contact equation, not by adding a fake bridge.
- Further correction: the Life mantle section does not have to grow at the same exponential rate as the spiral centerline. Keep separate exponents for centerline growth (`B`) and C-profile growth (`PROFILE_B`). The previous-turn profile ratio is `kp=exp(-PROFILE_B*TAU)`, which belongs in the endpoint contact solve; assuming `kp==k` overconstrains the form and can force persistent gaps.
- Bug in the first separate-profile attempt: adding `PROFILE_B` to the contact equation is meaningless unless the actual C profile radius field uses it. Define a profile radius field such as `PROFILE_END_RADIUS * exp(PROFILE_B*u)`, solve current endpoint contact against the previous turn's actual profile radius, and consider adjacent branch candidates beyond floor/ceil because the nearest closed-circle branch may not be the nearest C-arc branch.
- Better Life profile parameterization: do not define profile radius as `PROFILE_END_RADIUS * exp(PROFILE_B*u)`, because setting the terminal amplitude to zero makes the whole field zero and `PROFILE_B` inert. Use a seed/ember radius plus age-based growth, e.g. `PROFILE_START_RADIUS * exp(PROFILE_B * (u + TURNS*TAU))`, with terminal flare as an additive option. Contact solve should fall back to the base profile when exact contact is infeasible rather than widening into a pillbug.
- Contact must be solved surface-to-surface, not centerline-to-centerline. If the current C endpoint is placed on the previous turn's profile centerline, the wall radii interpenetrate and any seam placed there will expose the lie. Solve endpoint distance against `previousProfile + previousWall + currentWall`, refine current wall thickness once, and keep the endpoint geometry in the same angular frame as the solve. Terminal flare should be released from contact pull so `PROFILE_END_RADIUS` can actually widen the mouth instead of being cancelled by the previous-turn constraint.
- Further Life C-contact correction: never force contact by widening the C span toward a nearly closed circle. That only turns the section into an overlapping pillbug. The previous turn in the contact equation must be the actual solved previous profile radius, not `profileBaseRadius(u - TAU)`. Evaluate the finite same-phase growth lineage and solve each turn against the solved turn before it.
- Missing Life C-contact degree of freedom: a fixed C arc centered on the spiral whorl cannot generally touch the previous turn for the desired half-open span. `PROFILE_B` cannot fix an impossible triangle; below the spiral growth it stalls, above it grows but gaps. The coherent grammar is radius for fullness plus a solved section-space profile-center offset for contact. Solve `|vec2(D + oCur - oPrev,0) + radius*endpoint| = previousRadius + previousWall + currentWall`; do not fake contact by changing span.
- Life C-profile feasibility: once contact is solved by x-offset, radius is constrained by the endpoint's perpendicular component. If `radius * abs(sin(endpointAngle)) > previousRadius + previousWall + currentWall`, no x-offset can create contact. Clamp or derive profile radius inside that envelope before solving offset; otherwise high `PROFILE_B` explodes and low settings appear inert. Do not use a terminal release blend, because it breaks the seam continuity the end radius is supposed to preserve.
- Life C-profile recurrence must not anchor at the first visible loop. A variable-generation recurrence (`floor((u + TURNS*TAU)/TAU)`) creates a branch cut every revolution: the innermost loop grows naturally until it hits the clamped origin, then each later loop inherits the same discontinuity. Use fixed prehistory for every `u` and walk forward the same number of turns so there is no special visible origin.
- Fixed prehistory is also wrong for Life C-profile contact: it removes the branch cut by converging every visible turn toward the same mature solved profile, producing a tire/pastry. The better model is closed-form self-similarity: express profile fullness relative to spiral radius and solve offset directly with `k=exp(-B*TAU)`, e.g. `|vec2((1-k)*R*(1+beta),0)+radius*endpoint| = previousRadius + previousWall + currentWall`, then `offset=beta*R`. No recurrence, no clamped origin, no inherited mature profile.
- Retreat from the overfit contact solves: the readable fixed-center C profile was the right visual base even when it intersected. The coherent non-overlap rule is accretion ownership: newer shell may meet older shell but may not occupy its volume. Restore the readable fixed-center contact radius and trim each branch by the previous turn's signed volume (`current minus previous`) so the seam is an actual boolean boundary instead of another fragile radius/offset compensator.
- The readable overlapping model specifically included contact pull: widen the C span to the minimum endpoint-contact-feasible span, then pull the fixed-center profile radius toward the previous-turn contact solve. If that is omitted, parameter changes can produce a non-overlapping gap model instead of the useful raw overlap that the accretion trim is supposed to cut.
- The spec authority is a protective revolved seed/cardioid shell around a visible ember. The Nautilus spiral math should only coordinate seam, bead trail, aperture logic, and surface breathing.
- Lip, pearls, ribs, cracks, and chamber light are fields on the same shell/spiral coordinates, not separate child objects.
- Concept target: front view should show a heavy teal shell mass with a round outer whorl, a gold inner aperture bowl on the left, a visible ember near the inner spiral, pale raised ribs arcing from the center to the outer shell, and memory beads along the spiral lip. Oblique views may reveal thickness, but must not expose detached ornaments or a hollow ring.
- Working checkpoint as of `20260514-163152`: build Life in ordered layers. First solve the log spiral curve until it reads alone: ember at the core, about two turns, and a wide lip termination on the mouth side. Then inflate that curve into shell mass, carve the aperture from the same mouth field, reveal the ember, and only then add ribs/pearls as masks on the solved spiral domain.
- Cut line: if a future pass tries to tune ornament before the curve reads, stop. If the shell becomes an egg with spiral decals, delete it. If the spiral alone becomes a hollow C-shell, add support from the same domain instead of welding on separate organs.
- Aquarium placement correction: the Life spiral plane belongs in world `YZ`, with shell profile thickness along world `X`. In the preview set, `life-right` is the lip-side `Y+` camera, so use `life-left` as the reference comparison view when matching the concept art's visible spiral face. Do not fake the mouth by trimming the shell against the previous turn's volume; that cuts along the spiral plane, which is not where the biological aperture lives.
- Life material/detail correction: pearl ribs must be raised circular-section distance fields on the shell surface, not painted color masks. Inner shell material should be nacre/pearl with warped Voronoi gold fractures; the same fracture field can tint the teal enamel outside. Failed hypothesis: closing the C span near the terminal turn creates a false lip-side blade and should not be kept.
- Life containment correction: removing the terminal `u` bound before a correct C-span taper exists lets the final branch run into giant nacre fins. Keep the terminal bound as a temporary containment invariant until the mouth profile is solved by the C-domain itself.
- Rollback checkpoint: after the 2026-05-15 failed mouth/profile detour, the source shader was restored to the live-reload shader from `artifacts/dev-reload/slots/20260515-145843-490c3cac/Render/Shaders/D3D12LifeAgent.hlsl`. Treat that as the current coherent baseline before attempting more Life mouth work.
Latest active checkpoint:
- `artifacts/agent-previews/Life/20260514-193811/life-front.png`
- `artifacts/agent-previews/Life/20260514-193811/life-mouth-oblique.png`
- `artifacts/agent-previews/Life/20260514-193811/life-three-quarter.png`
Current judgment:
- The active source is the last accepted pre-detour C-sweep shader, not the later scaled-inverse/profile-rewrite experiments.
- Do not reintroduce the rounded terminal-overrun cap that replaced `d = max(d, u - LIFE_TERMINAL_U)`. It was the only diff between the restored source and the actual last known pre-nightmare build, and it did not earn its keep.
- The current form is not final. The next productive move is a narrow, reference-checked mouth/lip pass from this baseline, with the actual `Life_C_Sweep_ShaderToy.glsl` playground in sight.

View file

@ -1,232 +1,45 @@
project:
name: Aquarium Engine
root: E:\Projects\Aquarium-Engine
upstream: https://github.com/GameCult/Aquarium-Engine
name: Fensalir
root: E:\Projects\Fensalir
purpose: >
Native C# runtime and renderer core for Aquarium clients. The engine owns
the Win32 host, D3D12 renderer, input loop, client reload boundary, debug UI,
audio host, persistent-state transport, and future render graph
abstractions. Epiphany is the current first-party client: it owns frontend
intent, agent visuals, CultCache/CultNet semantics, and render-plan
configuration through the engine contracts.
Native C# runtime and renderer dependency for Aquarium-style clients. The
engine owns Win32 hosting, D3D12 rendering, input, hot reload, debug UI,
audio output, client contracts, fractal/field machinery, and engine demos.
current_model:
solution: Fensalir.sln
host:
solution: Aquarium.Engine.sln
window: src/Aquarium.Engine/Platform/Win32Window.cs
renderer: src/Aquarium.Engine/Render/D3D12Renderer.cs
height_field_shader: src/Aquarium.Engine/Render/Shaders/D3D12HeightField.hlsl
scene_shader: src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl
sdf_common_include: src/Aquarium.Engine/Render/Shaders/D3D12SdfCommon.hlsli
sdf_proxy_include: src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli
sdf_math_include: src/Aquarium.Engine/Render/Shaders/D3D12SdfMath.hlsli
post_shader: src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl
overlay_text: src/Aquarium.Engine/Render/DirectWriteOverlay.cs
debug_ui: src/Aquarium.Engine/Render/Ui/DebugUi.cs
synth_host: src/Aquarium.Engine/Audio/AquariumSynthHost.cs
wasapi_audio_device: src/Aquarium.Engine/Audio/WasapiAudioDevice.cs
faust_toolchain_sync: scripts/sync-faust-toolchain.ps1
synth_package_update: scripts/update-synth-package.ps1
local_package_feed: packages
runtime_loader: src/Aquarium.Engine/Runtime/ClientRuntimeLoader.cs
dev_reload: scripts/dev-reload.ps1
dev_watch: scripts/dev-watch.ps1
epiphany_client:
implementation: src/Aquarium.Engine
contracts: src/Aquarium.Engine.Contracts
implementation: src/Aquarium.Epiphany
render_plan: src/Aquarium.Epiphany/EpiphanyRenderPlan.cs
scene_builder: src/Aquarium.Epiphany/EpiphanySceneBuilder.cs
agent_preview: src/Aquarium.Epiphany.AgentPreview
shaders: src/Aquarium.Epiphany/Shaders
state_store: src/Aquarium.Epiphany/State/AquariumCultStateStore.cs
state_document: epiphany.aquarium.live_state
graphics_settings_document: epiphany.aquarium.graphics_settings
zyphos_client:
implementation: src/Aquarium.Zyphos
render_plan: src/Aquarium.Zyphos/ZyphosRenderPlan.cs
scene_builder: src/Aquarium.Zyphos/ZyphosSceneBuilder.cs
shaders: src/Aquarium.Zyphos/Shaders
worldbuilding_sync: docs/zyphos-eusocial-sync.md
setting_authority: E:\Projects\Eusocial Interbeing
purpose: >
Non-Epiphany planetary-scale rendering demo using the shared Aquarium host,
D3D12 renderer, reload transport, UI contracts, bloom, and SDF proxy path.
fensalir_client:
implementation: src/Aquarium.Fensalir
render_plan: src/Aquarium.Fensalir/FensalirRenderPlan.cs
scene_builder: src/Aquarium.Fensalir/FensalirSceneBuilder.cs
shaders: src/Aquarium.Fensalir/Shaders
world_patch: src/Aquarium.Fensalir/Worlds/fensalir-spine-emblem.aquageo
splash_prompt: research/rendering/fensalir-spine-emblem-ifs-demo.md
ground_truth_image: src/Aquarium.Engine/Assets/Fensalir-Splash.png
fixed_capture: scripts/capture-fensalir-frame.ps1
latest_capture: artifacts/fensalir-captures/fixed-cathedral-guide-clean.png
purpose: >
First artistic Fensalir reconstruction client: loads a .aquageo scene
patch, lowers marsh/water surface claims into shaped 2D ripple brushes,
projects cathedral detail from the patch-owned 2D field, and renders the
central luminous stacked-diamond spine through the normal SDF proxy path.
samples:
minimal_client: src/Aquarium.Sample.Minimal
renderer: src/Aquarium.Engine/Render
shaders: src/Aquarium.Engine/Render/Shaders
assets: src/Aquarium.Engine/Assets
scripts: scripts
fractal:
implementation: src/Aquarium.Engine.Fractal
tests: tests/Aquarium.Engine.Fractal.Tests
receipt_tool: tools/Aquarium.Fractal.Receipt
demos:
fensalir_splash: src/Aquarium.Fensalir
minimal: src/Aquarium.Sample.Minimal
zyphos: src/Aquarium.Zyphos
localcast: src/Aquarium.LocalCast
external_clients:
epiphany_aquarium: E:\Projects\Aquarium-Engine
dependencies:
cult_math: E:\Projects\CultMath
cult_lib: E:\Projects\CultLib
aqua_synth_faust: NuGet package AquaSynth.Faust 0.1.0-alpha.3 from repo-local packages feed
aqua_synth_core: transitive NuGet package AquaSynth.Core 0.1.0-alpha.3 from repo-local packages feed
docs:
doctrine: docs/aquarium-engine-doctrine.md
engine_client_boundary: docs/engine-client-boundary.md
epiphany_extraction_render_api_plan: docs/epiphany-extraction-render-api-plan.md
renderer: docs/hlsl-renderer.md
agent_sdf_visual_language: docs/epiphany-agent-sdf-visual-language.md
runtime_surface: docs/cult-runtime-surface.md
native_spine: docs/vortice-spine.md
tsr_inspired_taa_spec: docs/tsr-inspired-taa-spec.md
temporal_sdf_gaussian_field: docs/temporal-sdf-gaussian-field.md
temporal_spatial_evidence_reservoir: docs/temporal-spatial-evidence-reservoir.md
input: docs/input.md
zyphos_eusocial_sync: docs/zyphos-eusocial-sync.md
hdr_pbr_presentation: research/rendering/hdr-pbr-presentation.md
cube_sphere_fractal_brushes: research/rendering/cube-sphere-fractal-brushes.md
fractal_brush_architecture_plan: research/rendering/fractal-brush-architecture-plan.md
fractal_brush_implementation_roadmap: research/rendering/fractal-brush-implementation-roadmap.md
perfect_machine_architecture: docs/perfect-machine-architecture.md
fractal_reference_oracles: research/rendering/fractal-reference-oracles.md
aqua_synth_faust: repo-local NuGet package feed under packages
invariants:
- The camera target is the Grid center on the global XY plane.
- Grid radius follows twice the clamped camera zoom distance, then clamps to [20, 80]; pitch/yaw/projection do not resize it.
- Body anchors remain world-space and independent of the moving Grid window.
- Visible scene lighting is diegetic; no default ambient/global fill.
- Overlay text is DirectWrite final-pixel UI; scene text needs renderer-owned billboards.
- CultCache persists live app state through reload.
- Global GraphicsSettings persists renderer debug/HDR presentation knobs through restart and live reload.
- Failed shader/live-runtime reloads must leave the last good frame alive.
- Closed visible dev windows can be reopened from the last good slot without rebuilding.
renderer_state:
current:
- D3D12 default renderer behind IAquariumRenderer with explicit device/queue/swapchain, command allocators, fences, descriptor arenas, upload ring constants, tracked render targets, command events, resize rebuild, HDR/bloom/present, temporal diagnostics, and DirectWrite overlay bridge.
- Deferred scalar Grid height target rendered from additive brush passes.
- Epiphany owns its client surface brush list, SDF object table, SDF light table, shader manifest, and cursor projection policy through Aquarium.Engine.Contracts; the engine consumes those as generic rendering data.
- Direct-traced height-field surface is a terminating reflective surface over the scalar height field, sampling Studio2 PMREM mip 0.
- Fullscreen scene pass owns background and height-field surface only; each client-declared SDF shader owns a separate proxy pipeline, with shared proxy plumbing factored into generic engine HLSL includes. Proxy marching evaluates only sdfDistance; sdfSurface runs once at the refined hit and returns only PBR material values.
- Reusable SDF primitives live in engine D3D12SdfMath.hlsli; current Epiphany role character organs live under src/Aquarium.Epiphany/Shaders.
- Epiphany's cursor is a luminous five-petal hibiscus SDF centered one cursor radius above the XY plane so its contact tip reaches Grid z=0 without projecting to the Grid surface.
- SDF/analytic Epiphany role visuals upload current centers, previous centers, and state scalars through the public `AquariumSdfObject` contract.
- Agent SDF visual language is documented against Epiphany's published CultNet surfaces: identity from agent memory, motion from heartbeat, posture from role/coordinator/result surfaces, pressure from reorient/pressure surfaces, and expression through bounded orbit wandering plus CPU-visible gravity-well pulses.
- Life is a memory-nautilus seed-shell SDF: a protective revolved seed/cardioid shell owns the silhouette, a visible ember sits inside the aperture, and the logarithmic spiral domain owns seam, bead trail, pressure breathing, rupture scar, and surface masks. Nautilus references inform the coordinate mapping, not the object silhouette.
- SDF light buffer feeds Self emission and direct object lighting; future other Selves should join this light lane, not a generic field-instance table.
- Studio2 PMREM and irradiance DDS cubemaps are loaded as D3D12 TextureCubes; PMREM serves roughness-mip specular, irradiance serves Lambertian diffuse IBL for non-metal bodies.
- DirectWrite/Direct2D unified debug panel with tabs for Aquarium renderer controls, client-declared debug panels, terminal commands, and engine/client tooling surfaces.
- DirectWrite/Direct2D debug panels own body scrolling and clipping; overflowing client controls must stay inside the panel with a visible scrollbar.
- Client-declared UI panels are folded into the unified Debug tab strip by default; panels that opt into proximity fading render as independent floating overlays. Floating panels can right-anchor with negative left offsets and fade to 10% opacity when the mouse is not near.
- Aquarium UI exposes generic selectable tree rows with disclosure arrows. Clients use them for hierarchy navigation without teaching DebugUi about client-specific spatial domains.
- Aquarium exposes an optional synth/audio hosting boundary through AquariumSynthDocument; Aquarium consumes AquaSynth.Faust as a pinned repo-local NuGet package so synth-agent construction cannot break Aquarium builds until the package is intentionally updated. AquaSynth.Core arrives transitively for patch editor/parser surfaces. AquaSynth owns live patch compilation and native Faust factory lifetime; Aquarium owns debounce, playback scheduling, cached rendered buffers, and WASAPI output.
- Aquarium now exposes a generic queued PCM audio lane through AquariumAudioDocument. Epiphany Face voice uses it to play streamed Codex realtime output without reaching into the engine's WASAPI internals.
- Epiphany owns the first native Face realtime voice session controller. It stores loopback app-server URI, thread id, voice, and speech prompt in CultCache live state; starts `thread/realtime/start` with audio output over the experimental Codex app-server WebSocket JSON-RPC edge; sends text utterances; reflects ephemeral transcript/error/audio counters in the debug UI; and treats speech as transient Face surface, not durable memory or project authority.
- Face voice routing is no longer a singleton. Epiphany persists typed FaceVoiceEndpoint rows with id, display name, thread id, voice, prompt, enabled flag, XY proximity anchor, and an audible-radius scale. FaceVoiceRouter uses proximity only to choose the addressed endpoint for user input; enabled endpoints can all run concurrently so active rumination loops may speak regardless of listener distance. Distance shapes PCM output gain and stereo pan through AquariumAudioDocument/WasapiAudioDevice.
- Global GraphicsSettings document feeding render debug, exposure, bloom intensity, and bloom veil.
- Temporal resolve uses scene color, metadata, and control history only; dormant event history lanes are cut until a real producer and consumer contract exists.
- Aquarium owns the temporal SDF Gaussian field renderer boundary: clients can publish `AquariumTemporalGaussianField`, `TemporalGaussianAccumulator` can buffer stable keyed observations in world space, and D3D12 lowers those packets into an instanced compact-kernel splat pass that writes normal scene color/travel/metadata/control targets.
- Temporal Gaussian accumulation now lowers through `TemporalSpatialEvidenceReservoir`, a renderer-agnostic stable-key reservoir for confidence-weighted spatial samples with presentation delay, smoothing, velocity prediction, expiry, and max-track eviction. Evidence observations and samples carry explicit Form/Appearance/Transport layer and encoding metadata, and track identity is keyed by layer+encoding+stable key so opaque SDF and transparent density claims share the reservoir machinery without being blended into one track.
- LocalCast fallback GPU fusion history delegates to `TemporalSpatialEvidenceReservoir`. `LocalCastGpuFusionAccumulator` is now an adapter that maps LocalCast stable seeds into shared evidence observations and back into GPU fusion seeds; it does not own a parallel temporal sampling algorithm.
- The temporal reservoir ownership contract lives in docs/temporal-spatial-evidence-reservoir.md. `ResampledImportanceReservoir<TSample>` owns RIS/ReSTIR-style candidate selection math: selected sample, target value, weight sum, represented candidate count, merge, and contribution weight. Raw producer reservoirs own capture-time retention; Aquarium's temporal spatial evidence track layer owns stable resolved spatial history; lowerings own backend packet conversion; TAA owns pixel history validation.
- Isolated Epiphany agent preview renders run through the real D3D12 renderer from src/Aquarium.Epiphany.AgentPreview; preview isolation is owned by a preview-only scene shader, while the engine exposes only final-frame PNG capture.
- Zyphos is the first nontrivial non-Epiphany client: a planetary-scale demo with spherical-domain terrain, atmospheric rim lighting, night-side glints, fixed-sky Umbros, and first-pass eclipse cadence.
- Zyphos/Umbros render-scale binary constants live in src/Aquarium.Zyphos/ZyphosUmbrosSystem.cs: Umbros is modeled as a 0.9-radius mutually tidally locked twin at 8 Zyphos radii, while the dim primary star phase moves through the Zyphos-local frame.
- Zyphos simulates the neighboring Eusocial Interbeing setting; the vault at E:\Projects\Eusocial Interbeing owns canon, while Aquarium owns demo rendering constraints and feeds worldbuilding questions back through the vault's Zyphos Simulation Brief.
- Future planetary terrain and fractal body detail should use a cube-sphere/QSC-family address space: six quadtree cube faces projected to the sphere with a measured low-distortion warp, avoiding lat-long pole singularities.
- Fractal world patches can declare nested semantic domains before terrain tiles. The first .aquageo patch declares solar, orbital, planetary, lat/long, and cube-tile domains for both Zyphos and Umbros so DSL nodes can attach to explicit reference frames before backend lowering.
- The fractal grammar layer validates nested domain topology through FractalDomainGraph: duplicate keys, missing parents, cycles, and root-to-leaf paths are grammar concerns, not renderer folklore.
- Aquarium frames now carry a 3D camera target in addition to the legacy 2D ViewFrame center. Zyphos uses a camera composer to separate tracked subject, parent composition anchor, and camera orbit distance.
- Zyphos camera navigation is domain-key driven: selecting any solar/orbital/planetary/surface/object/tile domain in the floating Navigation tree changes the active camera mode, derives the parent pivot from the domain hierarchy, and lets mouse drag/wheel orbit or zoom that selected spatial frame. Zyphos and Umbros both expose lat/long surface domains, recursive subdomains, object-scale domains, and cube-tile children.
- Zyphos domain zoom is scale-aware: leaf/pebble domains can drive the camera below planetary distances, while larger domains retain broad framing. The parent star is a rendered SDF object as well as a light source, so selecting the Solar domain targets a visible sun instead of an invisible lamp.
- The current height-field stress backend uploads a fixed table of 64 shaped 2D brushes to D3D12. The richer Zyphos/Umbros .aquageo patch currently emits 61 brushes across recursive continent/forest/leaf/flame/coast/pebble and Umbros crater/boulder/pebble domains.
- Zyphos and Umbros SDF planet shaders include cheap bounded quadtree-style layered relief in object space, separate from the flattened 2D height-brush stress path.
- SceneState can disable the legacy height-field surface trace and request a procedural starfield background; Zyphos uses this for space views so the old Grid does not leak into planetary rendering.
- Future multiscale terrain and agent/body detail should come from bounded IFS brush grammars using shared shaped anisotropic envelopes; texture/material detail can be abundant, but traced SDF form detail must be proxy-bounded, LOD-gated, and measured.
- The IFS brush grammar should preserve a semantic ownership tree before backend lowering: local frames, deterministic seeds, named fields/captures, tags, payload bindings, LOD/cost tiers, and boolean-style union/subtract/common nodes should survive even if the first renderer path flattens them into fixed brush tables or SDF shader code.
- IFS LOD should be driven by cached node contribution summaries: each node carries bounds, projected error/material delta, coverage, cost, estimator state, residency, and summary/child payload handles so traversal can choose a stable hierarchy cut and render parent summaries while child details stream or fade in.
- Contribution weights cannot all be refreshed every frame; the cache needs a probabilistic online estimator that samples nodes under budget, updates mean/uncertainty/sample age/confidence, preserves nonzero exploration for visible or near-threshold nodes, and can later become a learned predictor without replacing conservative SDF bounds.
- Fractal resource planning is explicit: one plan coordinates CPU contribution updates, GPU selected-cut estimated cost, RAM resident payload budget, and SSD request pressure before backend packet compilation. This same budget language should generalize to Mimir-style sensor fusion because both fractal SDF probes and camera/microphone feature detections accumulate confidence-weighted spatial samples into temporal reservoirs.
- Fractal residency uses score-per-cost retention under RAM pressure and emits evicted node keys as planner output; real storage mutation waits for a page/payload store boundary.
- Compact 3D form splats now have explicit opaque and transparent CPU contracts: structural FractalProbeSample evidence carries layer/encoding metadata, Form/SignedDistance lowers through AquariumFractalSdfSplat3D and FractalSdfSplat3DCompiler, and Form/Density or Form/Extinction lowers through AquariumFractalDensitySplat3D and FractalDensitySplat3DCompiler. The same ResampledImportanceReservoir<FractalProbeSample> handles both; lowering rejects wrong encodings instead of silently forcing density through SDF semantics.
- The `.aquageo` DSL now accepts `density` and `extinction` field claims using the same compact brush syntax as `height`, and the recursive `flame` primitive emits Density payloads. FractalSummaryBuilder treats Height, SignedDistance, Density, and Extinction as Form contribution. FractalHeightBrushCompiler filters mixed trees to Height payloads for height-field lowering instead of forcing transparent fields into terrain brushes.
- The `.aquageo` DSL also accepts `ripple` claims for 2D surface domains. A ripple is a Height claim with the same compact shaped envelope plus explicit wave amplitude, radial frequency, speed, and sine-power, lowered into the existing AquariumHeightFieldBrush wave lane. This is the Aetheria-style cheap water primitive Fensalir uses for the marsh mirror plane.
- Hot fractal splats and their temporal cache layers must be GPU-resident packed buffers. AquariumPackedFractalSdfSplat3D is the first opaque 80-byte numeric Form packet; AquariumPackedFractalDensitySplat3D is the matching 80-byte transparent Form packet for density/extinction. The current SDF envelope, PBR material, and radiosity reservoir packets are distinct 64-byte UAV rows sampled in independent compute passes for Form, Appearance, and Transport, and FractalGpuReservoirBudgetPlanner now reports both backend pass kind and conceptual layer/encoding. SDF/PBR/radiosity are backend packet names, not the conceptual architecture. tools/Aquarium.Fractal.Receipt proves a D3D12 compute path can keep 2,000,000 splats and 2,000,000 reservoirs per layer resident on the GTX 1070. The fallback hash-fractal receipt updates 50,000 entries per reservoir layer per frame with 2 candidates/update and runs at 4.471 ms/frame, 223.7 FPS equivalent. The authored IFS-program path now uses precomposed selected-cut rows instead of replaying ancestry per splat; a depth-48 authored receipt with 4 compact transforms and 15,000 updates per reservoir layer runs at 4.908 ms/frame, 203.7 FPS equivalent, with full reservoir coverage in 133.3 frames. Receipts use only tiny checksum readback and compile the shared engine shader asset src/Aquarium.Engine/Render/Shaders/D3D12FractalReservoirCompute.hlsl.
- 2D-projected-to-3D splats have a pure compiler seam: FractalProjectedSdfSplatCompiler maps resident SignedDistance2D page payload samples through an explicit page-to-world function and normal into bounded AquariumFractalSdfSplat3D packets.
- AquariumSceneState now carries AquariumFractalReservoirField as the narrow client-to-renderer request for GPU-resident fractal reservoirs: resident splat count, stochastic splat updates per frame, IFS depth/seed, candidates per reservoir update, stochastic updates per pass, authored program transforms, and an explicit world center/radius. This is a budgeted GPU field request, not a CPU splat list.
- D3D12Renderer owns the first live fractal reservoir compute hook: a separate root signature, four compute pipelines from D3D12FractalReservoirCompute.hlsl, lazy UAV buffers for splats/SDF/PBR/radiosity/flame-state reservoirs, a UAV barrier after splat population, and per-frame dispatch when AquariumFractalReservoirField is present. The first frame seeds every resident splat; later frames dispatch only SplatUpdatesPerFrame. Zyphos requests the 2M-splat, 50k-splat-updates/frame, 20k-reservoir-updates/pass field by default.
- Fractal reservoir population can now be driven by authored `.aquageo` structure: FractalGpuProgramCompiler lowers selected DSL claims into compact, precomposed AquariumPackedFractalIfsTransform support rows, Zyphos passes those rows through AquariumFractalReservoirField.ProgramTransforms, and D3D12 uploads the small program buffer as a compute SRV so D3D12FractalReservoirCompute.hlsl can generate splats from authored selected-cut supports in O(1) per splat instead of replaying nested domain ancestry.
- The renderer now has a first visible layered-reservoir consumption path for computed fractal splats: D3D12FractalSplatRender.hlsl binds separate GPU SRVs for raw splats, SDF envelope reservoirs, PBR material reservoirs, and radiosity reservoirs, then draws a capped 524,288-instance tangent-frame surface subset into the scene after compute updates. This is an observability/render-consumption proof for the first Form/Appearance/Transport reservoir contract, not the final terrain/material/transport shading path.
- Zyphos now uses the fractal reservoir surface as its primary planetary terrain test path. The old direct SDF planet and Umbros proxy shaders are removed from the Zyphos render plan; the remaining SDF proxy is the parent star. Zyphos binds the fractal reservoir field to the Zyphos world center/radius, and D3D12FractalSplatRender maps SDF/PBR/radiosity reservoir splats into that world frame instead of drawing camera-relative blobs.
- Authored GPU fractal program rows preserve cube-sphere tile address through lowering: AquariumPackedFractalIfsTransform.TileAddress carries face, level, tile X, and tile Y while rotation cos/sin live in MaterialSeedShape.zw. D3D12FractalReservoirCompute maps selected-cut supports through that tile address onto cube-sphere faces instead of dumping every authored brush onto a generic +Z cap.
- Zyphos camera composition targets the selected domain's tracked center while preserving the parent domain as orbit/composition anchor. Parent pivots remain part of navigation, but selected planetary/tile domains must not be pushed offscreen by aiming the camera at their parent.
- Fractal reference parity starts before the renderer: FractalOwnershipTree can carry affine IFS definitions from `.aquageo`, FractalAffineIfsChaosGame generates deterministic point sets, and ApophysisReferenceParityTests compares a minimal Apophysis/FLAM3-style `.flame` linear Sierpinski fixture against an equivalent `.aquageo` candidate. Apophysis/FLAM3 are test oracles, not runtime code.
- The Apophysis/FLAM3 reference subset now has a runtime parser/evaluator boundary: FractalFlameFileParser reads `.flame` xforms with weight, color, affine coefficients, and `linear`/`spherical`/`bubble` variation weights; FractalFlameChaosGame produces deterministic point sets; FractalPointHistogramBuilder produces fixed histogram checksums for parity receipts.
- tools/Aquarium.Fractal.Receipt can now run a CPU flame histogram receipt with `--flame`; scripts/fractal-flame-receipt.ps1 emits the default linear/spherical/bubble fixture checksum 0xAEB1C81B under artifacts/fractal-flame-receipts. It can also hash binary PPM renders with `--reference-ppm`; scripts/fractal-flame-reference-render.ps1 drives a local external `flam3-render` through FLAM3's documented env vars and records image-shape/non-black/checksum receipts under artifacts/fractal-flame-reference without vendoring GPL renderer code.
- D3D12FractalReservoirCompute has an explicit flame 2D program mode for the proven FLAM3-style subset. FractalGpuProgramCompiler lowers flame rows into packed affine/variation program rows, and tools/Aquarium.Fractal.Receipt can run them with `--program-flame`. Flame mode now keeps 32-byte AquariumPackedFractalFlameState rows in a GPU UAV so touched splats advance persistent chaos-game state instead of replaying from the origin. Local GTX 1070 JWildfire selftest receipt after 8 full-population acquisition frames: 2,000,000 resident splats, 2,000,000 flame states, 2,000,000 first-slice Form/Appearance/Transport reservoirs each, steady 50,000 splat updates/frame, 15,000 reservoir updates/pass/frame, depth 8, 1.011 ms/frame, 988.8 FPS equivalent, 84.85% CPU-slice distribution parity, checksum 0xC633FADC3BD7E5BE.
- Fractal reservoir update selection now accepts a four-scalar PriorityFocus window: focus XY, radius, and strength. Under budget, D3D12FractalReservoirCompute samples a few stochastic candidate rows and prefers rows near the focus while preserving random exploration; full-refresh passes still map directly over every row. Zyphos derives the focus from the selected camera/domain target so zoomed domains can pressure reservoir work toward the visible region instead of treating every resident splat as equally urgent. The receipt harness exposes the same pressure through `--priority-focus` for focused GPU tests.
- The live GPU splat packet now carries a Form encoding tag in the existing material/confidence lane without changing the 80-byte stride. Flame-mode compute writes Density encoding; the visible fractal splat renderer uses two explicit graphics pipeline states over the same GPU-resident buffers: opaque signed-distance surface splats depth-test/write through D3D12FractalSurfaceSplatPS, while density/extinction splats run through D3D12FractalTransparentSplatPS with depth disabled and additive color-only blending. This is not full volume ray integration, but it makes depth/blend ownership explicit without adding a parallel transparent reservoir or buffer.
- Fractal splat render now writes the documented reservoir/TAA guide schema: x = combined reservoir confidence, y = sample age in frames, z = domain validity, w = invalidation code. Form/Appearance/Transport validation stays internal to the splat shader and no longer masquerades as guide-buffer age/validity.
- Render debug mode 12, `Reservoir/TAA Guide`, now displays the same schema it names: red confidence, green normalized sample age, blue domain validity.
- The first visual parity receipt compares GPU flame splat XY histograms against the CPU flame oracle. With 250,000 GPU readback splats and 1,000,000 CPU reference samples in 128x128 bounds -8,-8,8,8, the mixed flame fixture scores 84.05% distribution parity at frame 1, 84.10% at frame 5, 84.20% at frame 20, and 84.24% after the 60-frame/10-warmup receipt, with cosine similarity around 0.9955. This is a stable sampler score, not proof of learned convergence; visual-error feedback is still future work.
- View-conditioned visual parity now scores named camera windows instead of trusting one global histogram. For the depth-16 flame receipt, global -8..8 scores 97.09%, mid -1..1 scores 91.93%, close -0.5..0.5 scores 88.97%, and micro -0.25..0.25 scores 82.05% with 2,656 starved reference bins and 17.95% underrepresented mass. This is the current hard loss: the reservoir is not yet spending work based on camera-conditioned underrepresentation.
- FractalHistogramResidualFocus is the first pure residual-to-priority seam: given reference and candidate histograms, it estimates a focus center/radius/strength from underrepresented probability mass. tools/Aquarium.Fractal.Receipt now reports this suggested focus for each visual parity view, turning parity failure into a concrete reservoir pressure input instead of a contact-sheet shrug.
- tools/Aquarium.Fractal.Receipt has the first adaptive focus control-loop receipt. `--adaptive-focus` runs a warmup epoch, reads back the shared GPU-resident reservoir state, estimates a residual focus from visual parity, then continues measured frames over the same splat/reservoir/flame-state buffers using that focus. Local GTX 1070 smoke on the mixed flame fixture with 65,536 splats, 4,096 steady splat updates, 4,096 reservoir updates/pass, 50k CPU reference samples, and bounds -2..2 improved from the non-adaptive 67.07% distribution score to 70.89%, with 0.160 ms/frame measured GPU time. This proves the feedback shape, not a final no-stall runtime implementation.
- High-reference flame contact sheets should use at least 20M CPU oracle samples for visual judgment. The current artifact is artifacts/fractal-contact-sheet-hq/flame-parity-contact-sheet-hq.png, comparing 20M CPU oracle histograms against 250k Aquarium GPU readback samples for global frame 1/10 and zoom frame 1/10. The 1M-sample oracle was too noisy and should be treated as a scalar smoke check only.
- External flame visual references are now reproducible from ignored local artifacts. scripts/install-flame-reference-tools.ps1 hydrates JWildfire 9.00 plus Temurin JRE 17 under artifacts/reference-tools; scripts/fractal-flame-jwildfire-reference.ps1 prefers JWildfire's bundled FARenderJWF GPU renderer for explicit PNG output. Local GTX 1070 smoke rendered FARender's selftest.flame at 1024x1024 quality 2000 in 7.69 sec to artifacts/fractal-flame-jwildfire-reference/selftest-farender-20260520-233638.png. JWildfire/FARender is a visual reference artifact lane, not Aquarium runtime code.
- tools/Aquarium.Fractal.Receipt now has an external density oracle: --reference-density-ppm compares Aquarium GPU Form-envelope reservoir density against luminance density from a binary PPM rendered by an external flame renderer. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from JWildfire's selftest by reducing julian power and muting gaussian/wfield amounts while preserving FARender boilerplate. First GTX 1070 external-density receipt against FARender's 768x768 quality-2000 baby render scored only 15.33% when Aquarium still binned raw splat centers; after fixing JWildfire coefs packing and FARender disc angle convention the raw-center score reached 26.87%. Switching to reservoir density with full 2M-row reservoir refresh scored 22.14% at 42.053 ms/frame. Cutting the false JWildfire `normal` -> `linear` import and enforcing a one-bin density reconstruction footprint moved the score to 31.36% at 41.692 ms/frame in 256x256 bounds -2..2. Using `-HistogramBounds flame-camera` derives FARender-style bounds from flame size/scale/cam_zoom and raises the current score to about 43.1% with about 68% occupancy overlap. Full reservoir refresh now uses direct row indexing instead of stochastic hashing, so the same full-refresh gate runs at 7.508 ms/frame / 133.2 FPS equivalent instead of roughly 41 ms/frame. Aquarium still scores about 89.94% against its own CPU slice, so the remaining external gap is exact JWildfire variation/filter semantics, target density weighting, and view-conditioned reservoir feedback, not GPU sampling count.
- JWildfire selftest flame is the current honest faceplant target. After adding a first dialect slice for JWildfire coefficient layout, variationGroup, disc, julian, gaussian_blur, and post transforms, the depth-64 GTX 1070 receipt against Aquarium's CPU interpretation of artifacts/reference-tools/j-wildfire-9.00/lib/FARenderJWF/selftest.flame reaches 84.19% score in bounds -8..8 and 88.01% in bounds -128..128, but costs about 129 ms/frame. This is CPU/GPU slice parity, not external JWildfire visual parity. It proves the remaining hard work is richer dialect semantics plus persistent GPU iteration-state reservoirs; brute replaying deep flame iteration from zero per splat is not the final hot path.
- FractalFlameFileParser now has ParseFirstWithReport for import accountability. Receipt runs emit fractal-flame-import-report-*.json beside flame histogram/GPU receipts. The JWildfire selftest report currently records 15 accepted fields, 3 approximations, 45 ignored render/presentation fields, and 11 rejected geometry/sampling fields: wfield_* weighting-field attributes. Successful XML parsing is not successful translation.
- FractalFlameIterationState and FractalFlameIterationStepper define the persistent flame iteration invariant before GPU lowering: advancing a sample in chunks must match a one-shot advance from the same state. This is a parity/mock boundary for the future GPU state buffer, not a runtime CPU renderer.
- D3D12 capacity logging now reports active fractal reservoir resident MiB, splat count, visible debug splat count, updates/pass, and candidates/update so the GPU cache has a receipt in normal runtime logs, not just in the standalone benchmark.
- Fensalir has a fixed headless art-repro capture lane: `scripts/capture-fensalir-frame.ps1` builds disposable engine/client slots, launches the Fensalir client headless at an explicit resolution, waits for a ready frame, and writes a PNG through `IAquariumRenderer.SaveFramePng`. Headless capture disables overlay debug UI and locks the Fensalir camera/time so shader and `.aquageo` changes can be compared against `src/Aquarium.Engine/Assets/Fensalir-Splash.png` without manual window judgment.
- Fensalir currently has a temporary transparent cathedral guide in the scene shader to make splash-composition failures visible in fixed captures. This is scaffolding, not final architecture: the next coherent cut is a patch-lowered pane/rune/material field target separate from the marsh height field, so the cathedral glass/runes/waterline glow are authored DSL claims instead of hardcoded screen-space guidance.
- The consolidated fractal brush architecture plan defines the intended pipeline: client intent -> domain roots -> semantic IFS brush grammar -> ownership tree -> node summaries -> contribution cache/residency scheduler -> backend packets -> renderer passes -> debug/evidence, with explicit module boundaries and mockable unit-test seams.
- The Perfect Machine architecture lives in docs/perfect-machine-architecture.md. It is the current waterfall design authority for fast 2D/3D/projected scalar-field splats, Form/Appearance/Transport reservoir layers, SDF level-set surfaces for opaque solids, density/extinction/confidence fields for flames and sensor fusion, IFS `.aquageo` structural probes, Mimir-style camera/audio evidence candidates, ReSTIR/GRIS reservoir reuse, heuristic occupancy graph updates, resource-tier residency, and TAA guide-buffer integration.
- Fractal surface page planning now has a CPU boundary: FractalSurfacePagePlanner lowers selected IFS cuts into stable height, 2D signed-distance, material, and confidence page metadata; ZyphosFractalRenderPlan exposes these pages, while the live D3D12 page store and atlas remain future work.
- Surface page residency has its own mockable store boundary: IFractalSurfacePageStore plus FractalSurfacePageResidencyPlanner produce resident, missing, requested, evicted, and byte-budget rows for planned pages without collapsing page cache policy into node payload residency.
- Resident surface pages can now be rasterized on CPU through FractalSurfacePageRasterizer into height, 2D signed-distance support, material, and confidence sample payloads using the same shaped brush envelope math as the brush compiler. Zyphos caches those payloads by page key; D3D12 upload remains future work.
- The actionable fractal brush implementation roadmap lives in research/rendering/fractal-brush-implementation-roadmap.md and is the execution authority for the first pass.
- Bundled blue-noise texture assets are retained for future dithering/noise work even though the removed medium path no longer samples them.
- D3D11 scene renderer, runtime volumetrics, cloud/fog research, medium/froxel render targets, and the SDF-field renderer plan are shelved and removed from live repo state.
next:
- Extend explicit renderer debug modes for hits, normals, depth, bins, and lighting.
- Replace the current legacy fixed D3D12 pass execution with the declared AquariumRenderPlan graph compiler and executor.
- Keep src/Aquarium.Sample.Minimal building and launchable as the non-Epiphany boundary proof.
- Replace fixed fallback role motion with real CultNet-derived AgentVisualState.
- Build each agent role body from one coherent low-cost SDF form model before adding ornamental or state-reactive detail.
- Prototype cube-sphere projection candidates for Zyphos: tangent warp, fifth-order odd polynomial warp, and COBE/QSC-style bivariate warp. Measure area error, inverse lookup cost, and shader cost before committing the terrain address space.
- Define the first shaped brush envelope contract as a compact-support anisotropic kernel with rotation, radii/inverse covariance, falloff, shape controls, payload type, and explicit bounds shared by 2D terrain brushes and 3D SDF/body brushes.
- Design the first Aquarium IFS/CSG brush DSL as semantic claims plus an ownership tree, informed by VibeGeometry's `vg_grammar` and `vg_csg` tree: the tree is durable design; flattened shader packets are backend output.
- Define the first IFS LOD node-summary and contribution-cache layout before rendering recursive detail: stable domain keys, projected-error scoring, stochastic update priority, mean/variance/confidence estimator state, hysteresis fade weights, resident/missing child states, and debug views for score/uncertainty/sample-age/depth/cache/cost.
- Replace the legacy height-field plane with explicit 2D SDF domain surfaces. Aquarium's Grid should become a dynamic gravity-updated domain surface; Zyphos' terrain surfaces can stay static and should flow through the same cache-friendly surface-domain path.
- Follow the step-by-step roadmap in research/rendering/fractal-brush-architecture-plan.md before implementing recursive terrain or agent-body detail; first slice is cube-face projection harness, shaped 2D brush envelope, semantic claim/tree structs, flattening to the current height brush pass, and debug overlays.
- Begin fractal brush implementation with the first work packet from research/rendering/fractal-brush-implementation-roadmap.md: add Aquarium.Engine.Fractal plus tests, cube-face/tile keys, identity/tangent projection candidates, area distortion sampler, face-edge tests, and a projection report.
- Add diegetic labels and object-local menus after renderer and text contracts are stable.
- Continue replacing legacy fixed D3D12 pass execution with client-declared render targets, cameras, shaders, passes, resources, and features.
- Fensalir owns reusable host, renderer, audio, reload, contracts, and engine mechanisms.
- Client repos own policy, role names, narrative semantics, app state, and client shaders.
- 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.
next_actions:
- Keep main as the live development branch.
- Keep scripts/verify-boundaries.ps1 green: Aquarium.Engine and Aquarium.Engine.Contracts must not contain Epiphany/client policy nouns such as grid, agent, body, or epiphany.
- Verify publish output after every packaging-affecting change.
- Update AquaSynth.Faust/Core only by packing new versions into packages and changing the pinned PackageReference version deliberately.
- Grow renderer debug controls through the Direct2D/DirectWrite overlay path.
- Use scripts/render-agent-preview.ps1 for multi-view isolated SDF review before asking the user to judge another agent body iteration.
- Follow research/rendering/hdr-pbr-presentation.md before changing HDR exposure, bloom, tonemapping, or display transforms.
- Follow research/rendering/cube-sphere-fractal-brushes.md before replacing planetary terrain addressing, height brushes, or agent/body detail grammars.
- Follow research/rendering/fractal-brush-architecture-plan.md before implementing the fractal brush DSL, IFS LOD cache, recursive detail residency, or learned contribution scoring.
- Follow docs/perfect-machine-architecture.md before expanding field splat backends, density/extinction packets, sensor-fusion candidate adapters, reservoir guide buffers, occupancy graph state, or stochastic learned priority.
- Use research/rendering/fractal-brush-implementation-roadmap.md as the actionable execution checklist for fractal brush work.
- Next Fensalir art slice: replace the temporary screen-space cathedral guide with a `.aquageo`-owned transparent pane/rune/material field target and feed it to the scene shader as its own texture/buffer, separate from the marsh height field.
- Next flame reference slice: build a JWildfire import/lowering report that separates supported, approximated, ignored, and rejected variation fields before expanding the GPU op set. Do not hide dialect loss behind successful XML parsing.
- Next flame renderer slice: mirror FractalFlameIterationState in a GPU UAV so program-flame mode can advance resident sample state over frames instead of replaying all depth from zero per splat. Weighting fields remain explicit rejected diagnostics until they become real DSL operator nodes.
- Follow docs/zyphos-eusocial-sync.md and E:\Projects\Eusocial Interbeing\Eusocial Interbeing\World\Zyphos Simulation Brief.md before turning Zyphos visual choices into setting claims.
- Follow docs/tsr-inspired-taa-spec.md when changing current temporal diagnostics.
- Use docs/temporal-sdf-gaussian-field.md before wiring LocalCastBridge visual sensor fusion into Aquarium or scaling the temporal Gaussian renderer beyond the current bounded instanced pass; despite the historical filename, the live contract is temporal Form-field evidence, not surface-only SDF.
- Use docs/temporal-spatial-evidence-reservoir.md before adding any stable-key temporal spatial cache, sensor-fusion accumulator, SDF probe reservoir, or new evidence packet lowering.
- Promote future runtime knobs into typed CultCache documents before they become process folklore.
- Next Face voice slice: replace text-only input with microphone/WebRTC or a native capture bridge, bind endpoint anchors to real Face/body spatial projections instead of manual XY rows, then move the transport contract from Codex JSON-RPC compatibility toward a typed CultNet FaceVoiceSession once Epiphany advertises that surface.
- Keep Aquarium and Epiphany in one repo until the contracts and graph API are stable enough for Epiphany to move out without carrying engine policy or reload folklore with it.
- Set the real Git remote for this repo once `GameCult/Fensalir` exists.
- Keep `dotnet build Fensalir.sln` green after the split.
- Move any remaining client-only Epiphany state found here back to `E:\Projects\Aquarium-Engine`.
- Package contracts/host deliberately before replacing sibling source references in client repos.

View file

@ -1,7 +1,7 @@
{
"identity": {
"name": "Aquarium Engine Self",
"role": "native C# engine steward for the Epiphany Aquarium client",
"name": "Fensalir Engine Self",
"role": "native C# runtime and renderer steward for Aquarium-style clients",
"desire": "Keep the renderer, state, and interaction grammar owned, legible, and useful."
},
"taste": [
@ -15,7 +15,7 @@
"native_engine": [
"Aquarium Engine lives at E:\\Projects\\Aquarium-Engine. Main is the live development branch.",
"Aquarium.Engine is the native engine layer: Win32 hosting, D3D12 rendering, input translation, reload transport, resource abstractions, and future render graph execution. It must not know Epiphany frontend roles, policy, or semantic client layout.",
"Aquarium.Epiphany is the Epiphany client layer behind Aquarium.Engine.Contracts. It owns client runtime state, CultCache documents, CultNet interpretation, camera intent, and future render graph configuration against engine abstractions.",
"Epiphany Aquarium now lives in the adjacent client repo at E:\\Projects\\Aquarium-Engine and depends on Fensalir contracts. It owns client runtime state, CultCache documents, CultNet interpretation, camera intent, and render graph configuration against engine abstractions.",
"The render API migration target is docs/epiphany-extraction-render-api-plan.md: clients declare render targets, cameras, shaders, passes, buffers, textures, and features through Vortice-free Aquarium contracts; Aquarium compiles that graph into D3D12 resources, descriptors, barriers, pipeline state, resize, shader reload, presentation, and debug inspection.",
"Epiphany declares its render plan, SDF shader manifest, SDF object table, SDF light table, height-field brush list, and cursor projection through Aquarium.Engine.Contracts. Aquarium.Engine consumes those as generic rendering data and must stay free of Epiphany client policy nouns; run scripts/verify-boundaries.ps1 to enforce that boundary.",
"D3D12Renderer now compiles an AquariumRenderPlan description and allocates non-legacy client-declared render targets through engine descriptor/resource machinery. Current legacy fixed pass execution still needs to be replaced by graph pass execution.",

View file

@ -2,66 +2,15 @@
## Current Slice
Aquarium's active focus is the fractal reference gate for the Perfect Machine:
prove compact IFS/flame fixtures numerically before asking Zyphos visuals to
carry architectural truth.
Fensalir has been split into its own adjacent repo.
Keep the engine/client boundary strict:
- Aquarium.Engine owns Win32, D3D12, input, reload, renderer abstractions,
debug UI plumbing, and device/toolchain integration.
- Aquarium.Epiphany owns client policy, semantic state interpretation, role
layout, and render-plan configuration through Aquarium.Engine.Contracts.
- Aquarium consumes AquaSynth.Faust as a pinned package in the repo-local
`packages` feed; AquaSynth.Core arrives transitively for patch editor/parser
surfaces. AquaSynth owns synth internals and tests; update
the package only through an intentional version bump.
The renderer work that matters next:
- Continue replacing legacy fixed D3D12 pass execution with the declared render
graph.
- Keep body/SDF proxy passes bounded, per-object, and client-authored.
- Build role visuals from coherent low-cost form models before ornament.
- Use `research/rendering/cube-sphere-fractal-brushes.md` as the live map for
the cube-sphere planetary domain and shared IFS brush grammar migration.
Its IFS/CSG DSL notes now pull from neighboring VibeGeometry's `vg_grammar`
and `vg_csg` tree work.
Its LOD model uses cached node contribution summaries to select stable
hierarchy cuts and stream/fade child detail only when visible pixels justify
the cost. Contribution weights are online estimates: update a probabilistic
subset each frame, track uncertainty and sample age, and keep conservative
summaries as the safety floor if later learned scoring is added.
- The consolidated architecture and roadmap live at
`research/rendering/fractal-brush-architecture-plan.md`, including module
boundaries, mock seams, and unit/performance test strategy.
- The actionable execution checklist lives at
`research/rendering/fractal-brush-implementation-roadmap.md`; first work
packet is `Aquarium.Engine.Fractal` plus tests, cube-face/tile keys,
identity/tangent projections, area distortion sampler, face-edge tests, and
a projection report.
- Use `research/rendering/fractal-reference-oracles.md` for Apophysis/FLAM3
parity. Local parser/evaluator receipts are not external renderer proof.
`scripts/fractal-flame-reference-render.ps1` is the optional FLAM3-compatible
PPM receipt path when a local `flam3-render` binary is installed.
`scripts/fractal-splat-receipt.ps1 -ProgramFlame ... -SplatUpdates 50000`
is the GPU-resident flame stress receipt path; it initializes resident splats
once and then performs budgeted stochastic updates.
Add `-VisualParity -ReadbackSplats 250000 -HistogramSize 128x128` to score
GPU splat histograms against the CPU flame oracle. Current score plateaus
near 84.2% distribution parity / 0.9955 cosine, so the next architectural
need is visual-error feedback, not more raw sampling theater.
Use `-VisualParityView name:minX,minY,maxX,maxY` for camera-window truth.
The depth-16 flame receipt loses hard under zoom: micro -0.25..0.25 is 82.05%
with 2,656 starved reference bins. This is the next real reservoir target.
- Use `docs/zyphos-eusocial-sync.md` before evolving Zyphos world content.
Eusocial Interbeing owns canon in `E:\Projects\Eusocial Interbeing`; Zyphos
owns render constraints and feeds design questions back through the vault's
`Eusocial Interbeing/World/Zyphos Simulation Brief.md`.
- Preserve debug UI as shared chrome and input capture, not parallel panels.
- This repo owns the native runtime, renderer, contracts, fractal machinery,
host scripts, and engine demo clients.
- `E:\Projects\Aquarium-Engine` now owns only the Epiphany Aquarium client.
- There is no remote configured yet; set it to the future `GameCult/Fensalir`
remote before pushing.
## Verification
- `dotnet build Aquarium.Engine.sln --no-restore`
- `.\scripts\verify-boundaries.ps1`
- `.\scripts\dev-reload.ps1 -Headless -RetainSlots 4`
- `dotnet build Fensalir.sln`
- `.\scripts\dev-reload.ps1 -Headless -RetainSlots 4 -ClientProject src\Aquarium.Fensalir\Aquarium.Fensalir.csproj`

View file

@ -285,7 +285,7 @@ public sealed class ApophysisReferenceParityTests
private static string FindRepoRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Aquarium.Engine.sln")))
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Fensalir.sln")))
{
directory = directory.Parent;
}

View file

@ -45,7 +45,7 @@ public sealed class FractalFlameIterationStateTests
private static string FindRepoRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Aquarium.Engine.sln")))
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Fensalir.sln")))
{
directory = directory.Parent;
}

View file

@ -93,7 +93,7 @@ public sealed class FractalGpuProgramCompilerTests
private static string FindRepoRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Aquarium.Engine.sln")))
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Fensalir.sln")))
{
directory = directory.Parent;
}

View file

@ -81,7 +81,7 @@ public sealed class ProjectionTests
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "Aquarium.Engine.sln")))
if (File.Exists(Path.Combine(directory.FullName, "Fensalir.sln")))
{
return directory.FullName;
}
@ -89,7 +89,7 @@ public sealed class ProjectionTests
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find Aquarium.Engine.sln.");
throw new DirectoryNotFoundException("Could not find Fensalir.sln.");
}
private static string NormalizeLineEndings(string value)

View file

@ -23,7 +23,7 @@ public sealed class ZyphosNamingTests
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "Aquarium.Engine.sln")))
if (File.Exists(Path.Combine(directory.FullName, "Fensalir.sln")))
{
return directory.FullName;
}
@ -31,6 +31,6 @@ public sealed class ZyphosNamingTests
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find Aquarium.Engine.sln.");
throw new DirectoryNotFoundException("Could not find Fensalir.sln.");
}
}