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
+126 -50
View File
@@ -12,11 +12,15 @@
// inactive; there is only that LOD.
// - GetTextures returns bare file names: the wrapper prefixes the .spt directory.
// - 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/):
// - 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
// height, crossed-branch trunk, hand-picked composite atlas rects), no fronds.
// - Collision objects are one trunk cylinder; the .spt collision block is not parsed.
// height, crossed-branch trunk, leaf clusters), no fronds; a tree without leaf maps draws its
// proxy clusters with its frond-map rect.
// - Materials are the SDK defaults (white); the .spt material block is not parsed.
#include "SpeedTreeLib/StdAfx.h"
@@ -116,47 +120,56 @@ uint32_t species_seed(const std::string & s)
return h;
}
// Composite-atlas leaf rects, D3D image space (v down).
// Composite-atlas rect, D3D image space (v down).
struct UVRect
{
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())
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);
const bool fall = s.find("fall") != std::string::npos;
const bool winter = s.find("winter") != std::string::npos;
if (c.find("b1") != std::string::npos)
std::vector<UVRect> vLeaves, vFronds, vBillboards;
};
bool read_spt_texcoords(const unsigned char * p, size_t n, SSptTexCoords & 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 (fall)
return { { 0.00f, 0.05f, 0.25f, 0.25f }, { 0.25f, 0.25f, 0.50f, 0.50f } };
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 } };
if (i32_at(o) != 10000 || i32_at(o + 4) != 10002)
continue;
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)
{
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 } };
return false;
}
// .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;
}
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
{
float afAttributes[16] = {
@@ -258,7 +324,8 @@ struct STreeData
std::vector<const float *> vLeafTexPtrs, vLeafCoordPtrs;
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 fLeafLightingAdjustment = 0.75f;
bool bComputed = false;
@@ -285,6 +352,8 @@ struct STreeData
strCompositeTexture = basename_only(info.composite_texture);
strSelfShadowTexture = basename_only(info.self_shadow_texture);
read_spt_size(p, n, fFileSize, fFileVariance);
read_spt_collision(p, n, vCollisionObjects);
read_spt_texcoords(p, n, texCoords);
fSize = fFileSize;
fVariance = fFileVariance;
}
@@ -366,7 +435,11 @@ struct STreeData
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 int count = palm ? 16 : 24;
for (int i = 0; i < count; ++i)
@@ -418,8 +491,6 @@ struct STreeData
afBoundingBox[3] = R;
afBoundingBox[4] = R;
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
// 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));
}
unsigned int CSpeedTreeRT::GetCollisionObjectCount(void) { return 1; }
void CSpeedTreeRT::GetCollisionObject(unsigned int /*nIndex*/, ECollisionObjectType & eType, float * pPosition, float * pDimensions)
unsigned int CSpeedTreeRT::GetCollisionObjectCount(void)
{
const STreeData & tree = *m_pImpl->pTree;
eType = CO_CYLINDER;
return static_cast<unsigned int>(m_pImpl->pTree->vCollisionObjects.size());
}
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)
pPosition[0] = pPosition[1] = pPosition[2] = 0.0f;
std::memcpy(pPosition, o.afPosition, sizeof(o.afPosition));
if (pDimensions)
{
pDimensions[0] = tree.fTrunkRadius;
pDimensions[1] = tree.fTrunkHeight;
pDimensions[2] = 0.0f;
}
std::memcpy(pDimensions, o.afDimensions, sizeof(o.afDimensions));
}
+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)
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.
add_executable(port_packet_test ${PROJECT_SOURCE_DIR}/tests/port/port_packet_test.cpp
${PROJECT_SOURCE_DIR}/tests/port/port_packet_test_wire.cpp)