SpeedTree: read the .spt collision and composite texcoord blocks

The CSpeedTreeRT stand-in now returns the collision objects stored in
the .spt (12000 section: spheres and cylinders) instead of one fixed trunk
cylinder, and takes leaf texture rects from the 10000 section instead of
hand-picked atlas rects (trees without leaf maps use the frond rect).
SpeedGrass/BoundaryShapeManager are N_A (no 40250 caller).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 12:20:34 +09:00
co-authored by Claude Opus 5.5
parent 17cb7326c1
commit ed61b2d20f
8 changed files with 529 additions and 55 deletions
+2
View File
@@ -571,3 +571,5 @@
{"date": "2026-09-28T05:29:23+00:00", "unit": "EterLib/GrpImageTexture.cpp", "action": "unit K: DXT DDS textures upload the file's own mip levels (40250 CreateDDSTexture: max(1,dwMipMapCount), dwLinearSize>>2i Copy) instead of a regenerated chain; non-DDS keep the D3DX_DEFAULT full chain; mem: textures one level; invented is_ui_tex no-mip rule (icon/ui/locale/<=128px) deleted; dxt_decode_test registered in ctest with mip-level cases", "evidence": "ctest -j3 36/36; fake-server spawn frame 900: near terrain diff 0.00, far terrain 2.7/255 (minified levels now from the file), rest is UI timing (area title/chat); far-terrain look NEEDS_LIVE"} {"date": "2026-09-28T05:29:23+00:00", "unit": "EterLib/GrpImageTexture.cpp", "action": "unit K: DXT DDS textures upload the file's own mip levels (40250 CreateDDSTexture: max(1,dwMipMapCount), dwLinearSize>>2i Copy) instead of a regenerated chain; non-DDS keep the D3DX_DEFAULT full chain; mem: textures one level; invented is_ui_tex no-mip rule (icon/ui/locale/<=128px) deleted; dxt_decode_test registered in ctest with mip-level cases", "evidence": "ctest -j3 36/36; fake-server spawn frame 900: near terrain diff 0.00, far terrain 2.7/255 (minified levels now from the file), rest is UI timing (area title/chat); far-terrain look NEEDS_LIVE"}
{"date": "2026-09-28T05:33:21+00:00", "unit": "SpeedTreeLib/SpeedTreeWrapper.cpp", "action": "SpeedTree follow-up closed without code change: .spt 0x4E25 is one composite rect per file (87/87, independent of 1-3 leaf maps) and overlays on the atlases put it on the billboard tree image for most species, so it is not the leaf-map texcoords; the hand-picked leaf rects stay DIVERGENT, note updated", "evidence": "byte dump of b1_baobab_rt.spt tail (0x4E20/0x4E22 name/0x4E23 b/0x4E24 b/0x4E25 8f/0x4E21); count over 87 .spt files; atlas overlays of 9 species"} {"date": "2026-09-28T05:33:21+00:00", "unit": "SpeedTreeLib/SpeedTreeWrapper.cpp", "action": "SpeedTree follow-up closed without code change: .spt 0x4E25 is one composite rect per file (87/87, independent of 1-3 leaf maps) and overlays on the atlases put it on the billboard tree image for most species, so it is not the leaf-map texcoords; the hand-picked leaf rects stay DIVERGENT, note updated", "evidence": "byte dump of b1_baobab_rt.spt tail (0x4E20/0x4E22 name/0x4E23 b/0x4E24 b/0x4E25 8f/0x4E21); count over 87 .spt files; atlas overlays of 9 species"}
{"date": "2026-09-30T03:07:37+00:00", "unit": "MilesLib/*", "action": "MilesLib platform port: 40250 SoundBase/SoundData/SoundInstance2D/3D/Stream/SoundManager*/Type copied verbatim (only includes + one unused QPC local differ) over an AIL_* software mixer (MilesAil.cpp: 2D balance pan, 3D inverse distance min 1/max 200 + listener pan, threaded stream decode, loop counts, decompress_ASI/ADPCM to float WAV); host NativeAudioEngine drains MtMilesMix; CSoundManager Create/Destroy in PythonBoot (IsNoSoundCard), listener+Update from PythonApplicationCamera, Save/RestoreVolume on window focus, CSoundData::SetPackMode in PackInitialize; old AudioCommands/SoundInstance bridge deleted; 142/142 PORTED", "evidence": "port_miles_test ok (2D/3D/stream fades/DataToInstance); ctest all pass except pre-existing flaky login_flow; desktop live client MT_AUDIO_TRACE peaks 1.17/0.70 at login and ~0.02 in game; Android device not attached, NEEDS device run"} {"date": "2026-09-30T03:07:37+00:00", "unit": "MilesLib/*", "action": "MilesLib platform port: 40250 SoundBase/SoundData/SoundInstance2D/3D/Stream/SoundManager*/Type copied verbatim (only includes + one unused QPC local differ) over an AIL_* software mixer (MilesAil.cpp: 2D balance pan, 3D inverse distance min 1/max 200 + listener pan, threaded stream decode, loop counts, decompress_ASI/ADPCM to float WAV); host NativeAudioEngine drains MtMilesMix; CSoundManager Create/Destroy in PythonBoot (IsNoSoundCard), listener+Update from PythonApplicationCamera, Save/RestoreVolume on window focus, CSoundData::SetPackMode in PackInitialize; old AudioCommands/SoundInstance bridge deleted; 142/142 PORTED", "evidence": "port_miles_test ok (2D/3D/stream fades/DataToInstance); ctest all pass except pre-existing flaky login_flow; desktop live client MT_AUDIO_TRACE peaks 1.17/0.70 at login and ~0.02 in game; Android device not attached, NEEDS device run"}
{"date": "2026-09-30T03:19:50+00:00", "unit": "SpeedTreeLib/SpeedTreeWrapper.cpp", "action": "CSpeedTreeRT stand-in reads the .spt collision block (12000: 12002 sphere, 12003 cylinder) and the composite texcoord block (10000: 10002 leaf maps, 10003 frond, 10004 billboard) instead of a fixed trunk cylinder and hand-picked leaf rects; GetCollisionObjectCount/GetCollisionObject DIVERGENT -> PORTED; SetupLeafBuffers note updated (earlier 'no per-leaf-map texcoords' conclusion was wrong, the rects are in section 10000)", "evidence": "port_speedtree_test ok; token layout checked over 118 .spt extracted from the 40250 packs; fake-server Yongan view (camera 4500/3/0): tree crown now drawn from its own leaf-map cells, the wrong fruit/autumn cell is gone"}
{"date": "2026-09-30T03:19:50+00:00", "unit": "SpeedTreeLib/SpeedGrassRT.cpp", "action": "SpeedGrassRT/SpeedGrassWrapper/BoundaryShapeManager N_A: compiled (USE_SPEEDGRASS) but no caller outside those files", "evidence": "LC_ALL=C grep over ClientVS22/source for CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager"}
@@ -0,0 +1,32 @@
{
"reference": "SpeedTreeLib/BoundaryShapeManager.cpp",
"reference_sha256": "6fdfc42ca249991740567658bc0dced8dedddb387e2f4e1dcbad53b0d38fc964",
"priority": "P4",
"contracts": [],
"functions": {
"CBoundaryShapeManager::CBoundaryShapeManager": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CBoundaryShapeManager::~CBoundaryShapeManager": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CBoundaryShapeManager::LoadBsfFile": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CBoundaryShapeManager::PointInside": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CBoundaryShapeManager::PointInShape": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CBoundaryShapeManager::RandomPoint": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
}
}
}
@@ -0,0 +1,100 @@
{
"reference": "SpeedTreeLib/SpeedGrassRT.cpp",
"reference_sha256": "1fbd873c76a7aa57935035ea45a720527b9fd8ee478352806934fbb5f9c86d42",
"priority": "P4",
"contracts": [],
"functions": {
"VecInterpolate": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SBlade::SBlade": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SRegion::SRegion": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::CSpeedGrassRT": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::~CSpeedGrassRT": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::DeleteRegions": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::GetRegions": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::ParseBsfFile": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::CustomPlacement": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::GetLodParams": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SetLodParams": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::Cull": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SetWindDirection": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::GetWindDirection": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::GetCameraPos": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SetCamera": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::SetPerspective": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::CreateRegions": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::RotateAxisFromIdentity": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::ComputeFrustum": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::ComputeUnitBillboard": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::ConvertCoordsToCell": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassRT::OutsideFrustum": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
}
}
}
@@ -0,0 +1,36 @@
{
"reference": "SpeedTreeLib/SpeedGrassWrapper.cpp",
"reference_sha256": "c737f71f0115acabec0eb1ba4cf4a0ec9a169c8ac5f9ac32a789d2194ac00f53",
"priority": "P4",
"contracts": [],
"functions": {
"CSpeedGrassWrapper::CSpeedGrassWrapper": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::~CSpeedGrassWrapper": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::Draw": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::InitFromBsfFile": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::Color": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::Height": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
},
"CSpeedGrassWrapper::InitGraphics": {
"status": "N_A",
"note": "compiled in 40250 (Constants.h defines USE_SPEEDGRASS) but unreachable: CSpeedGrassRT/CSpeedGrassWrapper/CBoundaryShapeManager are referenced only inside SpeedGrass*/BoundaryShapeManager* (grep over ClientVS22/source); nothing to port"
}
}
}
@@ -68,7 +68,8 @@
"impl": [ "impl": [
"src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::SetupLeafBuffers" "src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::SetupLeafBuffers"
], ],
"note": "verbatim. DIVERGENT input: the stand-in's leaf clusters and composite-atlas rects are hand-picked (the .spt stores no per-leaf-map texcoords: leaf entries 0x0FA0-0x0FA7 carry colour/path/origin/size only, and the single 8-float 0x4E25 rect in the 0x4E20 composite block, one per file for 1-3 leaf maps, frames the billboard cell in most atlases; billboards never render in 40250 since SetLodLevel(1.0)); NEEDS_LIVE" "note": "verbatim. Leaf texture coordinates are the .spt composite-atlas rects (10000 section: 10002 leaf maps, 10003 frond map, 10004 billboard, 8 floats per card, v flipped under SetTextureFlip), replacing the hand-picked rects; a tree without leaf maps (palms, ferns, bare winter trees) draws the proxy clusters with its frond rect. DIVERGENT input remains: cluster positions/sizes are the stand-in's procedural proxy, not the closed engine's; NEEDS_LIVE",
"test": "tests/port/port_speedtree_test.cpp"
}, },
"CSpeedTreeWrapper::Advance": { "CSpeedTreeWrapper::Advance": {
"status": "PORTED", "status": "PORTED",
@@ -196,18 +197,20 @@
] ]
}, },
"CSpeedTreeWrapper::GetCollisionObjectCount": { "CSpeedTreeWrapper::GetCollisionObjectCount": {
"status": "DIVERGENT", "status": "PORTED",
"impl": [ "impl": [
"src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::GetCollisionObjectCount" "src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::GetCollisionObjectCount"
], ],
"note": "verbatim; the stand-in returns one trunk CO_CYLINDER (radius 35, height 750), not the .spt collision objects" "note": "verbatim; the CSpeedTreeRT stand-in returns the .spt collision block as stored (12000 section: 12002 sphere x y z r, 12003 cylinder x y z r h; 1-5 objects, none for aloe/fern ground plants, checked on all 118 pack .spt); the wrapper passes no size override (LoadTree fSize -1), so no SetTreeSize rescale applies",
"test": "tests/port/port_speedtree_test.cpp"
}, },
"CSpeedTreeWrapper::GetCollisionObject": { "CSpeedTreeWrapper::GetCollisionObject": {
"status": "DIVERGENT", "status": "PORTED",
"impl": [ "impl": [
"src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::GetCollisionObject" "src/port/SpeedTreeLib/SpeedTreeWrapper.cpp:CSpeedTreeWrapper::GetCollisionObject"
], ],
"note": "verbatim; the stand-in returns one trunk CO_CYLINDER (radius 35, height 750), not the .spt collision objects" "note": "verbatim; the CSpeedTreeRT stand-in returns the .spt collision block as stored (12000 section: 12002 sphere x y z r, 12003 cylinder x y z r h; 1-5 objects, none for aloe/fern ground plants, checked on all 118 pack .spt); the wrapper passes no size override (LoadTree fSize -1), so no SetTreeSize rescale applies",
"test": "tests/port/port_speedtree_test.cpp"
}, },
"CSpeedTreeWrapper::GetPosition": { "CSpeedTreeWrapper::GetPosition": {
"status": "PORTED", "status": "PORTED",
+126 -50
View File
@@ -12,11 +12,15 @@
// inactive; there is only that LOD. // inactive; there is only that LOD.
// - GetTextures returns bare file names: the wrapper prefixes the .spt directory. // - GetTextures returns bare file names: the wrapper prefixes the .spt directory.
// - GetTreeSize reads the Size / SizeVariance header tokens (2001 / 2002) of the .spt. // - GetTreeSize reads the Size / SizeVariance header tokens (2001 / 2002) of the .spt.
// - GetCollisionObject returns the .spt collision block (12000 section) as stored: 12002 sphere
// (x, y, z, radius), 12003 cylinder (x, y, z, radius, height); a tree without the section has none.
// - Leaf texture coordinates are the .spt composite-atlas rects (10000 section), v flipped under
// SetTextureFlip.
// What is not the SDK (DIVERGENT; the notes are on the wrapper in audit/port-map/SpeedTreeLib/): // What is not the SDK (DIVERGENT; the notes are on the wrapper in audit/port-map/SpeedTreeLib/):
// - Branch and leaf geometry cannot be generated from the .spt parameters without the closed // - Branch and leaf geometry cannot be generated from the .spt parameters without the closed
// engine; it is the previous procedural proxy (species hints from the file name, 1000-unit // engine; it is the previous procedural proxy (species hints from the file name, 1000-unit
// height, crossed-branch trunk, hand-picked composite atlas rects), no fronds. // height, crossed-branch trunk, leaf clusters), no fronds; a tree without leaf maps draws its
// - Collision objects are one trunk cylinder; the .spt collision block is not parsed. // proxy clusters with its frond-map rect.
// - Materials are the SDK defaults (white); the .spt material block is not parsed. // - Materials are the SDK defaults (white); the .spt material block is not parsed.
#include "SpeedTreeLib/StdAfx.h" #include "SpeedTreeLib/StdAfx.h"
@@ -116,47 +120,56 @@ uint32_t species_seed(const std::string & s)
return h; return h;
} }
// Composite-atlas leaf rects, D3D image space (v down). // Composite-atlas rect, D3D image space (v down).
struct UVRect struct UVRect
{ {
float u0 = 0.0f, v0 = 0.0f, u1 = 1.0f, v1 = 1.0f; float u0 = 0.0f, v0 = 0.0f, u1 = 1.0f, v1 = 1.0f;
}; };
std::vector<UVRect> foliage_rects(const std::string & species, const std::string & composite) // .spt texture-coordinate block: 10000, then 10002 <count> leaf maps, 10003 <count> frond maps, 10004
// <count> billboards, each entry 8 floats: the four card corners (+r,+u) (-r,+u) (-r,-u) (+r,-u) as
// (u, v) in the composite atlas with v up (the SDK flips v under SetTextureFlip), then 10001. Checked on
// the 118 .spt files of the 40250 packs: 0-2 leaf maps, always 1 frond map, 0-1 billboards.
struct SSptTexCoords
{ {
if (composite.empty()) std::vector<UVRect> vLeaves, vFronds, vBillboards;
return { { 0.0f, 0.0f, 1.0f, 1.0f } }; };
const std::string s = to_lower_str(species);
const std::string c = to_lower_str(composite); bool read_spt_texcoords(const unsigned char * p, size_t n, SSptTexCoords & out)
const bool fall = s.find("fall") != std::string::npos; {
const bool winter = s.find("winter") != std::string::npos; auto i32_at = [&](size_t o) {
if (c.find("b1") != std::string::npos) int32_t v = 0;
std::memcpy(&v, p + o, 4);
return v;
};
for (size_t o = 0; o + 8 <= n; ++o)
{ {
if (fall) if (i32_at(o) != 10000 || i32_at(o + 4) != 10002)
return { { 0.00f, 0.05f, 0.25f, 0.25f }, { 0.25f, 0.25f, 0.50f, 0.50f } }; continue;
return { { 0.25f, 0.02f, 0.50f, 0.23f }, { 0.25f, 0.18f, 0.50f, 0.36f }, { 0.00f, 0.27f, 0.27f, 0.49f } }; SSptTexCoords coords;
size_t c = o + 4;
for (int32_t token : { 10002, 10003, 10004 })
{
if (c + 8 > n || i32_at(c) != token)
return false;
const int32_t count = i32_at(c + 4);
c += 8;
if (count < 0 || count > 64 || c + size_t(count) * 32 > n)
return false;
std::vector<UVRect> & rects = token == 10002 ? coords.vLeaves : token == 10003 ? coords.vFronds : coords.vBillboards;
for (int32_t i = 0; i < count; ++i, c += 32)
{
float f[8];
std::memcpy(f, p + c, sizeof(f));
rects.push_back({ f[2], 1.0f - f[1], f[0], 1.0f - f[5] });
}
}
if (c + 4 > n || i32_at(c) != 10001)
return false;
out = std::move(coords);
return true;
} }
if (c.find("b2") != std::string::npos) return false;
{
if (fall)
return { { 0.00f, 0.00f, 0.25f, 0.25f }, { 0.25f, 0.25f, 0.50f, 0.50f } };
return { { 0.50f, 0.38f, 0.75f, 0.63f }, { 0.50f, 0.63f, 0.75f, 0.88f }, { 0.00f, 0.38f, 0.25f, 0.62f } };
}
if (c.find("b3") != std::string::npos)
{
if (fall)
return { { 0.25f, 0.25f, 0.50f, 0.50f } };
return { { 0.00f, 0.25f, 0.25f, 0.50f }, { 0.00f, 0.50f, 0.25f, 0.75f }, { 0.25f, 0.50f, 0.50f, 0.75f } };
}
if (c.find("n1") != std::string::npos)
{
if (winter)
return { { 0.00f, 0.36f, 0.50f, 0.58f }, { 0.25f, 0.55f, 0.52f, 0.75f } };
return { { 0.00f, 0.72f, 0.28f, 0.96f }, { 0.25f, 0.74f, 0.53f, 0.97f } };
}
if (c.find("n2") != std::string::npos)
return { { 0.00f, 0.48f, 0.27f, 0.75f }, { 0.26f, 0.73f, 0.58f, 1.00f }, { 0.75f, 0.48f, 1.00f, 0.80f } };
return { { 0.0f, 0.0f, 1.0f, 1.0f } };
} }
// .spt header: 1000 <len> "__IdvSpt_02_", 1002, 2000 <len> <branch texture>, 2001 <float size>, // .spt header: 1000 <len> "__IdvSpt_02_", 1002, 2000 <len> <branch texture>, 2001 <float size>,
@@ -186,6 +199,59 @@ bool read_spt_size(const unsigned char * p, size_t n, float & size, float & vari
return true; return true;
} }
struct SCollisionObject
{
CSpeedTreeRT::ECollisionObjectType eType = CSpeedTreeRT::CO_SPHERE;
float afPosition[3] = { 0.0f, 0.0f, 0.0f };
float afDimensions[3] = { 0.0f, 0.0f, 0.0f };
};
// .spt collision block: 12000, then per object 12002 <x y z radius> (sphere) or 12003 <x y z radius
// height> (cylinder) as floats, then 12001. The section sits after the texture-coordinate block (10000
// .. 10001) and before 13000; small ground plants (aloe flowers, ferns) have none. Checked on all 118
// .spt files of the 40250 packs: 1-5 objects, only these two tokens.
void read_spt_collision(const unsigned char * p, size_t n, std::vector<SCollisionObject> & out)
{
auto i32_at = [&](size_t o) {
int32_t v = 0;
std::memcpy(&v, p + o, 4);
return v;
};
for (size_t o = 0; o + 8 <= n; ++o)
{
if (i32_at(o) != 12000)
continue;
const int32_t next = i32_at(o + 4);
if (next != 12001 && next != 12002 && next != 12003)
continue;
std::vector<SCollisionObject> objects;
size_t c = o + 4;
for (;;)
{
if (c + 4 > n)
return;
const int32_t t = i32_at(c);
c += 4;
if (t == 12001)
break;
const size_t count = t == 12002 ? 4 : t == 12003 ? 5 : 0;
if (count == 0 || c + count * 4 > n)
return;
SCollisionObject obj;
obj.eType = t == 12002 ? CSpeedTreeRT::CO_SPHERE : CSpeedTreeRT::CO_CYLINDER;
float f[5] = {};
std::memcpy(f, p + c, count * 4);
c += count * 4;
std::memcpy(obj.afPosition, f, sizeof(obj.afPosition));
obj.afDimensions[0] = f[3];
obj.afDimensions[1] = count == 5 ? f[4] : 0.0f;
objects.push_back(obj);
}
out = std::move(objects);
return;
}
}
struct SStaticLight struct SStaticLight
{ {
float afAttributes[16] = { float afAttributes[16] = {
@@ -258,7 +324,8 @@ struct STreeData
std::vector<const float *> vLeafTexPtrs, vLeafCoordPtrs; std::vector<const float *> vLeafTexPtrs, vLeafCoordPtrs;
float afBoundingBox[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; float afBoundingBox[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
float fTrunkRadius = 35.0f, fTrunkHeight = 600.0f; std::vector<SCollisionObject> vCollisionObjects;
SSptTexCoords texCoords;
float afBranchMaterial[13], afFrondMaterial[13], afLeafMaterial[13]; float afBranchMaterial[13], afFrondMaterial[13], afLeafMaterial[13];
float fLeafLightingAdjustment = 0.75f; float fLeafLightingAdjustment = 0.75f;
bool bComputed = false; bool bComputed = false;
@@ -285,6 +352,8 @@ struct STreeData
strCompositeTexture = basename_only(info.composite_texture); strCompositeTexture = basename_only(info.composite_texture);
strSelfShadowTexture = basename_only(info.self_shadow_texture); strSelfShadowTexture = basename_only(info.self_shadow_texture);
read_spt_size(p, n, fFileSize, fFileVariance); read_spt_size(p, n, fFileSize, fFileVariance);
read_spt_collision(p, n, vCollisionObjects);
read_spt_texcoords(p, n, texCoords);
fSize = fFileSize; fSize = fFileSize;
fVariance = fFileVariance; fVariance = fFileVariance;
} }
@@ -366,7 +435,11 @@ struct STreeData
void BuildLeaves(float H, bool conifer, bool palm) void BuildLeaves(float H, bool conifer, bool palm)
{ {
const std::vector<UVRect> rects = foliage_rects(strName, strCompositeTexture); // Leaf clusters use the tree's leaf maps; a tree without leaf maps (palms, ferns, bare winter
// trees) gets its foliage from fronds in the SDK, which the proxy stands in for with the frond map.
std::vector<UVRect> rects = texCoords.vLeaves.empty() ? texCoords.vFronds : texCoords.vLeaves;
if (rects.empty())
rects.push_back(UVRect());
const uint32_t seed = species_seed(strName); const uint32_t seed = species_seed(strName);
const int count = palm ? 16 : 24; const int count = palm ? 16 : 24;
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
@@ -418,8 +491,6 @@ struct STreeData
afBoundingBox[3] = R; afBoundingBox[3] = R;
afBoundingBox[4] = R; afBoundingBox[4] = R;
afBoundingBox[5] = H; afBoundingBox[5] = H;
fTrunkRadius = H * 0.035f;
fTrunkHeight = H * 0.75f;
// PORT: the Godot host draws its own trees unless the native terrain renderer is on; the tree // PORT: the Godot host draws its own trees unless the native terrain renderer is on; the tree
// then has no geometry so the forest submits nothing. // then has no geometry so the forest submits nothing.
@@ -846,18 +917,23 @@ void CSpeedTreeRT::GetBoundingBox(float * pBounds) const
std::memcpy(pBounds, m_pImpl->pTree->afBoundingBox, 6 * sizeof(float)); std::memcpy(pBounds, m_pImpl->pTree->afBoundingBox, 6 * sizeof(float));
} }
unsigned int CSpeedTreeRT::GetCollisionObjectCount(void) { return 1; } unsigned int CSpeedTreeRT::GetCollisionObjectCount(void)
void CSpeedTreeRT::GetCollisionObject(unsigned int /*nIndex*/, ECollisionObjectType & eType, float * pPosition, float * pDimensions)
{ {
const STreeData & tree = *m_pImpl->pTree; return static_cast<unsigned int>(m_pImpl->pTree->vCollisionObjects.size());
eType = CO_CYLINDER; }
void CSpeedTreeRT::GetCollisionObject(unsigned int nIndex, ECollisionObjectType & eType, float * pPosition, float * pDimensions)
{
const auto & objects = m_pImpl->pTree->vCollisionObjects;
if (nIndex >= objects.size())
{
SetError("SpeedTreeRT: collision object index out of range");
return;
}
const auto & o = objects[nIndex];
eType = o.eType;
if (pPosition) if (pPosition)
pPosition[0] = pPosition[1] = pPosition[2] = 0.0f; std::memcpy(pPosition, o.afPosition, sizeof(o.afPosition));
if (pDimensions) if (pDimensions)
{ std::memcpy(pDimensions, o.afDimensions, sizeof(o.afDimensions));
pDimensions[0] = tree.fTrunkRadius;
pDimensions[1] = tree.fTrunkHeight;
pDimensions[2] = 0.0f;
}
} }
+5
View File
@@ -200,6 +200,11 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
target_link_libraries(port_miles_test PRIVATE port_platform) target_link_libraries(port_miles_test PRIVATE port_platform)
add_test(NAME port.miles COMMAND $<TARGET_FILE:port_miles_test>) add_test(NAME port.miles COMMAND $<TARGET_FILE:port_miles_test>)
# SpeedTreeLib: the CSpeedTreeRT stand-in's .spt size and collision blocks.
add_executable(port_speedtree_test ${PROJECT_SOURCE_DIR}/tests/port/port_speedtree_test.cpp)
target_link_libraries(port_speedtree_test PRIVATE port_platform)
add_test(NAME port.speedtree COMMAND $<TARGET_FILE:port_speedtree_test>)
# 批次 2V1: Packet.h struct sizes against the classic wire layouts. # 批次 2V1: Packet.h struct sizes against the classic wire layouts.
add_executable(port_packet_test ${PROJECT_SOURCE_DIR}/tests/port/port_packet_test.cpp add_executable(port_packet_test ${PROJECT_SOURCE_DIR}/tests/port/port_packet_test.cpp
${PROJECT_SOURCE_DIR}/tests/port/port_packet_test_wire.cpp) ${PROJECT_SOURCE_DIR}/tests/port/port_packet_test_wire.cpp)
+220
View File
@@ -0,0 +1,220 @@
// SpeedTreeLib: the CSpeedTreeRT stand-in (platform/SpeedTreeLib/SpeedTreeRT.cpp) reads the .spt size and
// collision blocks the 40250 wrapper asks for (GetTreeSize, GetCollisionObject in OnUpdateCollisionData).
// The blocks are built here in the .spt token layout of the 40250 pack files.
#include "SpeedTreeLib/StdAfx.h"
#include <SpeedTreeRT.h>
#include "../../src/platform/EterLib/RenderCommands3D.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
#define CHECK_NEAR(a, b) CHECK(std::fabs(double(a) - double(b)) < 1e-3)
namespace
{
struct Block
{
std::vector<unsigned char> bytes;
void i32(int32_t v) { bytes.insert(bytes.end(), reinterpret_cast<unsigned char *>(&v), reinterpret_cast<unsigned char *>(&v) + 4); }
void f32(float v) { bytes.insert(bytes.end(), reinterpret_cast<unsigned char *>(&v), reinterpret_cast<unsigned char *>(&v) + 4); }
void str(const std::string & s)
{
i32(int32_t(s.size()));
bytes.insert(bytes.end(), s.begin(), s.end());
}
};
// 1000 "__IdvSpt_02_", 1002, 2000 <bark>, 2001 size, 2002 variance, then filler up to the collision block.
Block header(float size, float variance)
{
Block b;
b.i32(1000);
b.str("__IdvSpt_02_");
b.i32(1002);
b.i32(2000);
b.str("BaobabBark.tga");
b.i32(2001);
b.f32(size);
b.i32(2002);
b.f32(variance);
// texture-coordinate block: two leaf maps, one frond map, one billboard; corners (+r,+u) (-r,+u)
// (-r,-u) (+r,-u) with v up (b1_baobab_rt.spt)
b.i32(10000);
b.i32(10002);
b.i32(2);
for (float f : { 0.5f, 0.25f, 0.25f, 0.25f, 0.25f, 0.0f, 0.5f, 0.0f })
b.f32(f);
for (float f : { 0.25f, 0.25f, 0.0f, 0.25f, 0.0f, 0.0f, 0.25f, 0.0f })
b.f32(f);
b.i32(10003);
b.i32(1);
for (float f : { 0.625f, 0.735f, 0.5f, 0.735f, 0.5f, 0.613f, 0.625f, 0.613f })
b.f32(f);
b.i32(10004);
b.i32(1);
for (float f : { 0.75f, 0.985f, 0.625f, 0.985f, 0.625f, 0.735f, 0.75f, 0.735f })
b.f32(f);
b.i32(10001);
return b;
}
// The same block without leaf maps (palms, ferns, bare winter trees).
Block header_no_leaves()
{
Block b;
b.i32(1000);
b.str("__IdvSpt_02_");
b.i32(1002);
b.i32(2000);
b.str("PalmBark.tga");
b.i32(2001);
b.f32(50.0f);
b.i32(2002);
b.f32(0.0f);
b.i32(10000);
b.i32(10002);
b.i32(0);
b.i32(10003);
b.i32(1);
for (float f : { 0.375f, 0.625f, 0.25f, 0.625f, 0.25f, 0.5f, 0.375f, 0.5f })
b.f32(f);
b.i32(10004);
b.i32(0);
b.i32(10001);
return b;
}
const float * leaf_texcoords(CSpeedTreeRT & tree, unsigned i)
{
CSpeedTreeRT::SGeometry geometry;
tree.GetGeometry(geometry, SpeedTree_LeafGeometry);
CHECK(geometry.m_sLeaves0.m_usLeafCount > i);
return geometry.m_sLeaves0.m_usLeafCount > i ? geometry.m_sLeaves0.m_pLeafMapTexCoords[i] : nullptr;
}
void test_leaf_texcoords()
{
SetNativeTerrainRenderEnabled(true);
CSpeedTreeRT::SetTextureFlip(true); // as CSpeedTreeWrapper::LoadTree
Block b = header(1100.0f, 150.0f);
CSpeedTreeRT tree;
CHECK(tree.LoadTree(b.bytes.data(), unsigned(b.bytes.size())));
CHECK(tree.Compute(nullptr, 1));
// leaf 0: leaf map 0, v flipped
if (const float * t = leaf_texcoords(tree, 0))
{
const float want[8] = { 0.5f, 0.75f, 0.25f, 0.75f, 0.25f, 1.0f, 0.5f, 1.0f };
for (int k = 0; k < 8; ++k)
CHECK_NEAR(t[k], want[k]);
}
// leaf 1: leaf map 1, mirrored in u by the proxy
if (const float * t = leaf_texcoords(tree, 1))
{
const float want[8] = { 0.0f, 0.75f, 0.25f, 0.75f, 0.25f, 1.0f, 0.0f, 1.0f };
for (int k = 0; k < 8; ++k)
CHECK_NEAR(t[k], want[k]);
}
// no leaf maps: the proxy clusters use the frond map
Block palm = header_no_leaves();
CSpeedTreeRT palmTree;
CHECK(palmTree.LoadTree(palm.bytes.data(), unsigned(palm.bytes.size())));
CHECK(palmTree.Compute(nullptr, 1));
if (const float * t = leaf_texcoords(palmTree, 0))
{
const float want[8] = { 0.375f, 0.375f, 0.25f, 0.375f, 0.25f, 0.5f, 0.375f, 0.5f };
for (int k = 0; k < 8; ++k)
CHECK_NEAR(t[k], want[k]);
}
SetNativeTerrainRenderEnabled(false);
}
void test_collision()
{
Block b = header(1100.0f, 150.0f);
b.i32(12000);
b.i32(12003); // cylinder x y z radius height
b.f32(0.0f);
b.f32(0.0f);
b.f32(0.0f);
b.f32(164.47f);
b.f32(582.55f);
b.i32(12002); // sphere x y z radius
b.f32(-10.44f);
b.f32(18.5f);
b.f32(775.42f);
b.f32(450.15f);
b.i32(12001);
b.i32(13000);
b.i32(13001);
CSpeedTreeRT tree;
CHECK(tree.LoadTree(b.bytes.data(), unsigned(b.bytes.size())));
float size = 0.0f, variance = 0.0f;
tree.GetTreeSize(size, variance);
CHECK_NEAR(size, 1100.0f);
CHECK_NEAR(variance, 150.0f);
CHECK(tree.GetCollisionObjectCount() == 2);
CSpeedTreeRT::ECollisionObjectType type = CSpeedTreeRT::CO_BOX;
float pos[3] = {}, dim[3] = {};
tree.GetCollisionObject(0, type, pos, dim);
CHECK(type == CSpeedTreeRT::CO_CYLINDER);
CHECK_NEAR(pos[0], 0.0f);
CHECK_NEAR(pos[2], 0.0f);
CHECK_NEAR(dim[0], 164.47f);
CHECK_NEAR(dim[1], 582.55f);
tree.GetCollisionObject(1, type, pos, dim);
CHECK(type == CSpeedTreeRT::CO_SPHERE);
CHECK_NEAR(pos[0], -10.44f);
CHECK_NEAR(pos[1], 18.5f);
CHECK_NEAR(pos[2], 775.42f);
CHECK_NEAR(dim[0], 450.15f);
// instances share the tree data
CSpeedTreeRT * instance = tree.MakeInstance();
CHECK(instance->GetCollisionObjectCount() == 2);
delete instance;
}
void test_no_collision()
{
// aloe flowers / ferns: no 12000 section, no collision objects
Block b = header(50.0f, 0.0f);
b.i32(13000);
b.i32(13001);
CSpeedTreeRT tree;
CHECK(tree.LoadTree(b.bytes.data(), unsigned(b.bytes.size())));
CHECK(tree.GetCollisionObjectCount() == 0);
}
} // namespace
int main()
{
test_collision();
test_no_collision();
test_leaf_texcoords();
if (g_failures)
{
std::fprintf(stderr, "port_speedtree_test: %d failure(s)\n", g_failures);
return 1;
}
std::printf("port_speedtree_test: ok\n");
return 0;
}