engine/engine/src/ext/valve/bsp.cpp

817 lines
29 KiB
C++

#include <uf/ext/valve/bsp.h>
#include <uf/ext/valve/mdl.h>
#include <uf/ext/valve/vtf.h>
#include <uf/ext/valve/vpk.h>
#include <uf/ext/valve/common.h>
#include <uf/ext/zlib/zlib.h>
namespace impl {
struct RGBE {
uint8_t r, g, b;
int8_t e;
};
constexpr int VBSP_HEADER_LUMPS = 64;
struct BspLump {
enum Type {
LUMP_ENTITIES = 0,
LUMP_PLANES = 1,
LUMP_TEXDATA = 2,
LUMP_VERTICES = 3,
LUMP_VISIBILITY = 4,
LUMP_NODES = 5,
LUMP_TEXINFO = 6,
LUMP_FACES = 7,
LUMP_LIGHTING = 8,
LUMP_OCCLUSION = 9,
LUMP_LEAFS = 10,
LUMP_FACEIDS = 11,
LUMP_EDGES = 12,
LUMP_SURFEDGES = 13,
LUMP_MODELS = 14,
LUMP_WORLDLIGHTS = 15,
LUMP_LEAFFACES = 16,
LUMP_LEAFBRUSHES = 17,
LUMP_BRUSHES = 18,
LUMP_BRUSHSIDES = 19,
LUMP_AREAS = 20,
LUMP_AREAPORTALS = 21,
LUMP_UNUSED0 = 22, // LUMP_PORTALS | LUMP_PROPCOLLISION
LUMP_UNUSED1 = 23, // LUMP_CLUSTERS | LUMP_PROPHULLS
LUMP_UNUSED2 = 24, // LUMP_PORTALVERTS | LUMP_FAKEENTITIES | LUMP_PROPHULLVERTS
LUMP_UNUSED3 = 25, // LUMP_CLUSTERPORTALS | LUMP_PROPTRIS
LUMP_DISPINFO = 26,
LUMP_ORIGINALFACES = 27,
LUMP_PHYSDISP = 28,
LUMP_PHYSCOLLIDE = 29,
LUMP_VERTNORMALS = 30,
LUMP_VERTNORMALINDICES = 31,
LUMP_DISP_LIGHTMAP_ALPHAS = 32,
LUMP_DISP_VERTS = 33,
LUMP_DISP_LIGHTMAP_SAMPLE_POSITIONS = 34,
LUMP_GAME_LUMP = 35,
LUMP_LEAFWATERDATA = 36,
LUMP_PRIMITIVES = 37,
LUMP_PRIMVERTS = 38,
LUMP_PRIMINDICES = 39,
LUMP_PAKFILE = 40,
LUMP_CLIPPORTALVERTS = 41,
LUMP_CUBEMAPS = 42,
LUMP_TEXDATA_STRING_DATA = 43,
LUMP_TEXDATA_STRING_TABLE = 44,
LUMP_OVERLAYS = 45,
LUMP_LEAFMINDISTTOWATER = 46,
LUMP_FACE_MACRO_TEXTURE_INFO = 47,
LUMP_DISP_TRIS = 48,
LUMP_PHYSCOLLIDESURFACE = 49, // LUMP_PROP_BLOB
LUMP_WATEROVERLAYS = 50,
LUMP_LEAF_AMBIENT_INDEX_HDR = 51, // LUMP_LIGHTMAPPAGES
LUMP_LEAF_AMBIENT_INDEX = 52, // LUMP_LIGHTMAPPAGEINFOS
LUMP_LIGHTING_HDR = 53,
LUMP_WORLDLIGHTS_HDR = 54,
LUMP_LEAF_AMBIENT_LIGHTING_HDR = 55,
LUMP_LEAF_AMBIENT_LIGHTING = 56,
LUMP_XZIPPAKFILE = 57,
LUMP_FACES_HDR = 58,
LUMP_MAP_FLAGS = 59,
LUMP_OVERLAY_FADES = 60,
LUMP_OVERLAY_SYSTEM_LEVELS = 61,
LUMP_PHYSLEVEL = 62,
LUMP_DISP_MULTIBLEND = 63,
};
int32_t offset;
int32_t length;
int32_t version;
int32_t uncompressedSize;
};
struct BspHeader {
int32_t magic; // 'VBSP'
int32_t version; // 19 or 20
BspLump lumps[VBSP_HEADER_LUMPS];
int32_t mapRevision;
};
typedef pod::Vector3f BspVertex;
typedef pod::Vector2s BspEdge;
struct BspFace {
uint16_t planenum;
uint8_t side;
uint8_t onNode;
int32_t firstedge;
int16_t numedges;
int16_t texinfo;
int16_t dispinfo;
int16_t surfaceFogVolumeID;
pod::Vector4ub styles;
int32_t lightofs;
float area;
pod::Vector2i lightmapTextureMins;
pod::Vector2i lightmapTextureSize;
int32_t origFace;
uint16_t numPrims;
uint16_t firstPrimID;
uint16_t smoothingGroups;
};
struct BspDispInfo {
pod::Vector3f startPosition;
int32_t dispVertStart;
int32_t dispTriStart;
int32_t power;
int32_t minTess;
int32_t maxTess;
int32_t smoothingAngle;
int32_t contents;
uint16_t mapFace;
int16_t lightmapAlphaStart;
int32_t lightmapSamplePositionStart;
uint8_t padding[130];
};
struct BspDispVert {
pod::Vector3f vec;
float dist;
float alpha;
};
struct BspTexInfo {
pod::Vector4f textureVecs[2];
pod::Vector4f lightmapVecs[2];
int32_t flags;
int32_t texData;
};
struct BspTexData {
pod::Vector3f reflectivity;
int32_t nameStringTableID;
int32_t width, height;
int32_t view_width, view_height;
};
struct BspModel {
pod::Vector3f mins, maxs;
pod::Vector3f origin;
int32_t headnode;
int32_t firstface, numfaces;
};
struct BspGameLump {
int32_t id;
uint16_t flags;
uint16_t version;
int32_t fileofs;
int32_t filelen;
};
struct BspContext {
uf::stl::vector<impl::BspVertex> vertices;
uf::stl::vector<impl::BspEdge> edges;
uf::stl::vector<int32_t> surfedges;
uf::stl::vector<impl::BspFace> faces;
// brush, brushside
// node, leaf
// leafface, leaffbrush
uf::stl::vector<impl::BspTexInfo> texinfos;
uf::stl::vector<impl::BspTexData> texdatas;
uf::stl::vector<int32_t> stringTable;
uf::stl::string stringData;
uf::stl::vector<impl::BspModel> models;
// visibility
uf::stl::vector<int8_t> entities;
uf::stl::vector<impl::BspGameLump> gameLumps;
uf::stl::vector<impl::BspDispInfo> dispinfos;
uf::stl::vector<impl::BspDispVert> dispverts;
// disptris
uf::stl::vector<uint8_t> pakfile;
// cubemaps
// overlay
uf::stl::vector<uint8_t> lighting;
// ambient lighting
// occlusion
// physics
// worldlight
// other
uf::stl::vector<int32_t> modelToMesh;
uf::stl::vector<int32_t> texdataToMaterial;
pod::Atlas lightmapAtlas;
};
pod::Atlas::hash_t faceHash( size_t i ) {
return ::fmt::format("face_{}", i);
}
void buildDisplacement( const impl::BspContext& context, impl::Meshlet& meshlet, size_t faceID ) {
const auto& face = context.faces[faceID];
const auto& info = context.dispinfos[face.dispinfo];
const auto& texInfo = context.texinfos[face.texinfo];
int side = (1 << info.power) + 1; // 2^power + 1
struct CornerData {
pod::Vector3f pos;
pod::Vector2f uv;
pod::Vector2f st;
};
uf::stl::vector<CornerData> corners(4);
float texWidth = 512.0f, texHeight = 512.0f;
if ( texInfo.texData >= 0 && texInfo.texData < context.texdatas.size() ) {
texWidth = (float)(context.texdatas[texInfo.texData].width);
texHeight = (float)(context.texdatas[texInfo.texData].height);
}
for ( int i = 0; i < 4; ++i ) {
int32_t se = context.surfedges[face.firstedge + i];
uint16_t vIdx = se >= 0 ? context.edges[se].x : context.edges[-se].y;
pod::Vector3f rawPos = context.vertices[vIdx];
corners[i].pos = rawPos;
pod::Vector4f v = rawPos; v.w = 1.0f;
corners[i].uv.x = uf::vector::dot( v, texInfo.textureVecs[0] ) / texWidth;
corners[i].uv.y = uf::vector::dot( v, texInfo.textureVecs[1] ) / texHeight;
corners[i].st.x = (uf::vector::dot( v, texInfo.lightmapVecs[0] ) - face.lightmapTextureMins.x) / (face.lightmapTextureSize.x + 1.0f);
corners[i].st.y = (uf::vector::dot( v, texInfo.lightmapVecs[1] ) - face.lightmapTextureMins.y) / (face.lightmapTextureSize.y + 1.0f);
}
int startIndex = 0;
float minDist = 999999.0f;
for ( int i = 0; i < 4; ++i ) {
float dist = uf::vector::distance( corners[i].pos, info.startPosition );
if ( dist < minDist ) {
minDist = dist;
startIndex = i;
}
}
std::rotate(corners.begin(), corners.begin() + startIndex, corners.end());
uint32_t startVertexID = meshlet.vertices.size();
for ( int y = 0; y < side; ++y ) {
float ty = (float)y / (side - 1);
pod::Vector3f posE0 = uf::vector::lerp( corners[0].pos, corners[1].pos, ty );
pod::Vector3f posE1 = uf::vector::lerp( corners[3].pos, corners[2].pos, ty );
pod::Vector2f uvE0 = uf::vector::lerp( corners[0].uv, corners[1].uv, ty );
pod::Vector2f uvE1 = uf::vector::lerp( corners[3].uv, corners[2].uv, ty );
pod::Vector2f stE0 = uf::vector::lerp( corners[0].st, corners[1].st, ty );
pod::Vector2f stE1 = uf::vector::lerp( corners[3].st, corners[2].st, ty );
for ( int x = 0; x < side; ++x ) {
float tx = (float)x / (side - 1);
pod::Vector3f basePos = uf::vector::lerp( posE0, posE1, tx );
pod::Vector2f finalUv = uf::vector::lerp( uvE0, uvE1, tx );
pod::Vector2f finalSt = uf::vector::lerp( stE0, stE1, tx );
int dispIdx = info.dispVertStart + y * side + x;
const auto& dVert = context.dispverts[dispIdx];
auto& vert = meshlet.vertices.emplace_back();
vert.position = impl::convertPos( basePos + (dVert.vec * dVert.dist) );
vert.uv = finalUv;
vert.st = uf::atlas::mapUv( context.lightmapAtlas, finalSt, impl::faceHash( faceID ) );
vert.color = { 1.0f, 1.0f, 1.0f, dVert.alpha / 255.0f };
}
}
for ( int y = 0; y < side; ++y ) {
for ( int x = 0; x < side; ++x ) {
int id = startVertexID + y * side + x;
pod::Vector3f pL = meshlet.vertices[startVertexID + y * side + std::max(x - 1, 0)].position;
pod::Vector3f pR = meshlet.vertices[startVertexID + y * side + std::min(x + 1, side - 1)].position;
pod::Vector3f pD = meshlet.vertices[startVertexID + std::max(y - 1, 0) * side + x].position;
pod::Vector3f pU = meshlet.vertices[startVertexID + std::min(y + 1, side - 1) * side + x].position;
pod::Vector3f tangent = uf::vector::normalize(pR - pL);
pod::Vector3f bitangent = uf::vector::normalize(pU - pD);
pod::Vector3f normal = uf::vector::normalize(uf::vector::cross(bitangent, tangent));
meshlet.vertices[id].normal = normal;
meshlet.vertices[id].tangent = tangent;
}
}
for ( int y = 0; y < side - 1; ++y ) {
for ( int x = 0; x < side - 1; ++x ) {
uint32_t v0 = startVertexID + y * side + x;
uint32_t v1 = startVertexID + y * side + (x + 1);
uint32_t v2 = startVertexID + (y + 1) * side + x;
uint32_t v3 = startVertexID + (y + 1) * side + (x + 1);
// might need to flip winding order
meshlet.indices.emplace_back(v0); meshlet.indices.emplace_back(v2); meshlet.indices.emplace_back(v1);
meshlet.indices.emplace_back(v1); meshlet.indices.emplace_back(v2); meshlet.indices.emplace_back(v3);
}
}
}
void addVertex( const impl::BspContext& context, impl::Meshlet& meshlet, uint16_t vertexID, pod::Vector3f pos, pod::Vector3f normal, size_t faceID ) {
auto& bounds = meshlet.primitive.instance.bounds;
if ( meshlet.vertices.empty() ) {
bounds.min = bounds.max = pos;
} else {
bounds.min = uf::vector::min( bounds.min, pos );
bounds.max = uf::vector::max( bounds.max, pos );
}
const auto& face = context.faces[faceID];
pod::Vector4f vertex = context.vertices[vertexID];
vertex.w = 1; // for dot products
// add index
meshlet.indices.emplace_back((uint32_t)(meshlet.vertices.size()));
// add vertex
auto& v = meshlet.vertices.emplace_back();
v.position = pos;
v.color = { 1.0f, 1.0f, 1.0f, 1.0f };
v.normal = normal;
// has texture information
if ( face.texinfo >= 0 && face.texinfo < context.texinfos.size() ) {
const auto& info = context.texinfos[face.texinfo];
if ( info.texData >= 0 && info.texData < context.texdatas.size() ) {
const auto& data = context.texdatas[info.texData];
v.uv.x = uf::vector::dot( vertex, info.textureVecs[0] ) / (float) data.width;
v.uv.y = uf::vector::dot( vertex, info.textureVecs[1] ) / (float) data.height;
}
v.st.x = (uf::vector::dot( vertex, info.lightmapVecs[0] ) + 0.5f - face.lightmapTextureMins.x) / (face.lightmapTextureSize.x + 1.0f);
v.st.y = (uf::vector::dot( vertex, info.lightmapVecs[1] ) + 0.5f - face.lightmapTextureMins.y) / (face.lightmapTextureSize.y + 1.0f);
v.st = uf::atlas::mapUv( context.lightmapAtlas, v.st, impl::faceHash( faceID ) );
v.tangent = uf::vector::normalize( impl::convertPos( info.textureVecs[0], 1) );
}
};
template<typename T>
uf::stl::vector<T> extractLump( const uf::stl::vector<uint8_t>& buffer, const impl::BspLump& lump ) {
uf::stl::vector<T> data;
if ( lump.length == 0 || lump.offset >= buffer.size() ) return data;
size_t count = lump.length / sizeof(T);
data.resize(count);
std::copy(buffer.data() + lump.offset, buffer.data() + lump.offset + lump.length, (uint8_t*)(data.data()) );
return data;
}
template<>
uf::stl::vector<impl::BspGameLump> extractLump( const uf::stl::vector<uint8_t>& buffer, const impl::BspLump& lump ) {
uf::stl::vector<impl::BspGameLump> data;
if ( lump.length == 0 || lump.offset >= buffer.size() ) return data;
const uint8_t* glData = buffer.data() + lump.offset;
int32_t lumpCount = *(const int32_t*)glData;
data.resize(lumpCount);
std::copy(glData + 4, glData + 4 + (lumpCount * sizeof(impl::BspGameLump)), (uint8_t*)(data.data()));
return data;
}
uf::stl::string extractLumpString( const uf::stl::vector<uint8_t>& buffer, const impl::BspLump& lump ) {
auto data = impl::extractLump<char>( buffer, lump );
return uf::stl::string( data.data(), data.size() );
}
void processNodes( pod::Graph& graph, const impl::BspContext& context, float scale = impl::sourceToMeters ) {
size_t lights = 0;
int32_t spawnID = -1;
uf::stl::vector<size_t> spawns;
uf::stl::unordered_map<uf::stl::string, size_t> targets;
for ( auto nodeID : graph.root.children ) {
auto& node = graph.nodes[nodeID];
auto& metadata = node.metadata["valve"];
auto classname = metadata["classname"].as<uf::stl::string>("");
//UF_MSG_INFO("Entity found: {}", classname);
node.name = classname;
// parse origin
auto origin = metadata["origin"].as<uf::stl::string>("");
if ( origin != "" ) {
auto position = impl::str2vec<pod::Vector3f>( origin );
node.transform.position = impl::convertPos( position, scale );
}
// parse angles
// to-do: fix oddities
auto angles = metadata["angles"].as<uf::stl::string>("");
if ( angles != "" ) {
auto pyr = impl::str2vec<pod::Vector3f>( angles ) * DEG_2_RAD;
pyr.x = -pyr.x;
node.transform.orientation = uf::quaternion::euler( pyr );
}
// parse model
auto model = metadata["model"].as<uf::stl::string>();
if ( classname == "worldspawn" ) {
node.mesh = context.modelToMesh[0]; // implicitly bind to model 0
} else if ( model.starts_with("*") ) {
int modelID = std::stoi( model.substr(1) );
if ( 0 <= modelID && modelID < context.modelToMesh.size() ) {
node.mesh = context.modelToMesh[modelID];
}
} else if ( model.length() > 4 && model.ends_with(".mdl") ) {
auto it = std::find(graph.meshes.begin(), graph.meshes.end(), model);
if ( it == graph.meshes.end() ) {
if ( ext::valve::loadMdl(graph, model) ) {
node.mesh = (int32_t)(graph.meshes.size() - 1);
}
} else {
node.mesh = (int32_t)std::distance(graph.meshes.begin(), it);
}
}
// parse lighting info
if ( classname.starts_with("light") ) {
auto lightKeyName = ::fmt::format( "{}_{}", classname, nodeID );
auto& light = graph.lights[lightKeyName];
light.color = { 1.0f, 1.0f, 1.0f };
light.intensity = 200.0f;
light.range = 0.0f;
// read color and intensity
auto _light = metadata["_light"].as<uf::stl::string>("");
if ( _light != "" ) {
// to-do: do not use stringstream
std::istringstream stream(_light);
light.color = { 255.0f, 255.0f, 255.0f };
stream >> light.color.x >> light.color.y >> light.color.z;
light.color /= 255.0f;
if (!(stream >> light.intensity)) light.intensity = 200.0f;
}
// to-do: read range
light.intensity *= 0.2f; // scale down
}
// parse player spawn info
if ( classname == "info_player_start" ) {
spawnID = nodeID;
} else if ( classname.starts_with("info_player_") ) {
spawns.emplace_back(nodeID);
}
auto targetname = metadata["targetname"].as<uf::stl::string>("");
if ( targetname != "" ) {
targets[targetname] = nodeID;
}
// to-do: add additional parsing
}
uf::stl::vector<int32_t> newChildren;
for ( auto nodeID : graph.root.children ) {
auto& node = graph.nodes[nodeID];
auto& metadata = node.metadata["valve"];
auto parentname = metadata["parentname"].as<uf::stl::string>("");
if ( parentname != "" && targets.count(parentname) > 0 ) {
auto parentID = targets[parentname];
auto& parentNode = graph.nodes[parentID];
parentNode.children.emplace_back(nodeID);
node.transform = uf::transform::relative( parentNode.transform, node.transform );
} else {
newChildren.emplace_back(nodeID);
}
}
graph.root.children = newChildren;
// no valid spawn
UF_ASSERT( !(spawnID == -1 && spawns.empty()) ); // to-do: make the engine implicitly spawn the player at origin
// pick a random candidate if none was found
if ( spawnID == -1 ) spawnID = uf::stl::random( spawns );
for ( auto nodeID : spawns ) {
auto& node = graph.nodes[nodeID];
if ( nodeID == spawnID ) {
node.name = "info_player_start"; // mutate into spawn
} else if ( node.name == "info_player_start" ) {
node.name = ::fmt::format( "_{}", node.name ); // mutate out of spawn
}
}
}
}
void ext::valve::loadBsp( pod::Graph& graph, const uf::stl::string& filename, const uf::Serializer& metadata ) {
uf::stl::vector<uint8_t> buffer;
if ( !uf::io::readAsBuffer(buffer, filename) ) {
UF_MSG_ERROR("Failed to read BSP data: {}", filename);
return;
}
const impl::BspHeader* header = (const impl::BspHeader*)(buffer.data());
if ( header->magic != 0x50534256 ) {
UF_MSG_ERROR("Invalid VBSP magic number: {}", filename);
return;
}
auto& storage = uf::graph::getStorage( graph );
graph.name = filename;
graph.metadata = metadata;
graph.root.name = "%ROOT%";
graph.root.index = -1;
impl::BspContext context;
context.vertices = impl::extractLump<impl::BspVertex>(buffer, header->lumps[impl::BspLump::LUMP_VERTICES]);
context.edges = impl::extractLump<impl::BspEdge>(buffer, header->lumps[impl::BspLump::LUMP_EDGES]);
context.surfedges = impl::extractLump<int32_t>(buffer, header->lumps[impl::BspLump::LUMP_SURFEDGES]);
context.faces = impl::extractLump<impl::BspFace>(buffer, header->lumps[impl::BspLump::LUMP_FACES]);
context.texinfos = impl::extractLump<impl::BspTexInfo>(buffer, header->lumps[impl::BspLump::LUMP_TEXINFO]);
context.texdatas = impl::extractLump<impl::BspTexData>(buffer, header->lumps[impl::BspLump::LUMP_TEXDATA]);
context.stringTable = impl::extractLump<int32_t>(buffer, header->lumps[impl::BspLump::LUMP_TEXDATA_STRING_TABLE]);
context.stringData = impl::extractLumpString(buffer, header->lumps[impl::BspLump::LUMP_TEXDATA_STRING_DATA]);
context.models = impl::extractLump<impl::BspModel>(buffer, header->lumps[impl::BspLump::LUMP_MODELS]);
context.entities = impl::extractLump<int8_t>(buffer, header->lumps[impl::BspLump::LUMP_ENTITIES]);
context.gameLumps = impl::extractLump<impl::BspGameLump>(buffer, header->lumps[impl::BspLump::LUMP_GAME_LUMP]);
context.dispinfos = impl::extractLump<impl::BspDispInfo>(buffer, header->lumps[impl::BspLump::LUMP_DISPINFO]);
context.dispverts = impl::extractLump<impl::BspDispVert>(buffer, header->lumps[impl::BspLump::LUMP_DISP_VERTS]);
context.pakfile = impl::extractLump<uint8_t>(buffer, header->lumps[impl::BspLump::LUMP_PAKFILE]);
context.lighting = impl::extractLump<uint8_t>(buffer, header->lumps[impl::BspLump::LUMP_LIGHTING]);
context.modelToMesh.assign( context.models.size(), -1 );
context.texdataToMaterial.assign( context.texdatas.size(), -1 );
// mount pakfile
size_t pakfileMount = uf::vfs::mount( ext::zlib::createZipMount(::fmt::format("pakfile://{}", filename), context.pakfile, 1000 ) );
// read materials
for ( int32_t texDataID = 0; texDataID < context.texdatas.size(); ++texDataID ) {
const auto& data = context.texdatas[texDataID];
uf::stl::string matName = "missing_texture";
// lookup material name
if ( data.nameStringTableID >= 0 && data.nameStringTableID < context.stringTable.size() ) {
int32_t offset = context.stringTable[data.nameStringTableID];
if ( offset >= 0 && offset < context.stringData.size() ) {
matName = uf::string::lowercase( context.stringData.c_str() + offset );
}
}
size_t imageID = graph.images.size();
auto imgKeyName = graph.images.emplace_back(matName);
auto& image = storage.images[imgKeyName].data;
size_t textureID = graph.textures.size();
auto texKeyName = graph.textures.emplace_back(matName);
storage.textures[texKeyName].index = imageID;
size_t materialID = graph.materials.size();
context.texdataToMaterial[texDataID] = materialID;
auto matKeyName = graph.materials.emplace_back(matName);
auto& material = storage.materials[matKeyName];
material.indexAlbedo = textureID;
material.colorBase = {1.0f, 1.0f, 1.0f, 1.0f};
material.factorMetallic = 0.0f;
material.factorRoughness = 1.0f;
material.factorOcclusion = 1.0f;
//UF_MSG_INFO("Material found: {}", matName);
}
// read lightmaps
auto atlasImageID = graph.images.size();
auto atlasTextureID = graph.textures.size();
for ( size_t i = 3; i < context.lighting.size(); i += 4 ) {
int8_t exp = (int8_t)context.lighting[i];
context.lighting[i] = (uint8_t)(exp + 128);
}
for ( auto faceID = 0; faceID < context.faces.size(); ++faceID ) {
const auto& face = context.faces[faceID];
if ( face.lightofs == -1 || context.lighting.empty() ) continue;
size_t width = face.lightmapTextureSize.x + 1;
size_t height = face.lightmapTextureSize.y + 1;
uf::Image image;
image.loadFromBuffer( (uint8_t*)(context.lighting.data() + face.lightofs), { width, height }, 8, 4 );
uf::atlas::add( context.lightmapAtlas, image, impl::faceHash( faceID ) );
}
{
UF_MSG_DEBUG("Generating new lightmap atlas...");
uf::atlas::generate( context.lightmapAtlas, 0.0f );
//UF_MSG_DEBUG("Generated lightmap atlas.");
auto& imageKey = graph.images.emplace_back("lightmap_atlas");
auto& textureKey = graph.textures.emplace_back("lightmap_atlas");
storage.images[imageKey].data = uf::atlas::get( context.lightmapAtlas );
storage.textures[textureKey].index = atlasImageID;
}
// read models
for ( auto m = 0; m < context.models.size(); ++m ) {
const auto& model = context.models[m];
uf::stl::unordered_map<int32_t, impl::Meshlet> meshlets; // group by material IDs
for ( auto i = 0; i < model.numfaces; ++i ) {
const auto faceID = model.firstface + i;
const auto& face = context.faces[faceID];
if ( face.numedges < 3 ) continue;
if ( face.texinfo < 0 || face.texinfo >= context.texinfos.size() ) continue;
int32_t texDataID = context.texinfos[face.texinfo].texData;
if ( texDataID < 0 || texDataID >= context.texdatas.size() ) continue;
size_t materialID = context.texdataToMaterial[texDataID];
// read brush
auto& meshlet = meshlets[materialID];
meshlet.primitive.instance.materialID = materialID;
if ( 0 <= face.lightofs ) {
meshlet.primitive.instance.lightmapID = atlasTextureID;
}
if ( face.dispinfo != -1 ) {
impl::buildDisplacement( context, meshlet, faceID );
continue;
}
const auto edgeID = face.firstedge;
int32_t pivotSurfEdge = context.surfedges[edgeID];
uint16_t pivotVertID = pivotSurfEdge >= 0 ? context.edges[pivotSurfEdge].x : context.edges[-pivotSurfEdge].y;
pod::Vector3f p0 = impl::convertPos( context.vertices[pivotVertID] );
for ( int16_t i = 1; i < face.numedges - 1; ++i ) {
int32_t se1 = context.surfedges[edgeID + i];
int32_t se2 = context.surfedges[edgeID + i + 1];
uint16_t v1 = se1 >= 0 ? context.edges[se1].x : context.edges[-se1].y;
uint16_t v2 = se2 >= 0 ? context.edges[se2].x : context.edges[-se2].y;
pod::Vector3f p1 = impl::convertPos( context.vertices[v1] );
pod::Vector3f p2 = impl::convertPos( context.vertices[v2] );
pod::Vector3f normal = uf::vector::normalize(uf::vector::cross(p1 - p0, p2 - p0));
impl::addVertex( context, meshlet, pivotVertID, p0, normal, faceID );
impl::addVertex( context, meshlet, v1, p1, normal, faceID );
impl::addVertex( context, meshlet, v2, p2, normal, faceID );
}
}
if ( meshlets.empty() ) continue;
auto meshName = ::fmt::format("model_{}", m);
context.modelToMesh[m] = graph.meshes.size();
graph.meshes.emplace_back(meshName);
graph.primitives.emplace_back(meshName);
auto& mesh = storage.meshes[meshName];
auto& primitives = storage.primitives[meshName];
mesh.compile( meshlets, primitives );
}
// read entities
{
bool parsing = false;
uf::stl::unordered_map<uf::stl::string, uf::stl::string> dict;
uf::stl::string string( (const char*) context.entities.data() );
uf::stl::string line;
std::istringstream stream(string);
while ( std::getline(stream, line) ) {
if ( line.find("{") != uf::stl::string::npos ) {
parsing = true;
dict.clear();
} else if ( line.find("}") != uf::stl::string::npos ) {
parsing = false;
// create node
auto nodeID = graph.nodes.size();
auto& node = graph.nodes.emplace_back();
auto& metadata = node.metadata["valve"];
for ( const auto& [k, v] : dict ) metadata[k] = v; // store as metadata for later parsing
// add node as child
graph.root.children.emplace_back( nodeID );
} else if ( parsing ) {
uf::stl::string key, value;
if ( impl::parseKeyValue(line, key, value) ) dict[key] = value;
}
}
}
// read static props
for ( const auto& item : context.gameLumps ) {
if ( item.id == 1936749168 ) { // 'sprp' (Static Props)
const uint8_t* sprpData = buffer.data() + item.fileofs;
int offset = 0;
int32_t entries = *(const int32_t*)(sprpData + offset); offset += 4;
uf::stl::vector<uf::stl::string> dict(entries);
for ( int32_t d = 0; d < entries; ++d ) {
dict[d] = uf::stl::string((const char*)(sprpData + offset), 128).c_str();
offset += 128;
}
int32_t leafEntries = *(const int32_t*)(sprpData + offset); offset += 4;
offset += leafEntries * sizeof(uint16_t);
int32_t propEntries = *(const int32_t*)(sprpData + offset); offset += 4;
int propStride = 0;
switch ( item.version ) {
case 4: propStride = 56; break;
case 5: propStride = 60; break;
case 6: propStride = 64; break;
case 7: propStride = 68; break;
case 8: propStride = 68; break;
case 9: propStride = 72; break;
case 10: propStride = 76; break;
case 11: propStride = 80; break;
default:
UF_MSG_WARNING("Unknown static prop version: {}", item.version);
propStride = (item.filelen - offset) / propEntries;
break;
}
for ( int32_t p = 0; p < propEntries; ++p ) {
const uint8_t* propData = sprpData + offset + (p * propStride);
pod::Vector3f origin = *(const pod::Vector3f*)(propData + 0);
pod::Vector3f angles = *(const pod::Vector3f*)(propData + 12);
uint16_t type = *(const uint16_t*)(propData + 24);
if ( type < dict.size() ) {
auto nodeID = graph.nodes.size();
auto& node = graph.nodes.emplace_back();
auto& metadata = node.metadata["valve"];
metadata["classname"] = "prop_static";
metadata["model"] = dict[type];
metadata["origin"] = ::fmt::format("{} {} {}", origin.x, origin.y, origin.z);
metadata["angles"] = ::fmt::format("{} {} {}", angles.x, angles.y, angles.z);
graph.root.children.emplace_back( nodeID );
}
}
break; // no need to keep searching
}
}
impl::processNodes( graph, context );
// load materials
uf::stl::vector<uint8_t> missing_pixels = { 255, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 255 };
for ( auto matName : graph.materials ) {
uf::Serializer vmt;
auto vmtPath = ::fmt::format("materials/{}.vmt", matName);
auto vtfPath = ::fmt::format("materials/{}.vtf", matName);
auto& image = storage.images[matName].data;
auto& material = storage.materials[matName];
if ( !ext::valve::loadVmt( vmt, vmtPath ) ) goto PEETAH;
material.factorMetallic = vmt["$metalness"].as<float>(0.0f);
material.factorRoughness = vmt["$roughness"].as<float>(1.0f);
if ( vmt["$envmap"].as<uf::stl::string>() != "" ) material.factorRoughness = 0.3f;
if ( vmt["$phong"].as<uf::stl::string>("0") == "1" ) material.factorRoughness = std::min(material.factorRoughness, 0.5f);
if ( vmt["$translucent"].as<uf::stl::string>("0") == "1" ) {
material.modeAlpha = 1; // BLEND
} else if ( vmt["$alphatest"].as<uf::stl::string>("0") == "1" ) {
material.modeAlpha = 2; // MASK
material.factorAlphaCutoff = vmt["$alphatestreference"].as<float>(0.5f);
}
if ( vmt["$nocull"].as<uf::stl::string>("0") == "1" ) material.modeCull = 0;
// VMTs usually define emissive masks in the albedo's alpha channel or a separate mask
// set it to a white glow for now until I can patch the shader
if ( vmt["$selfillum"].as<uf::stl::string>("0") == "1" ) material.colorEmissive = { 1.0f, 1.0f, 1.0f, 1.0f };
if ( !vmt["$basetexture"].is<uf::stl::string>() ) goto PEETAH;
vtfPath = ::fmt::format("materials/{}.vtf", vmt["$basetexture"].as<uf::stl::string>());
if ( !ext::valve::loadVtf( image, vtfPath ) ) goto PEETAH;
continue;
PEETAH:
image.loadFromBuffer( missing_pixels, { 2, 2 }, 8, 4 );
}
graph.metadata["exporter"]["unwrap"] = false; // not necessary to unwrap
uf::graph::postprocess( graph );
// unmount pakfile
uf::vfs::unmount( pakfileMount );
}