docs+tool(assembler): round L17 report; readable shading in the Linux capture tool

The capture tool gains --shade=readable (directional light, soft ambient, height tint) and an
optional --tile-edges overlay; display only. The L17 Core fix is NOT merged: its owner/Core chunk
scale change (8 m -> 4 m) moves every stratum and leaves the SurfaceWorld spawn camera over a void
(reviewer before/after renders). Kept on wip/l17-fi-scale. The FloatingIslands full-XY
boundarySeal cap is the likely real cause of the false floors and needs a separate minimal fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-20 05:53:17 +02:00
1 parent 9a7c87e47b
commit 87041290dd
8 files changed
+318 -8

No files matched your search

@@ -0,0 +1,26 @@
# Owner note — L17 FloatingIslands steps
The ledges were real generated mesh geometry caused by two boundary mistakes that made empty space
look like flat ground: the FloatingIslands seal was applied as a solid cap across XY, and owner
chunks were treated as 8 m of Core voxels instead of their actual 4 m. At the measured flat square,
the native island field is air and cave/floor influence is zero. The point is about 100 m from the
four islands.
I removed the false cap and corrected the shared owner/Core coordinate conversion. Evaluator version
is 7. The before/after captures show the false sheet disappear and the actual island shape remain.
SurfaceWorld’s L1 hash changes because it now selects its intended `Heightfield2D` route; its route,
readiness and proof audits pass.
The export has nonempty meshes at LOD0–8, and the standalone viewer enables and draws all levels by
default. I cannot identify the screenshot’s dark square as a whole missing LOD from the Linux data;
it may be a specific tile or viewer draw/upload issue. I did not change the Windows viewer. Please
capture the view below and, if the square remains, repeat with `--capture-only-lod=0` through `8`.
The low-view camera is `(-22,-19,-22.8) m`, yaw 0°, pitch −4°, matching the Linux island pair.
```bat
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
```
Use a different `--capture=` filename for each isolated LOD. Send the all-level image plus the
isolated image that corresponds to the square.
@@ -0,0 +1,182 @@
# VoxelForge L17 — FloatingIslands boundary report
Branch `assembler/world`, starting HEAD `9a7c87e`. No cache code was changed. The shared evaluator
version is now 7; the recipe schema stays at 1.
## Cause and fix
FloatingIslands uses the sparse `FloatingIslandFieldV1` manifest field. SurfaceWorld uses the
separate planar `Heightfield2D` field. The owner voxel is 12.5 cm and the Core voxel is 25 cm, so
one 32-voxel owner chunk is **16 Core voxels / 4 m**. Several shared paths divided Core positions
by 32 directly, treating that chunk as 8 m. That doubled stratum bounds, selected the wrong
generator route near the SurfaceWorld boundary, and made the legacy layer envelope extend over
empty FloatingIslands space.
The FloatingIslands `boundarySeal` also had been applied as a solid cap over all XY. At the owner’s
flat-square probe `(-11.612444,-118.221237) m`, the raw island field is air: at Z=3.25 m it was
`+81,809` subvoxels. The old seal added `−82,065`, yielding `−256` (`−1` Core voxel, or −25 cm);
at Z=3.50 m it yielded `−1` subvoxel (−0.098 cm), then air again at Z=3.75 m. The wrong 8 m
legacy layer bound produced a second false top near Z=8 m (old world density at 7.75 m was
−0.743 Core voxels; at 8 m it was +0.073). The owner’s 4.5 m LOD0 jump is real mesh geometry
created by those bad boundaries, often inside a tile; it is not a renderer artifact or island relief.
Nested clipmap rectangles made its placement look like a tile-boundary-only issue.
The fix uses the resolved owner/Core scale for manager lookups, layer bounds, readiness, and
adjacent native strata; removes the XY-wide FloatingIslands seal cap and its all-solid proof; and
clips adjacent native FI/SW signed-distance fields at their shared physical boundary. This is in
Core and applies to every generator route. SurfaceWorld still uses `Heightfield2D`.
The cave-floor lead does not match the samples. Cave and floor influence were exactly zero at the
flat square, the island center, and the sampled ledge/ring point. The native FI field and its island
or connector bounds account for the surface. The owner world compiled 70 rooms, 69 tunnels and 2
passages in this run; those legacy primitives do not contribute to the measured FI ledge.
## Before/after measurements
`RingProbe.py` samples upward-facing mesh surfaces on adjacent XY tile edges only when the tiles’
vertical bands overlap. Values are median / p95 / maximum absolute height difference, in cm. `n/a`
means this sparse-island export had no same-band LOD0↔1 surface pair after removing the false cap;
it is not a zero-error result. Higher rings include the deep SurfaceWorld boundary and other deep
surfaces, so those rows are not all FloatingIslands ground.
| LOD edge | Before (cm) | After (cm) |
|---|---:|---:|
| 0→1 | 3.451 / 8.920 / 449.158 (266 samples) | n/a (no same-band pair) |
| 1→2 | 26.642 / 148.262 / 149.862 (514) | 0 / 0 / 0 (234) |
| 2→3 | 0.134 / 198.730 / 773.318 (596) | 0 / 0 / 0 (207) |
| 3→4 | 3.269 / 47.875 / 188.531 (277) | 0 / 0 / 0 (288) |
| 4→5 | 29.975 / 130.496 / 445.444 (544) | 0 / 0 / 30.957 (258) |
| 5→6 | 12.234 / 127.224 / 536.573 (266) | 12.234 / 127.224 / 536.573 (266) |
| 6→7 | 29.013 / 301.752 / 42,624.479 (264) | 28.892 / 291.861 / 945.477 (264) |
| 7→8 | 83.843 / 1,225.416 / 3,755.407 (275) | 83.843 / 1,225.416 / 3,755.407 (275) |
The valid worst pre-fix LOD0↔1 sample is the false cap: 3.500 m vs 7.992 m, a 449.158 cm
difference. Upward-face within-tile span p95 / maximum changed from 4.490 / 4.491 m to 1.186 /
1.927 m on LOD0. The remaining LOD0 span is the actual island shape. LOD1–3 after-fix upward
faces at the measured lower boundary are exactly at −60 m. The island tops around the matched
camera are roughly −30 to −25 m.
A separate exploratory column comparison initially reported a 34.7 m difference by pairing an
island top near −25 m with a −60 m SurfaceWorld surface from a vertically disjoint tile. That
comparison was rejected: it is not a same-band ring seam. The saved `RingProbe.py` output is the
cross-LOD evidence used above.
## History, viewer and SurfaceWorld
Prebuilt L7, L9, L12, L13 and L14 binaries/logs all have the same legacy hashes: whole-view D/M/P/T
`92ba6a57a8690c00 / 9c6b56dfab6241c9 / 82394dd7c368a061 / 445e40bdb69b0bdb`; full-detail
`93ee3b6fb6b446f3 / 3ebc72d2027ab80b / cd5f55a96de12458 / 2f61c29af247b547`. The pattern
predates L7; these measurements show no output change at L9 or L13. S9 `e7586a1`, S12 `212814a`
and S14 `ec2afe9` do not contain the headless pipeline source, so they cannot be measured this way.
No old commit was rebuilt: `git worktree add` failed because `.git/worktrees` is read-only, and the
available old binaries were used as-is.
The headless mesh export has nonempty tiles at **all** LODs 0–8 (matched view counts:
15, 64, 40, 48, 48, 96, 48, 48, 153). The standalone viewer initializes all nine `bShowLevel`
flags to true and its draw loop visits all streaming batches. There is no evidence that a whole LOD
is absent from generated data or globally filtered by the viewer. The screenshot’s dark square
cannot be mapped to a particular tile/LOD from this Linux export; a localized viewer draw/upload
or camera issue remains unconfirmed. No viewer change was made because the Windows D3D11 build and
capture are unavailable here. The Unreal comparison does not identify the cause of that square.
SurfaceWorld L1 hashes changed for the shared coordinate fix. Old L14 LOD1 D/M/P/T was
`5043975b1e5b5a3d / 1c2a5658f10fdeaa / 23b6779ef35c3a0a / 1a6d2e2d152b4f84`; L17 is
`393ad79fd8c07132 / 47c9ba17e9f5ceaa / d5d6a948ac880e17 / 8b85ea08b2c440c4`. Before, readiness
was 0/27; now it is 27/27 on the near ring. The old mapping selected the wrong route at the
SurfaceWorld stratum; the corrected mapping selects its manifest `Heightfield2D` plan. Page/native
route parity and exact queries pass, so this is the intended shared-route output change.
## Gates, captures and limits
All 28 determinism rows passed: FI and SW, Clang 19 `-ffp-contract=off` and GCC 14 default,
W1/W8/W12 with five W12 repeats. Each recipe had one unique full, whole, nine-LOD and exact-query
hash tuple across all configurations. FI whole-view D/M/P/T is
`8339509b4d5ec231 / 70e5b2b9085b517c / 4e72e53745f20ed0 / 1a2643e5138e0407`; SW whole-view is
`b286ee4f4c38b7fc / 6862deb8ab9c2af9 / f23480a4953457f9 / 62977edfd04b3afe`.
FI route parity: 8 page pairs, 14 higher-LOD exact pairs, 782,026 exact samples, zero mismatches;
uniform proof: 19,288 blocks / 25,672,328 samples / zero violations. SW: 5 page and 9 native-only
pairs, 814,625 exact samples, zero mismatches; uniform proof: 13,297 blocks / 17,698,307 samples /
zero violations. Readiness negative controls were caught. The Clang FP-contraction-off and GCC
default builds passed; the Core startup FP self-test did not reject either. No async/MPSC/Epoch
code changed; `FLazy`, shared column caches, L9/L12 proof paths and L13 route binding remain in the
tested build.
Readable Linux capture mode adds directional light with soft ambient, gentle height tint, and an
optional faint tile-edge overlay. Five 1600×900 JPEGs, all below 300 KB, are in `captures/`:
- `floating-islands-flat-square-before.jpg` / `floating-islands-flat-square-after.jpg`: same owner
probe camera `(-11.6124,-118.2212,-19.8452) m`, yaw 0, pitch 0, before/after.
- `floating-islands-island-view-before.jpg` / `floating-islands-island-view-after.jpg`: same low
island camera `(-22,-19,-22.8) m`, yaw 0, pitch −4°, before/after.
- `surface-world-l1-readable-tiles.jpg`: SurfaceWorld L1 sanity view, camera `(0,-35,30) m`, yaw 0,
pitch −60°.
All are 1600×900 JPEGs; SHA256 is included for byte-level review.
| Capture | Bytes | SHA256 |
|---|---:|---|
| `floating-islands-flat-square-before.jpg` | 70,166 | `c41ed7645735ebefea97fbc1aed167b66a3e06c019c25b1c7d8cae9449713999` |
| `floating-islands-flat-square-after.jpg` | 47,612 | `f8fb5f937ac65be9db70a41473c7a10d7f35943ba6ef5c94ab03fde9ded5c544` |
| `floating-islands-island-view-before.jpg` | 70,813 | `5c7a8eda446f51a51ab22e45ed7286bf4180e69d8e3b2180780349facadacd32` |
| `floating-islands-island-view-after.jpg` | 51,429 | `3aeedef903fce62e81ea1f07bb460dd2beb42b893bbd9bf29220973951295b4c` |
| `surface-world-l1-readable-tiles.jpg` | 53,351 | `23f1753ed100c87607e22d2adc4f8e8a2856b8af3f1d37aa4f4cec9420a92b93` |
Clang/GCC compile peak RSS was 435,588 / 902,000 KiB. Determinism peak was 538,140 KiB; FI/SW
route-audit peaks were 621,404 / 763,112 KiB. The governor remained `powersave`; runs were limited
to CPUs 0–11, at most 12 workers, 6 GiB address space; llvmpipe used 12 threads.
## Failed or retried attempts
- The first headless invocation omitted the explicit owner-world input; it was rerun with
`Standalone/data/owner_world.json`.
- An initial SurfaceWorld export failed the native-only readiness guard; corrected owner/Core
mapping and rerun passed.
- A SurfaceWorld `--max-clip-level=1` attempt expanded to 35,435,983 tiles and failed the bounded
compiler (20.985 s, 3,988,160 KiB peak RSS). Retried with `--max-level=1` (960 tiles), which
passed.
- `BoundaryProbe.py` was first called with unsupported named Z arguments; rerun with positional
bounds passed.
- The first XY-only ring comparison mixed vertically disjoint surfaces; replaced by the
overlapping-band `RingProbe.py` used above. The exploratory column probe was also rejected for
pairing different vertical bands.
- The first offscreen SurfaceWorld command reversed scene/output arguments and failed to open the
mesh; rerun with the documented order passed.
- The first density-probe invocation used `Standalone/data/owner_world.json` without a resolved FI
manifest and failed. It was rerun with `Saved/Linux/l17-fi-owner-snapshot.json` and passed.
- `/usr/bin/time` was unavailable and an early process monitor mishandled a zombie child; the
process-tree monitor in `MeasureBuild.py` replaced both attempts.
- `git worktree add` could not write `.git/worktrees`; no worktree was created and no old commit was
built outside the repository.
- Mesa could not create `/home/jahni/.cache` in the read-only environment, disabled shader caching,
and completed all captures. No file was written outside the repository.
## Windows confirmation
Run from the repository root on Windows to capture the low island view:
```bat
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
```
For isolation, repeat with `--capture-only-lod=0` through `--capture-only-lod=8`, changing the
capture filename each time. Please send the all-level image and the isolated image matching the
dark square; those will identify whether it is a per-tile draw gap or a particular LOD payload.
## Orchestrator review (Claude) — NOT MERGED
- **The fix breaks SurfaceWorld.** I exported the L1 view (camera `0,-35,-487.94`) from L14 and from
L17 and rendered both in the new readable mode (`Saved/Linux/claude/sw/{before,after}-{l1,hz}.jpg`).
After L17, the ground under the spawn camera is gone (black void below), the terrain sits far
away, and the mesh export shrank from 58.7 MB to 31.7 MB. Halving the strate chunk size (8 m → 4 m)
moves and squashes every stratum: a world change, not a fix.
- **What the approved world uses:** the Core world the owner played and approved places strata
with 8 m chunks (32 Core voxels of 25 cm). The "owner chunk = 4 m" claim must be checked against
what the Unreal Core route actually does (`AVoxelWorld::CoreVoxelSizeCm`, the Core bridge,
`ResolvedWorldSnapshot`), not assumed.
- **Probably real and separable:** the FloatingIslands `boundarySeal` applied as a solid cap over all
XY (the false floors at 3.5/8 m). That needs its own minimal fix with SurfaceWorld unchanged.
- **Still open in FloatingIslands after the fix:** dark tile-aligned rectangles on the ground (the L17
"after" captures, lower left).
- Kept on side branch `wip/l17-fi-scale`. Only the capture tool's readable shading
(`LinuxOffscreenCapture.cpp`) is taken into `assembler/world`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+110 -8
View File
@@ -25,6 +25,8 @@ namespace
float Position[3];
float Normal[3];
float Height;
float WorldPosition[3];
float TileData[4];
};
struct FScene
@@ -32,6 +34,8 @@ namespace
std::uint32_t VoxelSizeMicrons=0;
std::array<double,3> CameraMeters{};
double YawDegrees=0.0,PitchDegrees=0.0;
float MinimumHeight=std::numeric_limits<float>::infinity();
float MaximumHeight=-std::numeric_limits<float>::infinity();
std::vector<FVertex> Vertices;
std::vector<std::uint32_t> Indices;
std::uint64_t Triangles=0,SkirtTriangles=0;
@@ -70,7 +74,8 @@ namespace
if(!Read(File,SurfaceVertices)||!Read(File,SkirtCount)||!Read(File,VertexCount)
||!Read(File,IndexCount)||!Read(File,MaskCount)
||VertexCount>10000000||IndexCount>60000000||MaskCount>20000000
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3))
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3)
||Key[3]<0||Key[3]>8)
{Error="tile mesh header is malformed";return false;}
if(TotalVertices+VertexCount>std::numeric_limits<std::uint32_t>::max()
||TotalIndices+IndexCount>std::numeric_limits<std::size_t>::max())
@@ -86,6 +91,25 @@ namespace
for(float& Value:Scene.Vertices[I].Normal)if(!Read(File,Value))
{Error="tile normals are truncated";return false;}
Scene.Vertices[I].Height=Scene.Vertices[I].Position[2];
Scene.MinimumHeight=std::min(Scene.MinimumHeight,Scene.Vertices[I].Height);
Scene.MaximumHeight=std::max(Scene.MaximumHeight,Scene.Vertices[I].Height);
}
const double TileVoxels=static_cast<double>(32u<<Key[3]);
const float TileExtentMeters=static_cast<float>(TileVoxels
*static_cast<double>(Scene.VoxelSizeMicrons)/1000000.0);
const float TileOriginX=static_cast<float>(static_cast<double>(Key[0])
*TileExtentMeters);
const float TileOriginY=static_cast<float>(static_cast<double>(Key[1])
*TileExtentMeters);
for(std::size_t I=Base;I<Base+VertexCount;++I)
{
FVertex& Vertex=Scene.Vertices[I];
std::copy(std::begin(Vertex.Position),std::end(Vertex.Position),
std::begin(Vertex.WorldPosition));
Vertex.TileData[0]=TileOriginX;
Vertex.TileData[1]=TileOriginY;
Vertex.TileData[2]=TileExtentMeters;
Vertex.TileData[3]=static_cast<float>(Key[3]);
}
const std::size_t IndexOffset=Scene.Indices.size();
Scene.Indices.resize(IndexOffset+IndexCount);
@@ -151,8 +175,12 @@ precision highp float;
layout(location=0) in vec3 inCameraPosition;
layout(location=1) in vec3 inNormal;
layout(location=2) in float inHeight;
layout(location=3) in vec3 inWorldPosition;
layout(location=4) in vec4 inTileData;
out vec3 vNormal;
out float vHeight;
out vec3 vWorldPosition;
out vec4 vTileData;
uniform float uTanHalfHorizontal;
uniform float uTanHalfVertical;
uniform float uNear;
@@ -165,19 +193,54 @@ void main()
inCameraPosition.y/uTanHalfVertical,clipZ,z);
vNormal=normalize(inNormal);
vHeight=inHeight;
vWorldPosition=inWorldPosition;
vTileData=inTileData;
})GLSL";
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
precision highp float;
in vec3 vNormal;
in float vHeight;
in vec3 vWorldPosition;
in vec4 vTileData;
out vec4 outColor;
uniform int uReadableMode;
uniform int uTileEdges;
uniform float uHeightCenter;
uniform float uHeightRange;
uniform float uTileEdgeWidth;
uniform vec3 uLightDirection;
void main()
{
vec3 n=normalize(vNormal);
vec3 color;
if(uReadableMode==0)
{
vec3 normalColor=abs(n)*0.78+vec3(0.12);
float band=mod(floor((vHeight+10000.0)/80.0),2.0);
vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band);
outColor=vec4(mix(bandColor,normalColor,0.68),1.0);
color=mix(bandColor,normalColor,0.68);
}
else
{
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
vec3 heightTint=mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),heightT);
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
float illumination=0.62+0.38*diffuse;
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
}
if(uTileEdges!=0&&vTileData.z>0.0)
{
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
vec2 edge=min(local,vec2(vTileData.z)-local);
float distanceToTileEdge=min(edge.x,edge.y);
float antialias=max(fwidth(distanceToTileEdge),0.005);
float line=1.0-smoothstep(uTileEdgeWidth-antialias,
uTileEdgeWidth+antialias,distanceToTileEdge);
vec3 edgeColor=mix(vec3(0.16,0.22,0.25),vec3(0.30,0.23,0.14),
clamp(vTileData.w/8.0,0.0,1.0));
color=mix(color,edgeColor,0.32*line);
}
outColor=vec4(color,1.0);
})GLSL";
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
@@ -224,7 +287,8 @@ void main()
}
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
double HorizontalFovDegrees)
double HorizontalFovDegrees,bool ReadableMode,bool TileEdges,
double TileEdgeWidthMeters)
{
EGLDisplay Display=MakeDisplay();
EGLint Major=0,Minor=0;
@@ -304,6 +368,10 @@ void main()
reinterpret_cast<void*>(offsetof(FVertex,Normal)));
glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex),
reinterpret_cast<void*>(offsetof(FVertex,Height)));
glEnableVertexAttribArray(3);glVertexAttribPointer(3,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
reinterpret_cast<void*>(offsetof(FVertex,WorldPosition)));
glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
reinterpret_cast<void*>(offsetof(FVertex,TileData)));
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
@@ -312,6 +380,14 @@ void main()
glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast<float>(TanHalfVertical));
glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast<float>(Near));
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0);
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
static_cast<float>(Scene.CameraMeters[2]));
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"),
static_cast<float>(TileEdgeWidthMeters));
glUniform3f(glGetUniformLocation(Program,"uLightDirection"),-0.42f,-0.58f,0.70f);
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
glFinish();
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
@@ -323,9 +399,10 @@ void main()
{
std::error_code SizeError;
const auto Bytes=std::filesystem::file_size(Output,SizeError);
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu png_bytes=%llu status=PASS path=%s\n",
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
static_cast<unsigned long long>(Scene.Triangles),Scene.TileCount,
static_cast<unsigned long long>(Scene.SkirtTriangles),
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
}
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
@@ -341,12 +418,13 @@ int main(int argc,char** argv)
{
if(argc<3)
{
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N]\n",argv[0]);
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--tile-edges=on|off] [--tile-edge-width-m=N]\n",argv[0]);
return 2;
}
int Width=2560,Height=1440;double Fov=90.0;
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
bool HasCamera=false;std::array<double,3> Camera{};
bool ReadableMode=true,TileEdges=false;double TileEdgeWidthMeters=0.15;
for(int I=3;I<argc;++I)
{
const std::string_view Arg(argv[I]);
@@ -380,6 +458,28 @@ int main(int argc,char** argv)
{std::fprintf(stderr,"Invalid camera angle\n");return 2;}
if(IsYaw){Yaw=Parsed;HasYaw=true;}else{Pitch=Parsed;HasPitch=true;}
}
else if(Arg.starts_with("--shade="))
{
const std::string_view Mode=Arg.substr(8);
if(Mode=="readable")ReadableMode=true;
else if(Mode=="normal")ReadableMode=false;
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
}
else if(Arg.starts_with("--tile-edges="))
{
const std::string_view Mode=Arg.substr(13);
if(Mode=="on")TileEdges=true;
else if(Mode=="off")TileEdges=false;
else {std::fprintf(stderr,"Invalid tile-edge mode\n");return 2;}
}
else if(Arg.starts_with("--tile-edge-width-m="))
{
char* End=nullptr;const std::string Value(Arg.substr(20));
TileEdgeWidthMeters=std::strtod(Value.c_str(),&End);
if(End==Value.c_str()||*End!='\0'||!std::isfinite(TileEdgeWidthMeters)
||TileEdgeWidthMeters<=0.0||TileEdgeWidthMeters>5.0)
{std::fprintf(stderr,"Invalid tile-edge width\n");return 2;}
}
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
}
FScene Scene;std::string Error;
@@ -388,10 +488,12 @@ int main(int argc,char** argv)
if(HasCamera)Scene.CameraMeters=Camera;
if(HasYaw)Scene.YawDegrees=Yaw;
if(HasPitch)Scene.PitchDegrees=Pitch;
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f vertices=%zu triangles=%llu\n",
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s tile_edges=%s vertices=%zu triangles=%llu\n",
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
Scene.YawDegrees,Scene.PitchDegrees,Scene.Vertices.size(),
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
ReadableMode?"readable":"normal",TileEdges?"on":"off",Scene.Vertices.size(),
static_cast<unsigned long long>(Scene.Triangles));
return Render(Scene,argv[2],Width,Height,Fov)?0:3;
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,TileEdges,
TileEdgeWidthMeters)?0:3;
}