mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
- Shader 94 ("Basic") draws with vertex colors: its class's technique setup
names Technique_Basic_Lighting_VertColor, as 38 "Basic VC" does. The views
treated it as having none, so Nimbus Station's glom pines (tag S84) came
out as their grey texture, white. The other shader looks in use were
checked the same way (33, 35, 37, 70, 82, 84, 105 hold).
- Two layer shaders: NiTexturingProperty's dark texture and the UV set each
texture's flags name are read, and the views draw "Two Layers Blended" as
the dark texture under the base one by the vertex alpha (no longer as
opacity) and "Two Textures Added" as in TwoLayersAdded_PS. Avant Gardens'
snowy grass mounds were see-through hills. The client ships no technique
for the blended ones, so that blend follows the meshes' data.
- The near plane follows how far out the camera is (distance / 400, 0.5 to
20) instead of a fixed 0.5 over a far plane in the thousands, so ground
overlays, floor rings and road pieces stop fighting in far views.
- Conversion format 3 (new model data), so kept conversions are made again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
812 lines
32 KiB
C++
812 lines
32 KiB
C++
#include "NifFile.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <set>
|
|
|
|
#include "json.hpp"
|
|
|
|
namespace {
|
|
constexpr uint32_t Version(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { return (a << 24) | (b << 16) | (c << 8) | d; }
|
|
constexpr uint32_t MIN_VERSION = Version(20, 2, 0, 5); // strings in a table and block sizes in the header
|
|
constexpr uint32_t MAX_VERSION = Version(20, 3, 0, 9);
|
|
constexpr uint32_t MAX_BLOCKS = 200000;
|
|
constexpr uint32_t MAX_DEPTH = 64;
|
|
constexpr uint16_t APP_CULLED = 1; // NiAVObject flag: hidden
|
|
|
|
// Little-endian reads that fail (and stay failed) instead of running past the end
|
|
class Reader {
|
|
public:
|
|
explicit Reader(std::string_view data) : m_Data(data) {}
|
|
|
|
template<typename T>
|
|
T Read() {
|
|
T value{};
|
|
if (!m_Ok || sizeof(T) > m_Data.size() - m_Pos) {
|
|
m_Ok = false;
|
|
return value;
|
|
}
|
|
std::memcpy(&value, m_Data.data() + m_Pos, sizeof(T));
|
|
m_Pos += sizeof(T);
|
|
return value;
|
|
}
|
|
|
|
float Float() { return Read<float>(); }
|
|
uint8_t U8() { return Read<uint8_t>(); }
|
|
uint16_t U16() { return Read<uint16_t>(); }
|
|
uint32_t U32() { return Read<uint32_t>(); }
|
|
int32_t I32() { return Read<int32_t>(); }
|
|
|
|
void Skip(uint64_t bytes) {
|
|
if (!m_Ok || bytes > m_Data.size() - m_Pos) {
|
|
m_Ok = false;
|
|
return;
|
|
}
|
|
m_Pos += static_cast<size_t>(bytes);
|
|
}
|
|
|
|
// `count` values of T, or empty (and failed) if there aren't that many
|
|
template<typename T>
|
|
std::vector<T> Array(uint64_t count) {
|
|
std::vector<T> values;
|
|
if (!m_Ok || count > (m_Data.size() - m_Pos) / sizeof(T)) {
|
|
m_Ok = false;
|
|
return values;
|
|
}
|
|
values.resize(static_cast<size_t>(count));
|
|
std::memcpy(values.data(), m_Data.data() + m_Pos, static_cast<size_t>(count) * sizeof(T));
|
|
m_Pos += static_cast<size_t>(count) * sizeof(T);
|
|
return values;
|
|
}
|
|
|
|
std::string SizedString() {
|
|
const auto length = U32();
|
|
if (!m_Ok || length > m_Data.size() - m_Pos) {
|
|
m_Ok = false;
|
|
return {};
|
|
}
|
|
std::string value(m_Data.substr(m_Pos, length));
|
|
m_Pos += length;
|
|
return value;
|
|
}
|
|
|
|
bool Ok() const { return m_Ok; }
|
|
size_t Position() const { return m_Pos; }
|
|
|
|
private:
|
|
std::string_view m_Data;
|
|
size_t m_Pos = 0;
|
|
bool m_Ok = true;
|
|
};
|
|
|
|
// Rotation (row-major, for column vectors), translation and uniform scale: p' = t + s * R p
|
|
struct Transform {
|
|
std::array<float, 9> r{ 1, 0, 0, 0, 1, 0, 0, 0, 1 };
|
|
std::array<float, 3> t{};
|
|
float s{ 1.0f };
|
|
|
|
std::array<float, 3> Rotate(const std::array<float, 3>& v) const {
|
|
return { r[0] * v[0] + r[1] * v[1] + r[2] * v[2], r[3] * v[0] + r[4] * v[1] + r[5] * v[2], r[6] * v[0] + r[7] * v[1] + r[8] * v[2] };
|
|
}
|
|
|
|
std::array<float, 3> Apply(const std::array<float, 3>& v) const {
|
|
const auto rotated = Rotate(v);
|
|
return { t[0] + s * rotated[0], t[1] + s * rotated[1], t[2] + s * rotated[2] };
|
|
}
|
|
|
|
// This transform, then `local` inside it (parent * child)
|
|
Transform Then(const Transform& local) const {
|
|
Transform out;
|
|
for (int row = 0; row < 3; row++) {
|
|
for (int col = 0; col < 3; col++) {
|
|
out.r[row * 3 + col] = r[row * 3] * local.r[col] + r[row * 3 + 1] * local.r[3 + col] + r[row * 3 + 2] * local.r[6 + col];
|
|
}
|
|
}
|
|
out.t = Apply(local.t);
|
|
out.s = s * local.s;
|
|
return out;
|
|
}
|
|
};
|
|
|
|
// The properties in effect at a point of the tree: a child's own property of a type replaces its parent's
|
|
struct Properties {
|
|
int32_t material = -1;
|
|
int32_t alpha = -1;
|
|
int32_t texturing = -1;
|
|
int32_t vertexColor = -1;
|
|
int32_t stencil = -1;
|
|
int32_t shaderTag = -1; // the nearest multishader tag ("S05__...") on the way down the tree
|
|
};
|
|
|
|
struct NetHeader {
|
|
std::string name;
|
|
};
|
|
|
|
struct AvHeader {
|
|
std::string name;
|
|
uint16_t flags{};
|
|
Transform transform;
|
|
std::vector<int32_t> properties;
|
|
};
|
|
|
|
class Parser {
|
|
public:
|
|
Parser(std::string_view data, uint32_t lod) : m_Data(data), m_Lod(lod) {}
|
|
|
|
std::optional<NifFile::Model> Run(std::string& error) {
|
|
if (!ReadHeader(error)) return std::nullopt;
|
|
m_Model.version = m_Version;
|
|
|
|
// The footer lists the roots; the first block is the root when it can't be read
|
|
std::vector<int32_t> roots;
|
|
Reader footer(m_Data.substr(m_FooterStart));
|
|
const auto count = footer.U32();
|
|
if (footer.Ok() && count <= m_Blocks.size()) roots = footer.Array<int32_t>(count);
|
|
if (roots.empty() && !m_Blocks.empty()) roots.push_back(0);
|
|
|
|
for (const auto root : roots) Visit(root, Transform{}, Properties{}, 0);
|
|
|
|
for (size_t i = 0; i < m_Blocks.size(); i++) {
|
|
if (!m_Used.contains(static_cast<int32_t>(i))) {
|
|
const auto& type = m_Types[m_Blocks[i].type];
|
|
if (!IsDrawnType(type)) m_Model.skipped[type]++;
|
|
}
|
|
}
|
|
|
|
bool first = true;
|
|
for (const auto& mesh : m_Model.meshes) {
|
|
for (size_t i = 0; i + 2 < mesh.positions.size(); i += 3) {
|
|
for (int axis = 0; axis < 3; axis++) {
|
|
const auto value = mesh.positions[i + axis];
|
|
m_Model.min[axis] = first ? value : std::min(m_Model.min[axis], value);
|
|
m_Model.max[axis] = first ? value : std::max(m_Model.max[axis], value);
|
|
}
|
|
first = false;
|
|
}
|
|
}
|
|
return std::move(m_Model);
|
|
}
|
|
|
|
// The DDS file for pixel data block `index` (see NifFile::EmbeddedTexture)
|
|
std::optional<std::string> Dds(int32_t index, std::string& error) {
|
|
if (!ReadHeader(error)) return std::nullopt;
|
|
const auto* type = TypeOf(index);
|
|
if (!type || (*type != "NiPixelData" && *type != "NiPersistentSrcTextureRendererData")) return std::nullopt;
|
|
auto reader = BlockReader(index);
|
|
const auto format = reader.U32();
|
|
reader.U8(); // bits per pixel
|
|
reader.U32(); // renderer hint
|
|
reader.U32(); // extra data
|
|
reader.U8(); // flags
|
|
const auto tiling = reader.U32();
|
|
if (m_Version >= Version(20, 3, 0, 4)) reader.U8(); // sRGB
|
|
reader.Skip(4 * 10); // channels
|
|
reader.I32(); // palette
|
|
const auto mipCount = reader.U32();
|
|
const auto bytesPerPixel = reader.U32();
|
|
if (!reader.Ok() || mipCount == 0 || mipCount > 16 || tiling != 0) return std::nullopt;
|
|
std::vector<std::array<uint32_t, 3>> mips; // width, height, offset
|
|
for (uint32_t i = 0; i < mipCount; i++) mips.push_back({ reader.U32(), reader.U32(), reader.U32() });
|
|
const auto pixelCount = reader.U32();
|
|
if (*type == "NiPersistentSrcTextureRendererData") {
|
|
reader.U32(); // padded pixel count
|
|
reader.U32(); // faces
|
|
reader.U32(); // platform
|
|
} else {
|
|
reader.U32(); // faces
|
|
}
|
|
std::string_view pixels;
|
|
if (reader.Ok() && pixelCount <= m_Blocks[index].size - reader.Position()) pixels = m_Data.substr(m_Blocks[index].offset + reader.Position(), pixelCount);
|
|
const auto width = mips[0][0], height = mips[0][1];
|
|
if (pixels.empty() || width == 0 || height == 0 || width > 8192 || height > 8192 || mips[0][2] != 0) return std::nullopt;
|
|
|
|
// DDS header (Microsoft's DDS_HEADER and DDS_PIXELFORMAT)
|
|
std::array<uint32_t, 31> header{};
|
|
header[0] = 124;
|
|
header[1] = 0x1 | 0x2 | 0x4 | 0x1000 | 0x20000; // caps, height, width, pixel format, mipmap count
|
|
header[2] = height;
|
|
header[3] = width;
|
|
header[6] = mipCount;
|
|
header[18] = 32; // pixel format size
|
|
if (format >= 4 && format <= 6) {
|
|
header[19] = 0x4; // four CC
|
|
const char* fourCc = format == 4 ? "DXT1" : format == 5 ? "DXT3" : "DXT5";
|
|
std::memcpy(&header[20], fourCc, 4);
|
|
} else if ((format == 0 && bytesPerPixel == 3) || (format == 1 && bytesPerPixel == 4)) {
|
|
header[19] = format == 1 ? 0x41 : 0x40; // RGB, with alpha
|
|
header[21] = bytesPerPixel * 8;
|
|
header[22] = 0x000000FF; // red first in memory
|
|
header[23] = 0x0000FF00;
|
|
header[24] = 0x00FF0000;
|
|
header[25] = format == 1 ? 0xFF000000 : 0;
|
|
} else {
|
|
return std::nullopt;
|
|
}
|
|
header[26] = 0x1000 | (mipCount > 1 ? 0x400008 : 0); // texture, mipmaps
|
|
std::string out = "DDS ";
|
|
out.append(reinterpret_cast<const char*>(header.data()), header.size() * 4);
|
|
out.append(pixels);
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
struct Block {
|
|
uint16_t type{};
|
|
size_t offset{};
|
|
uint32_t size{};
|
|
};
|
|
|
|
std::string_view m_Data;
|
|
uint32_t m_Lod;
|
|
uint32_t m_Version{};
|
|
std::vector<std::string> m_Types;
|
|
std::vector<std::string> m_Strings;
|
|
std::vector<Block> m_Blocks;
|
|
size_t m_FooterStart{};
|
|
std::set<int32_t> m_Used;
|
|
std::set<int32_t> m_Visiting;
|
|
std::vector<std::vector<float>> m_UvSets; // the UV sets of the geometry being read
|
|
NifFile::Model m_Model;
|
|
|
|
static bool IsDrawnType(const std::string& type) {
|
|
return type == "NiNode" || type == "NiLODNode" || type == "NiBillboardNode" || type == "NiSwitchNode" ||
|
|
type == "NiTriShape" || type == "NiTriStrips" || type == "NiTriShapeData" || type == "NiTriStripsData" ||
|
|
type == "NiMaterialProperty" || type == "NiAlphaProperty" || type == "NiTexturingProperty" || type == "NiSourceTexture" ||
|
|
type == "NiVertexColorProperty" || type == "NiStencilProperty" || type == "NiRangeLODData" ||
|
|
// Read and ignored: they change nothing a still picture shows
|
|
type == "NiSpecularProperty" || type == "NiZBufferProperty" || type == "NiShadeProperty" || type == "NiStringExtraData";
|
|
}
|
|
|
|
bool ReadHeader(std::string& error) {
|
|
const auto newline = m_Data.substr(0, 128).find('\n');
|
|
if (newline == std::string_view::npos || !(m_Data.starts_with("Gamebryo File Format") || m_Data.starts_with("NetImmerse File Format"))) {
|
|
error = "not a Gamebryo file";
|
|
return false;
|
|
}
|
|
Reader header(m_Data.substr(newline + 1));
|
|
m_Version = header.U32();
|
|
if (m_Version < MIN_VERSION || m_Version > MAX_VERSION) {
|
|
error = "unsupported version";
|
|
return false;
|
|
}
|
|
const auto endian = header.U8();
|
|
const auto userVersion = header.U32();
|
|
const auto blockCount = header.U32();
|
|
if (!header.Ok() || endian != 1 || userVersion != 0 || blockCount > MAX_BLOCKS) {
|
|
error = "unsupported header (big-endian or another game's user version)";
|
|
return false;
|
|
}
|
|
const auto typeCount = header.U16();
|
|
for (uint32_t i = 0; i < typeCount && header.Ok(); i++) m_Types.push_back(header.SizedString());
|
|
const auto typeIndex = header.Array<uint16_t>(blockCount);
|
|
const auto sizes = header.Array<uint32_t>(blockCount);
|
|
const auto stringCount = header.U32();
|
|
header.U32(); // longest string
|
|
for (uint32_t i = 0; i < stringCount && header.Ok(); i++) m_Strings.push_back(header.SizedString());
|
|
const auto groupCount = header.U32();
|
|
header.Skip(static_cast<uint64_t>(groupCount) * 4);
|
|
if (!header.Ok()) {
|
|
error = "truncated header";
|
|
return false;
|
|
}
|
|
|
|
size_t offset = newline + 1 + header.Position();
|
|
m_Blocks.reserve(blockCount);
|
|
for (uint32_t i = 0; i < blockCount; i++) {
|
|
const uint16_t type = typeIndex[i] & 0x7FFF; // the high bit marks PhysX blocks
|
|
if (type >= m_Types.size() || sizes[i] > m_Data.size() - offset) {
|
|
error = "block " + std::to_string(i) + " is out of range";
|
|
return false;
|
|
}
|
|
m_Blocks.push_back({ type, offset, sizes[i] });
|
|
offset += sizes[i];
|
|
}
|
|
m_FooterStart = offset;
|
|
return true;
|
|
}
|
|
|
|
const std::string* TypeOf(int32_t index) const {
|
|
if (index < 0 || static_cast<size_t>(index) >= m_Blocks.size()) return nullptr;
|
|
return &m_Types[m_Blocks[index].type];
|
|
}
|
|
|
|
Reader BlockReader(int32_t index) const {
|
|
const auto& block = m_Blocks[index];
|
|
return Reader(m_Data.substr(block.offset, block.size));
|
|
}
|
|
|
|
std::string String(uint32_t index) const {
|
|
return index < m_Strings.size() ? m_Strings[index] : std::string{};
|
|
}
|
|
|
|
NetHeader ReadNet(Reader& reader) {
|
|
NetHeader net;
|
|
net.name = String(reader.U32());
|
|
const auto extra = reader.U32();
|
|
reader.Skip(static_cast<uint64_t>(extra) * 4);
|
|
reader.I32(); // controller
|
|
return net;
|
|
}
|
|
|
|
AvHeader ReadAv(Reader& reader) {
|
|
AvHeader av;
|
|
av.name = ReadNet(reader).name;
|
|
av.flags = reader.U16();
|
|
for (auto& value : av.transform.t) value = reader.Float();
|
|
// Matrix33 is stored m11, m21, m31, m12, ... (nif.xml): column by column
|
|
std::array<float, 9> stored{};
|
|
for (auto& value : stored) value = reader.Float();
|
|
for (int row = 0; row < 3; row++) {
|
|
for (int col = 0; col < 3; col++) av.transform.r[row * 3 + col] = stored[col * 3 + row];
|
|
}
|
|
av.transform.s = reader.Float();
|
|
const auto count = reader.U32();
|
|
av.properties = reader.Array<int32_t>(count);
|
|
reader.I32(); // collision object
|
|
return av;
|
|
}
|
|
|
|
Properties Inherit(Properties properties, const std::vector<int32_t>& own) {
|
|
for (const auto ref : own) {
|
|
const auto* type = TypeOf(ref);
|
|
if (!type) continue;
|
|
if (*type == "NiMaterialProperty") properties.material = ref;
|
|
else if (*type == "NiAlphaProperty") properties.alpha = ref;
|
|
else if (*type == "NiTexturingProperty") properties.texturing = ref;
|
|
else if (*type == "NiVertexColorProperty") properties.vertexColor = ref;
|
|
else if (*type == "NiStencilProperty") properties.stencil = ref;
|
|
m_Used.insert(ref);
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
void Visit(int32_t index, const Transform& parent, Properties properties, uint32_t depth) {
|
|
const auto* type = TypeOf(index);
|
|
if (!type || depth > MAX_DEPTH || m_Visiting.contains(index)) return;
|
|
m_Visiting.insert(index);
|
|
if (*type == "NiNode" || *type == "NiLODNode" || *type == "NiBillboardNode" || *type == "NiSwitchNode") {
|
|
m_Used.insert(index);
|
|
VisitNode(index, *type, parent, properties, depth);
|
|
} else if (*type == "NiTriShape" || *type == "NiTriStrips") {
|
|
m_Used.insert(index);
|
|
VisitGeometry(index, parent, properties);
|
|
}
|
|
m_Visiting.erase(index);
|
|
}
|
|
|
|
void VisitNode(int32_t index, const std::string& type, const Transform& parent, Properties properties, uint32_t depth) {
|
|
auto reader = BlockReader(index);
|
|
const auto av = ReadAv(reader);
|
|
const auto childCount = reader.U32();
|
|
const auto children = reader.Array<int32_t>(childCount);
|
|
const auto effectCount = reader.U32();
|
|
reader.Skip(static_cast<uint64_t>(effectCount) * 4);
|
|
if (!reader.Ok()) return;
|
|
|
|
const auto world = parent.Then(av.transform);
|
|
// Recorded even when hidden: attach points often are
|
|
if (!av.name.empty() && !m_Model.nodes.contains(av.name)) {
|
|
NifFile::NodeTransform node;
|
|
for (int i = 0; i < 9; i++) node.rotation[i] = world.r[i] * world.s;
|
|
node.translation = world.t;
|
|
m_Model.nodes.emplace(av.name, node);
|
|
}
|
|
if (av.flags & APP_CULLED) return;
|
|
properties = Inherit(properties, av.properties);
|
|
if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag;
|
|
|
|
std::vector<int32_t> drawn = children;
|
|
if (type == "NiSwitchNode" || type == "NiLODNode") {
|
|
reader.U16(); // switch flags
|
|
const auto active = reader.U32();
|
|
drawn.clear();
|
|
if (type == "NiSwitchNode") {
|
|
if (reader.Ok() && active < children.size()) drawn.push_back(children[active]);
|
|
} else if (const auto chosen = ChooseLod(reader.I32(), children)) {
|
|
drawn.push_back(*chosen);
|
|
}
|
|
}
|
|
for (const auto child : drawn) Visit(child, world, properties, depth + 1);
|
|
}
|
|
|
|
// The child of an NiLODNode for m_Lod: children ordered nearest range first (the most detailed)
|
|
std::optional<int32_t> ChooseLod(int32_t dataRef, const std::vector<int32_t>& children) {
|
|
if (children.empty()) return std::nullopt;
|
|
std::vector<std::pair<float, int32_t>> order;
|
|
const auto* dataType = TypeOf(dataRef);
|
|
if (dataType && *dataType == "NiRangeLODData") {
|
|
m_Used.insert(dataRef);
|
|
auto data = BlockReader(dataRef);
|
|
data.Skip(12); // center
|
|
const auto levels = data.U32();
|
|
for (uint32_t i = 0; i < levels && data.Ok() && i < children.size(); i++) {
|
|
const auto nearExtent = data.Float();
|
|
data.Float(); // far extent
|
|
if (data.Ok()) order.emplace_back(nearExtent, children[i]);
|
|
}
|
|
}
|
|
if (order.size() != children.size()) {
|
|
order.clear();
|
|
for (size_t i = 0; i < children.size(); i++) order.emplace_back(static_cast<float>(i), children[i]);
|
|
}
|
|
std::stable_sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
|
|
return order[std::min<size_t>(m_Lod, order.size() - 1)].second;
|
|
}
|
|
|
|
void VisitGeometry(int32_t index, const Transform& parent, Properties properties) {
|
|
auto reader = BlockReader(index);
|
|
const auto av = ReadAv(reader);
|
|
const auto dataRef = reader.I32();
|
|
const auto skin = reader.I32();
|
|
if (!reader.Ok() || (av.flags & APP_CULLED)) return;
|
|
properties = Inherit(properties, av.properties);
|
|
if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag;
|
|
const auto* dataType = TypeOf(dataRef);
|
|
if (!dataType || (*dataType != "NiTriShapeData" && *dataType != "NiTriStripsData")) return;
|
|
m_Used.insert(dataRef);
|
|
|
|
NifFile::Mesh mesh;
|
|
m_UvSets.clear();
|
|
if (!ReadGeometryData(dataRef, *dataType == "NiTriStripsData", parent.Then(av.transform), mesh) || mesh.indices.empty()) return;
|
|
uint8_t baseSet = 0, darkSet = 0;
|
|
mesh.material = ReadMaterial(properties, baseSet, darkSet);
|
|
// Each texture reads the UV set its flags name (TexturingMapFlags' low byte), the first when that's missing
|
|
const auto set = [this](uint8_t index) { return index < m_UvSets.size() ? m_UvSets[index] : m_UvSets.empty() ? std::vector<float>{} : m_UvSets[0]; };
|
|
mesh.uvs = set(baseSet);
|
|
if (!mesh.material.darkTexture.empty() || mesh.material.embeddedDarkTexture >= 0) mesh.uvs2 = set(darkSet);
|
|
mesh.material.shaderTag = properties.shaderTag;
|
|
if (skin >= 0) {
|
|
m_Model.skinned++;
|
|
m_Used.insert(skin);
|
|
}
|
|
m_Model.meshes.push_back(std::move(mesh));
|
|
}
|
|
|
|
bool ReadGeometryData(int32_t index, bool strips, const Transform& transform, NifFile::Mesh& mesh) {
|
|
auto reader = BlockReader(index);
|
|
reader.I32(); // group ID
|
|
const auto count = reader.U16();
|
|
reader.U8(); // keep flags
|
|
reader.U8(); // compress flags
|
|
if (reader.U8()) {
|
|
const auto vertices = reader.Array<float>(static_cast<uint64_t>(count) * 3);
|
|
mesh.positions.reserve(vertices.size());
|
|
for (size_t i = 0; i + 2 < vertices.size(); i += 3) {
|
|
const auto p = transform.Apply({ vertices[i], vertices[i + 1], vertices[i + 2] });
|
|
mesh.positions.insert(mesh.positions.end(), p.begin(), p.end());
|
|
}
|
|
}
|
|
const auto dataFlags = reader.U16();
|
|
if (reader.U8()) {
|
|
const auto normals = reader.Array<float>(static_cast<uint64_t>(count) * 3);
|
|
mesh.normals.reserve(normals.size());
|
|
for (size_t i = 0; i + 2 < normals.size(); i += 3) {
|
|
auto n = transform.Rotate({ normals[i], normals[i + 1], normals[i + 2] });
|
|
const auto length = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
if (length > 0.0f) for (auto& value : n) value /= length;
|
|
mesh.normals.insert(mesh.normals.end(), n.begin(), n.end());
|
|
}
|
|
if (dataFlags & 4096) reader.Skip(static_cast<uint64_t>(count) * 24); // tangents and bitangents
|
|
}
|
|
reader.Skip(16); // bounding sphere
|
|
if (reader.U8()) {
|
|
const auto colors = reader.Array<float>(static_cast<uint64_t>(count) * 4);
|
|
mesh.colors.reserve(colors.size());
|
|
for (const auto value : colors) mesh.colors.push_back(static_cast<uint8_t>(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f)));
|
|
}
|
|
const auto uvSets = dataFlags & 63;
|
|
for (int set = 0; set < uvSets; set++) m_UvSets.push_back(reader.Array<float>(static_cast<uint64_t>(count) * 2));
|
|
reader.U16(); // consistency flags
|
|
reader.I32(); // additional data
|
|
const auto triangles = reader.U16();
|
|
if (!strips) {
|
|
reader.U32(); // triangle points
|
|
if (reader.U8()) mesh.indices = reader.Array<uint16_t>(static_cast<uint64_t>(triangles) * 3);
|
|
} else {
|
|
const auto stripCount = reader.U16();
|
|
const auto lengths = reader.Array<uint16_t>(stripCount);
|
|
if (reader.U8()) {
|
|
for (const auto length : lengths) {
|
|
const auto points = reader.Array<uint16_t>(length);
|
|
for (size_t i = 2; i < points.size(); i++) {
|
|
const auto a = points[i - 2], b = points[i - 1], c = points[i];
|
|
if (a == b || b == c || a == c) continue;
|
|
if (i % 2 == 0) mesh.indices.insert(mesh.indices.end(), { a, b, c });
|
|
else mesh.indices.insert(mesh.indices.end(), { a, c, b });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!reader.Ok() || mesh.positions.size() != static_cast<size_t>(count) * 3) return false;
|
|
if (mesh.normals.size() != mesh.positions.size()) mesh.normals.clear();
|
|
if (mesh.colors.size() != static_cast<size_t>(count) * 4) mesh.colors.clear();
|
|
std::erase_if(m_UvSets, [count](const std::vector<float>& set) { return set.size() != static_cast<size_t>(count) * 2; });
|
|
// Drop triangles pointing past the vertices
|
|
std::vector<uint16_t> valid;
|
|
valid.reserve(mesh.indices.size());
|
|
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
|
if (mesh.indices[i] < count && mesh.indices[i + 1] < count && mesh.indices[i + 2] < count) {
|
|
valid.insert(valid.end(), { mesh.indices[i], mesh.indices[i + 1], mesh.indices[i + 2] });
|
|
}
|
|
}
|
|
mesh.indices = std::move(valid);
|
|
return true;
|
|
}
|
|
|
|
// A texture slot's NiSourceTexture: an external file name or the block of pixels stored in the file
|
|
void ReadSource(int32_t source, std::string& file, int32_t& embedded) {
|
|
const auto* type = TypeOf(source);
|
|
if (!type || *type != "NiSourceTexture") return;
|
|
m_Used.insert(source);
|
|
auto texture = BlockReader(source);
|
|
ReadNet(texture);
|
|
const auto external = texture.U8();
|
|
const auto name = String(texture.U32());
|
|
const auto pixels = texture.I32();
|
|
const auto* pixelType = TypeOf(pixels);
|
|
if (texture.Ok() && external == 1) file = name;
|
|
else if (texture.Ok() && pixelType && (*pixelType == "NiPixelData" || *pixelType == "NiPersistentSrcTextureRendererData")) {
|
|
embedded = pixels;
|
|
m_Used.insert(pixels);
|
|
}
|
|
}
|
|
|
|
NifFile::Material ReadMaterial(const Properties& properties, uint8_t& baseSet, uint8_t& darkSet) {
|
|
NifFile::Material material;
|
|
if (properties.material >= 0) {
|
|
auto reader = BlockReader(properties.material);
|
|
ReadNet(reader);
|
|
reader.Skip(12); // ambient
|
|
std::array<float, 3> diffuse{}, emissive{};
|
|
for (auto& value : diffuse) value = reader.Float();
|
|
reader.Skip(12); // specular
|
|
for (auto& value : emissive) value = reader.Float();
|
|
reader.Float(); // glossiness
|
|
const auto alpha = reader.Float();
|
|
if (reader.Ok()) {
|
|
material.diffuse = diffuse;
|
|
material.emissive = emissive;
|
|
material.alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
}
|
|
}
|
|
if (properties.alpha >= 0) {
|
|
auto reader = BlockReader(properties.alpha);
|
|
ReadNet(reader);
|
|
const auto flags = reader.U16();
|
|
const auto threshold = reader.U8();
|
|
if (reader.Ok()) {
|
|
material.alphaBlend = flags & 1;
|
|
material.alphaTest = flags & 0x200;
|
|
material.alphaThreshold = threshold;
|
|
}
|
|
}
|
|
if (properties.vertexColor >= 0) {
|
|
auto reader = BlockReader(properties.vertexColor);
|
|
ReadNet(reader);
|
|
const auto flags = reader.U16();
|
|
if (reader.Ok()) material.vertexColorMode = static_cast<uint8_t>((flags >> 4) & 3);
|
|
}
|
|
if (properties.stencil >= 0) {
|
|
auto reader = BlockReader(properties.stencil);
|
|
ReadNet(reader);
|
|
const auto flags = reader.U16();
|
|
if (reader.Ok()) material.doubleSided = ((flags >> 10) & 3) == 3; // DRAW_BOTH
|
|
}
|
|
if (properties.texturing >= 0) {
|
|
auto reader = BlockReader(properties.texturing);
|
|
ReadNet(reader);
|
|
reader.U16(); // flags
|
|
reader.U32(); // texture count
|
|
// TexDesc (nif.xml, 20.1.0.3 on): source, TexturingMapFlags (clamp in bits 12-15, UV set in 0-7), whether a
|
|
// texture transform follows (translation, scale, rotation, method, center: 32 bytes)
|
|
const auto texDesc = [&reader](int32_t& source, uint16_t& flags) {
|
|
source = reader.I32();
|
|
flags = reader.U16();
|
|
if (reader.U8()) reader.Skip(32);
|
|
};
|
|
int32_t source = -1;
|
|
uint16_t flags = 0;
|
|
if (reader.U8()) { // has base texture
|
|
texDesc(source, flags);
|
|
if (reader.Ok()) {
|
|
ReadSource(source, material.texture, material.embeddedTexture);
|
|
const auto clamp = (flags >> 12) & 0xF;
|
|
material.clampU = clamp == 0 || clamp == 1;
|
|
material.clampV = clamp == 0 || clamp == 2;
|
|
baseSet = static_cast<uint8_t>(flags & 0xFF);
|
|
}
|
|
}
|
|
if (reader.U8()) { // has dark texture
|
|
texDesc(source, flags);
|
|
if (reader.Ok()) {
|
|
ReadSource(source, material.darkTexture, material.embeddedDarkTexture);
|
|
darkSet = static_cast<uint8_t>(flags & 0xFF);
|
|
}
|
|
}
|
|
}
|
|
return material;
|
|
}
|
|
};
|
|
|
|
void Append(std::string& out, const void* data, size_t bytes) {
|
|
out.append(static_cast<const char*>(data), bytes);
|
|
}
|
|
|
|
void Pad(std::string& out) {
|
|
while (out.size() % 4) out.push_back('\0');
|
|
}
|
|
|
|
nlohmann::json Color(const std::array<float, 3>& color) {
|
|
return { std::round(color[0] * 1000.0f) / 1000.0f, std::round(color[1] * 1000.0f) / 1000.0f, std::round(color[2] * 1000.0f) / 1000.0f };
|
|
}
|
|
}
|
|
|
|
namespace NifFile {
|
|
int32_t ShaderTag(std::string_view name) {
|
|
// The client reads the tag with sscanf: "S%d" at the start of the name, else "_S%d" after the first "_S"
|
|
const auto number = [](std::string_view digits) -> int32_t {
|
|
size_t i = 0;
|
|
while (i < digits.size() && (digits[i] == ' ' || digits[i] == '\t')) i++;
|
|
// A signed number names no mapShaders row, like no number at all
|
|
if (i >= digits.size() || digits[i] < '0' || digits[i] > '9') return -1;
|
|
int32_t value = 0;
|
|
for (; i < digits.size() && digits[i] >= '0' && digits[i] <= '9' && value < 100000; i++) value = value * 10 + (digits[i] - '0');
|
|
return value;
|
|
};
|
|
if (name.starts_with('S')) {
|
|
const auto tag = number(name.substr(1));
|
|
if (tag >= 0) return tag;
|
|
}
|
|
const auto at = name.find("_S");
|
|
if (at == std::string_view::npos || at + 3 >= name.size()) return -1;
|
|
return number(name.substr(at + 2));
|
|
}
|
|
|
|
int32_t MultishaderPart(std::optional<int32_t> tagShader) {
|
|
return tagShader && *tagShader >= 3 && *tagShader <= 0x6C ? *tagShader : LEGO_SHADER;
|
|
}
|
|
|
|
eTextureAlpha TextureAlphaFor(int32_t shader) {
|
|
switch (shader) {
|
|
// LEGOPPLighting: textured alone the texture's alpha is forced to 1; with vertex colors it only lays the
|
|
// texture over them (lerp by its alpha) and the vertex alpha is what shows through
|
|
case 4: case 5: case 12: case 25: case 27: case 28: case 29: case 30: case 50: case 72: case 88:
|
|
// Darkling: the same lay-over; alpha from lighting or the fade
|
|
case 75: case 76: case 77: case 102: case 103: case 104:
|
|
return eTextureAlpha::DECAL;
|
|
// LEGOPPLighting_Item: texture alpha forced to 1, multiplied by the vertex colors
|
|
case 31: case 48:
|
|
// TerrainMeshLighting_Rim: texture times vertex colors, alpha only the fade
|
|
case 3:
|
|
return eTextureAlpha::IGNORED;
|
|
default:
|
|
return eTextureAlpha::OPACITY;
|
|
}
|
|
}
|
|
|
|
uint8_t ShaderLookFor(int32_t shader) {
|
|
// By the technique each shader class sets up (ShaderManager's factory table at 0x01889608, indexed by gameValue;
|
|
// the class's technique setup names it). Checked in the client: 33 and 82 Technique_Basic_NoLighting_VertColor_
|
|
// NoTexture, 35 and 84 Technique_Basic_NoLighting_VertColor, 37 Technique_Basic_Lighting_VertColor_NoTexture,
|
|
// 38 and 94 Technique_Basic_Lighting_VertColor, 70 Technique_AlphaAsAlpha_UVScrolling_SimpleV_NoLighting_
|
|
// AlphaAnim, 105 Technique_TwoLayersBlended_NoLighting_VertColor_UVScrolling. The rest follow their mapShaders
|
|
// labels ("NL" no lighting, "NT" no texture, "VC" vertex colors)
|
|
switch (shader) {
|
|
// Basic NL Material, Over Everything Material Unlit
|
|
case 32: case 108:
|
|
return UNLIT | NO_VERTEX_COLORS | MATERIAL_COLOR;
|
|
// Basic NL, Basic NL UVAnim, OneSidedAlpha NL (and skinned), Opaque NL NoFog
|
|
case 34: case 36: case 56: case 61: case 83:
|
|
return UNLIT | NO_VERTEX_COLORS;
|
|
// (94 "Basic" is Technique_Basic_Lighting_VertColor like "Basic VC": its shader's technique setup, the vtable
|
|
// slot at +0x90 of the class made at 0x0045f240, names that technique, so it's the usual look)
|
|
// VertColor_NoLight_NoTex_AnimAlpha, VC_NL_NoTex_2D, Basic NL VC NT, OneSidedAlpha NL VC NT (and skinned),
|
|
// Basic NL NT, Opaque NL VC NT NoFog
|
|
case 11: case 16: case 33: case 58: case 63: case 80: case 82:
|
|
return UNLIT | NO_TEXTURE;
|
|
// Basic VC NT, Opaque VC NT NoFog
|
|
case 37: case 85:
|
|
return NO_TEXTURE;
|
|
// Two Textures Added NL VC AnimUV (TwoLayersAdded_PS in BasicShaders.fx), Two Layers Added VC AnimUV
|
|
case 93:
|
|
return UNLIT | TWO_LAYERS_ADDED;
|
|
case 107:
|
|
return TWO_LAYERS_ADDED;
|
|
// Two Layers Blended NL VC AnimUV and Two Layers Blended VC AnimUV. The client names techniques for them
|
|
// (Technique_TwoLayersBlended_*) that no shader it ships has, so how the game draws them is a guess: the dark
|
|
// texture under the base one by the vertex alpha, as the meshes' data suggests (Avant Gardens' snow caps
|
|
// and grass fade into rock by it)
|
|
case 105:
|
|
return UNLIT | TWO_LAYERS_BLENDED;
|
|
case 106:
|
|
return TWO_LAYERS_BLENDED;
|
|
// VertColor_NoLighting_Alpha, VertColorTex_NoLight_AlphaBlend and _AlphaTest, VC_NoLighting_2D, Over
|
|
// Everything (Unlit), Basic NL VC, LEGO-No Light, OneSidedAlpha NL VC (and skinned), OneSidedAlpha NL
|
|
// AnimAlpha, the NoLight scrolling UVs, Opaque NL VC NoFog, Additive NoLight VertColor, Distortion (Ocean)
|
|
// Unlit
|
|
case 8: case 10: case 54: case 15: case 23: case 35: case 52: case 57: case 62: case 68: case 70: case 73: case 81:
|
|
case 84: case 87: case 101:
|
|
return UNLIT;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
std::optional<Model> Parse(std::string_view data, uint32_t lod, std::string& error) {
|
|
return Parser(data, lod).Run(error);
|
|
}
|
|
|
|
std::optional<std::string> EmbeddedTexture(std::string_view data, int32_t block) {
|
|
std::string error;
|
|
return Parser(data, 0).Dds(block, error);
|
|
}
|
|
|
|
std::string Encode(const Model& model, const std::vector<std::string>& textures, const std::vector<std::string>& darkTextures) {
|
|
std::string body;
|
|
nlohmann::json meshes = nlohmann::json::array();
|
|
std::vector<std::string> names;
|
|
for (size_t m = 0; m < model.meshes.size(); m++) {
|
|
const auto& mesh = model.meshes[m];
|
|
const auto& material = mesh.material;
|
|
const auto vertices = mesh.positions.size() / 3;
|
|
const std::string texture = m < textures.size() ? textures[m] : std::string{};
|
|
const auto indexOf = [&names](const std::string& name) {
|
|
if (name.empty()) return -1;
|
|
const auto it = std::find(names.begin(), names.end(), name);
|
|
const auto index = static_cast<int32_t>(it - names.begin());
|
|
if (it == names.end()) names.push_back(name);
|
|
return index;
|
|
};
|
|
const int32_t textureIndex = indexOf(texture);
|
|
const int32_t darkIndex = indexOf(m < darkTextures.size() ? darkTextures[m] : std::string{});
|
|
const bool uv2 = darkIndex >= 0 && mesh.uvs2.size() == vertices * 2;
|
|
nlohmann::json entry{
|
|
{"offset", body.size()}, {"vertices", vertices}, {"indices", mesh.indices.size()},
|
|
{"normals", !mesh.normals.empty()}, {"uv", !mesh.uvs.empty() && textureIndex >= 0}, {"colors", !mesh.colors.empty()},
|
|
{"diffuse", Color(material.diffuse)}, {"emissive", Color(material.emissive)}, {"alpha", std::round(material.alpha * 1000.0f) / 1000.0f},
|
|
{"blend", material.alphaBlend}, {"test", material.alphaTest ? material.alphaThreshold : -1}, {"doubleSided", material.doubleSided},
|
|
{"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV},
|
|
{"shaderTag", material.shaderTag}, {"darkTexture", uv2 ? darkIndex : -1}, {"uv2", uv2}
|
|
};
|
|
Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float));
|
|
if (!mesh.normals.empty()) {
|
|
std::vector<int8_t> packed(mesh.normals.size());
|
|
for (size_t i = 0; i < packed.size(); i++) packed[i] = static_cast<int8_t>(std::lround(std::clamp(mesh.normals[i], -1.0f, 1.0f) * 127.0f));
|
|
Append(body, packed.data(), packed.size());
|
|
Pad(body);
|
|
}
|
|
if (entry["uv"].get<bool>()) Append(body, mesh.uvs.data(), mesh.uvs.size() * sizeof(float));
|
|
if (uv2) Append(body, mesh.uvs2.data(), mesh.uvs2.size() * sizeof(float));
|
|
if (!mesh.colors.empty()) Append(body, mesh.colors.data(), mesh.colors.size());
|
|
Append(body, mesh.indices.data(), mesh.indices.size() * sizeof(uint16_t));
|
|
Pad(body);
|
|
meshes.push_back(std::move(entry));
|
|
}
|
|
nlohmann::json header{
|
|
{"version", 1}, {"meshes", meshes}, {"textures", names},
|
|
{"min", { model.min[0], model.min[1], model.min[2] }}, {"max", { model.max[0], model.max[1], model.max[2] }}
|
|
};
|
|
auto text = header.dump();
|
|
while (text.size() % 4) text.push_back(' ');
|
|
std::string out;
|
|
const auto length = static_cast<uint32_t>(text.size());
|
|
Append(out, &length, sizeof(length));
|
|
out += text;
|
|
out += body;
|
|
return out;
|
|
}
|
|
|
|
std::optional<std::string> KfmModelPath(std::string_view data) {
|
|
const auto newline = data.substr(0, 128).find('\n');
|
|
if (newline == std::string_view::npos || data.substr(0, newline).find("KFM") == std::string_view::npos) return std::nullopt;
|
|
Reader reader(data.substr(newline + 1));
|
|
reader.U8(); // little endian
|
|
auto path = reader.SizedString();
|
|
if (!reader.Ok() || path.empty()) return std::nullopt;
|
|
return path;
|
|
}
|
|
}
|