mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
fix(dashboard): white pines, see-through hills and fighting surfaces in the 3D views
- 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>
This commit is contained in:
@@ -248,6 +248,7 @@ namespace {
|
|||||||
size_t m_FooterStart{};
|
size_t m_FooterStart{};
|
||||||
std::set<int32_t> m_Used;
|
std::set<int32_t> m_Used;
|
||||||
std::set<int32_t> m_Visiting;
|
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;
|
NifFile::Model m_Model;
|
||||||
|
|
||||||
static bool IsDrawnType(const std::string& type) {
|
static bool IsDrawnType(const std::string& type) {
|
||||||
@@ -448,8 +449,14 @@ namespace {
|
|||||||
m_Used.insert(dataRef);
|
m_Used.insert(dataRef);
|
||||||
|
|
||||||
NifFile::Mesh mesh;
|
NifFile::Mesh mesh;
|
||||||
|
m_UvSets.clear();
|
||||||
if (!ReadGeometryData(dataRef, *dataType == "NiTriStripsData", parent.Then(av.transform), mesh) || mesh.indices.empty()) return;
|
if (!ReadGeometryData(dataRef, *dataType == "NiTriStripsData", parent.Then(av.transform), mesh) || mesh.indices.empty()) return;
|
||||||
mesh.material = ReadMaterial(properties);
|
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;
|
mesh.material.shaderTag = properties.shaderTag;
|
||||||
if (skin >= 0) {
|
if (skin >= 0) {
|
||||||
m_Model.skinned++;
|
m_Model.skinned++;
|
||||||
@@ -491,10 +498,7 @@ namespace {
|
|||||||
for (const auto value : colors) mesh.colors.push_back(static_cast<uint8_t>(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f)));
|
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;
|
const auto uvSets = dataFlags & 63;
|
||||||
if (uvSets > 0) {
|
for (int set = 0; set < uvSets; set++) m_UvSets.push_back(reader.Array<float>(static_cast<uint64_t>(count) * 2));
|
||||||
mesh.uvs = reader.Array<float>(static_cast<uint64_t>(count) * 2);
|
|
||||||
reader.Skip(static_cast<uint64_t>(uvSets - 1) * count * 8);
|
|
||||||
}
|
|
||||||
reader.U16(); // consistency flags
|
reader.U16(); // consistency flags
|
||||||
reader.I32(); // additional data
|
reader.I32(); // additional data
|
||||||
const auto triangles = reader.U16();
|
const auto triangles = reader.U16();
|
||||||
@@ -519,7 +523,7 @@ namespace {
|
|||||||
if (!reader.Ok() || mesh.positions.size() != static_cast<size_t>(count) * 3) return false;
|
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.normals.size() != mesh.positions.size()) mesh.normals.clear();
|
||||||
if (mesh.colors.size() != static_cast<size_t>(count) * 4) mesh.colors.clear();
|
if (mesh.colors.size() != static_cast<size_t>(count) * 4) mesh.colors.clear();
|
||||||
if (mesh.uvs.size() != static_cast<size_t>(count) * 2) mesh.uvs.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
|
// Drop triangles pointing past the vertices
|
||||||
std::vector<uint16_t> valid;
|
std::vector<uint16_t> valid;
|
||||||
valid.reserve(mesh.indices.size());
|
valid.reserve(mesh.indices.size());
|
||||||
@@ -532,7 +536,25 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
NifFile::Material ReadMaterial(const Properties& properties) {
|
// 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;
|
NifFile::Material material;
|
||||||
if (properties.material >= 0) {
|
if (properties.material >= 0) {
|
||||||
auto reader = BlockReader(properties.material);
|
auto reader = BlockReader(properties.material);
|
||||||
@@ -578,26 +600,30 @@ namespace {
|
|||||||
ReadNet(reader);
|
ReadNet(reader);
|
||||||
reader.U16(); // flags
|
reader.U16(); // flags
|
||||||
reader.U32(); // texture count
|
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
|
if (reader.U8()) { // has base texture
|
||||||
const auto source = reader.I32();
|
texDesc(source, flags);
|
||||||
const auto flags = reader.U16();
|
if (reader.Ok()) {
|
||||||
const auto* type = TypeOf(source);
|
ReadSource(source, material.texture, material.embeddedTexture);
|
||||||
if (reader.Ok() && type && *type == "NiSourceTexture") {
|
|
||||||
m_Used.insert(source);
|
|
||||||
auto texture = BlockReader(source);
|
|
||||||
ReadNet(texture);
|
|
||||||
const auto external = texture.U8();
|
|
||||||
const auto file = String(texture.U32());
|
|
||||||
const auto pixels = texture.I32();
|
|
||||||
const auto* pixelType = TypeOf(pixels);
|
|
||||||
if (texture.Ok() && external == 1) material.texture = file;
|
|
||||||
else if (texture.Ok() && pixelType && (*pixelType == "NiPixelData" || *pixelType == "NiPersistentSrcTextureRendererData")) {
|
|
||||||
material.embeddedTexture = pixels;
|
|
||||||
m_Used.insert(pixels);
|
|
||||||
}
|
|
||||||
const auto clamp = (flags >> 12) & 0xF;
|
const auto clamp = (flags >> 12) & 0xF;
|
||||||
material.clampU = clamp == 0 || clamp == 1;
|
material.clampU = clamp == 0 || clamp == 1;
|
||||||
material.clampV = clamp == 0 || clamp == 2;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,9 +688,12 @@ namespace NifFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ShaderLookFor(int32_t shader) {
|
uint8_t ShaderLookFor(int32_t shader) {
|
||||||
// By the techniques of each mapShaders row (its label names them: "NL" no lighting, "NT" no texture, "VC"
|
// By the technique each shader class sets up (ShaderManager's factory table at 0x01889608, indexed by gameValue;
|
||||||
// vertex colors), e.g. 38 "Basic VC" is Technique_Basic_Lighting_VertColor, 33 "Basic NL VC NT"
|
// the class's technique setup names it). Checked in the client: 33 and 82 Technique_Basic_NoLighting_VertColor_
|
||||||
// Technique_Basic_NoLighting_VertColor_NoTexture, 32 "Basic NL Material" Technique_Basic_Material_NoLighting
|
// 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) {
|
switch (shader) {
|
||||||
// Basic NL Material, Over Everything Material Unlit
|
// Basic NL Material, Over Everything Material Unlit
|
||||||
case 32: case 108:
|
case 32: case 108:
|
||||||
@@ -672,9 +701,8 @@ namespace NifFile {
|
|||||||
// Basic NL, Basic NL UVAnim, OneSidedAlpha NL (and skinned), Opaque NL NoFog
|
// Basic NL, Basic NL UVAnim, OneSidedAlpha NL (and skinned), Opaque NL NoFog
|
||||||
case 34: case 36: case 56: case 61: case 83:
|
case 34: case 36: case 56: case 61: case 83:
|
||||||
return UNLIT | NO_VERTEX_COLORS;
|
return UNLIT | NO_VERTEX_COLORS;
|
||||||
// Basic, the lit one without vertex colors
|
// (94 "Basic" is Technique_Basic_Lighting_VertColor like "Basic VC": its shader's technique setup, the vtable
|
||||||
case 94:
|
// slot at +0x90 of the class made at 0x0045f240, names that technique, so it's the usual look)
|
||||||
return NO_VERTEX_COLORS;
|
|
||||||
// VertColor_NoLight_NoTex_AnimAlpha, VC_NL_NoTex_2D, Basic NL VC NT, OneSidedAlpha NL VC NT (and skinned),
|
// 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
|
// Basic NL NT, Opaque NL VC NT NoFog
|
||||||
case 11: case 16: case 33: case 58: case 63: case 80: case 82:
|
case 11: case 16: case 33: case 58: case 63: case 80: case 82:
|
||||||
@@ -682,12 +710,25 @@ namespace NifFile {
|
|||||||
// Basic VC NT, Opaque VC NT NoFog
|
// Basic VC NT, Opaque VC NT NoFog
|
||||||
case 37: case 85:
|
case 37: case 85:
|
||||||
return NO_TEXTURE;
|
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
|
// 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
|
// 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, Two Textures Added NL
|
// AnimAlpha, the NoLight scrolling UVs, Opaque NL VC NoFog, Additive NoLight VertColor, Distortion (Ocean)
|
||||||
// VC AnimUV, Distortion (Ocean) Unlit, Two Layers Blended NL VC AnimUV
|
// 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 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 93: case 101: case 105:
|
case 84: case 87: case 101:
|
||||||
return UNLIT;
|
return UNLIT;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -703,7 +744,7 @@ namespace NifFile {
|
|||||||
return Parser(data, 0).Dds(block, error);
|
return Parser(data, 0).Dds(block, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Encode(const Model& model, const std::vector<std::string>& textures) {
|
std::string Encode(const Model& model, const std::vector<std::string>& textures, const std::vector<std::string>& darkTextures) {
|
||||||
std::string body;
|
std::string body;
|
||||||
nlohmann::json meshes = nlohmann::json::array();
|
nlohmann::json meshes = nlohmann::json::array();
|
||||||
std::vector<std::string> names;
|
std::vector<std::string> names;
|
||||||
@@ -712,19 +753,23 @@ namespace NifFile {
|
|||||||
const auto& material = mesh.material;
|
const auto& material = mesh.material;
|
||||||
const auto vertices = mesh.positions.size() / 3;
|
const auto vertices = mesh.positions.size() / 3;
|
||||||
const std::string texture = m < textures.size() ? textures[m] : std::string{};
|
const std::string texture = m < textures.size() ? textures[m] : std::string{};
|
||||||
int32_t textureIndex = -1;
|
const auto indexOf = [&names](const std::string& name) {
|
||||||
if (!texture.empty()) {
|
if (name.empty()) return -1;
|
||||||
const auto it = std::find(names.begin(), names.end(), texture);
|
const auto it = std::find(names.begin(), names.end(), name);
|
||||||
textureIndex = static_cast<int32_t>(it - names.begin());
|
const auto index = static_cast<int32_t>(it - names.begin());
|
||||||
if (it == names.end()) names.push_back(texture);
|
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{
|
nlohmann::json entry{
|
||||||
{"offset", body.size()}, {"vertices", vertices}, {"indices", mesh.indices.size()},
|
{"offset", body.size()}, {"vertices", vertices}, {"indices", mesh.indices.size()},
|
||||||
{"normals", !mesh.normals.empty()}, {"uv", !mesh.uvs.empty() && textureIndex >= 0}, {"colors", !mesh.colors.empty()},
|
{"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},
|
{"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},
|
{"blend", material.alphaBlend}, {"test", material.alphaTest ? material.alphaThreshold : -1}, {"doubleSided", material.doubleSided},
|
||||||
{"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV},
|
{"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV},
|
||||||
{"shaderTag", material.shaderTag}
|
{"shaderTag", material.shaderTag}, {"darkTexture", uv2 ? darkIndex : -1}, {"uv2", uv2}
|
||||||
};
|
};
|
||||||
Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float));
|
Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float));
|
||||||
if (!mesh.normals.empty()) {
|
if (!mesh.normals.empty()) {
|
||||||
@@ -734,6 +779,7 @@ namespace NifFile {
|
|||||||
Pad(body);
|
Pad(body);
|
||||||
}
|
}
|
||||||
if (entry["uv"].get<bool>()) Append(body, mesh.uvs.data(), mesh.uvs.size() * sizeof(float));
|
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());
|
if (!mesh.colors.empty()) Append(body, mesh.colors.data(), mesh.colors.size());
|
||||||
Append(body, mesh.indices.data(), mesh.indices.size() * sizeof(uint16_t));
|
Append(body, mesh.indices.data(), mesh.indices.size() * sizeof(uint16_t));
|
||||||
Pad(body);
|
Pad(body);
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ namespace NifFile {
|
|||||||
int32_t embeddedTexture{ -1 }; // else the block with the texture's pixels in the file (EmbeddedTexture)
|
int32_t embeddedTexture{ -1 }; // else the block with the texture's pixels in the file (EmbeddedTexture)
|
||||||
bool clampU{};
|
bool clampU{};
|
||||||
bool clampV{};
|
bool clampV{};
|
||||||
|
// NiTexturingProperty's dark texture (its second slot), which the client's two layer shaders blend or add to
|
||||||
|
// the base texture; as `texture` and `embeddedTexture`
|
||||||
|
std::string darkTexture;
|
||||||
|
int32_t embeddedDarkTexture{ -1 };
|
||||||
int32_t shaderTag{ -1 }; // mapShaders id from a multishader tag in the name of the mesh or a node above it
|
int32_t shaderTag{ -1 }; // mapShaders id from a multishader tag in the name of the mesh or a node above it
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +46,8 @@ namespace NifFile {
|
|||||||
Material material;
|
Material material;
|
||||||
std::vector<float> positions; // x, y, z per vertex, in the model's space
|
std::vector<float> positions; // x, y, z per vertex, in the model's space
|
||||||
std::vector<float> normals; // empty when the geometry has none
|
std::vector<float> normals; // empty when the geometry has none
|
||||||
std::vector<float> uvs; // u, v per vertex, empty when none
|
std::vector<float> uvs; // u, v per vertex, empty when none: the UV set the base texture names
|
||||||
|
std::vector<float> uvs2; // the UV set the dark texture names, empty without one
|
||||||
std::vector<uint8_t> colors; // r, g, b, a per vertex (sRGB), empty when none
|
std::vector<uint8_t> colors; // r, g, b, a per vertex (sRGB), empty when none
|
||||||
std::vector<uint16_t> indices; // triangles
|
std::vector<uint16_t> indices; // triangles
|
||||||
};
|
};
|
||||||
@@ -82,7 +87,11 @@ namespace NifFile {
|
|||||||
UNLIT = 1, // no lighting: the colors as they are (the "NoLighting" techniques)
|
UNLIT = 1, // no lighting: the colors as they are (the "NoLighting" techniques)
|
||||||
NO_TEXTURE = 2, // the texture isn't sampled ("NoTexture")
|
NO_TEXTURE = 2, // the texture isn't sampled ("NoTexture")
|
||||||
NO_VERTEX_COLORS = 4, // vertex colors aren't read
|
NO_VERTEX_COLORS = 4, // vertex colors aren't read
|
||||||
MATERIAL_COLOR = 8 // NiMaterialProperty's diffuse color is (the "Material" techniques)
|
MATERIAL_COLOR = 8, // NiMaterialProperty's diffuse color is (the "Material" techniques)
|
||||||
|
// Two textures (base and dark, each with its UV set): blended by the vertex alpha, which is then no opacity
|
||||||
|
// ("Two Layers Blended"), or added, weighted by the material's diffuse red and green ("Two Textures Added")
|
||||||
|
TWO_LAYERS_BLENDED = 16,
|
||||||
|
TWO_LAYERS_ADDED = 32
|
||||||
};
|
};
|
||||||
|
|
||||||
// eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function
|
// eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function
|
||||||
@@ -114,10 +123,11 @@ namespace NifFile {
|
|||||||
* A model for the browser: a little-endian uint32 with the length of a JSON header, the header (padded with spaces
|
* A model for the browser: a little-endian uint32 with the length of a JSON header, the header (padded with spaces
|
||||||
* to a multiple of 4), then the binary data it describes. Per mesh at "offset": float32 positions (3 per vertex),
|
* to a multiple of 4), then the binary data it describes. Per mesh at "offset": float32 positions (3 per vertex),
|
||||||
* int8 normals (3 per vertex, times 127, padded to 4 bytes) when "normals", float32 UVs (2 per vertex) when "uv",
|
* int8 normals (3 per vertex, times 127, padded to 4 bytes) when "normals", float32 UVs (2 per vertex) when "uv",
|
||||||
* uint8 RGBA colors when "colors", then uint16 indices (padded to 4 bytes). `textures[i]` is where mesh i's texture
|
* float32 dark texture UVs when "uv2", uint8 RGBA colors when "colors", then uint16 indices (padded to 4 bytes).
|
||||||
* is (empty: none); the header lists each once in "textures" and a mesh's "texture" indexes it (-1: none).
|
* `textures[i]` is where mesh i's texture is (empty: none) and `darkTextures[i]` its dark texture; the header
|
||||||
|
* lists each once in "textures" and a mesh's "texture" and "darkTexture" index it (-1: none).
|
||||||
*/
|
*/
|
||||||
std::string Encode(const Model& model, const std::vector<std::string>& textures);
|
std::string Encode(const Model& model, const std::vector<std::string>& textures, const std::vector<std::string>& darkTextures = {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A texture stored inside a .nif (NiPixelData or NiPersistentSrcTextureRendererData, block `block`) as a DDS file
|
* A texture stored inside a .nif (NiPixelData or NiPersistentSrcTextureRendererData, block `block`) as a DDS file
|
||||||
|
|||||||
@@ -245,8 +245,9 @@ namespace {
|
|||||||
* Bump when NifFile's output changes: converted models kept on disk are made again, and the manifests' "format"
|
* Bump when NifFile's output changes: converted models kept on disk are made again, and the manifests' "format"
|
||||||
* goes into the viewers' model and texture URLs so browsers don't keep drawing the old ones (they're cached for
|
* goes into the viewers' model and texture URLs so browsers don't keep drawing the old ones (they're cached for
|
||||||
* a week). 2: meshes carry their multishader tag; conversions without it drew glom parts with the LEGO shader.
|
* a week). 2: meshes carry their multishader tag; conversions without it drew glom parts with the LEGO shader.
|
||||||
|
* 3: dark textures and the UV set each texture names.
|
||||||
*/
|
*/
|
||||||
constexpr uint32_t FORMAT_VERSION = 2;
|
constexpr uint32_t FORMAT_VERSION = 3;
|
||||||
|
|
||||||
// A zone's lighting (WorldScene::Lighting) for the viewers' shaders
|
// A zone's lighting (WorldScene::Lighting) for the viewers' shaders
|
||||||
nlohmann::json LightingJson(const WorldScene::Lighting& lighting) {
|
nlohmann::json LightingJson(const WorldScene::Lighting& lighting) {
|
||||||
@@ -591,12 +592,17 @@ namespace {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
const auto folder = FolderOf(path);
|
const auto folder = FolderOf(path);
|
||||||
std::vector<std::string> textures; // per mesh: a res path, "#<block>" for one stored in the .nif, or empty
|
// Per mesh: a res path, "#<block>" for one stored in the .nif, or empty; for its base and its dark texture
|
||||||
|
const auto where = [&folder](int32_t embedded, const std::string& file) {
|
||||||
|
if (embedded >= 0) return "#" + std::to_string(embedded);
|
||||||
|
return file.empty() ? std::string{} : FindTexture(folder, file);
|
||||||
|
};
|
||||||
|
std::vector<std::string> textures, darkTextures;
|
||||||
for (const auto& mesh : model->meshes) {
|
for (const auto& mesh : model->meshes) {
|
||||||
if (mesh.material.embeddedTexture >= 0) textures.push_back("#" + std::to_string(mesh.material.embeddedTexture));
|
textures.push_back(where(mesh.material.embeddedTexture, mesh.material.texture));
|
||||||
else textures.push_back(mesh.material.texture.empty() ? std::string{} : FindTexture(folder, mesh.material.texture));
|
darkTextures.push_back(where(mesh.material.embeddedDarkTexture, mesh.material.darkTexture));
|
||||||
}
|
}
|
||||||
auto encoded = std::make_shared<const std::string>(NifFile::Encode(*model, textures));
|
auto encoded = std::make_shared<const std::string>(NifFile::Encode(*model, textures, darkTextures));
|
||||||
g_Disk.Store(target, *encoded);
|
g_Disk.Store(target, *encoded);
|
||||||
return encoded;
|
return encoded;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||||
import { parseModel, mergeMeshes, linearColors } from '/js/scenery-core.js';
|
import { parseModel, mergeMeshes, linearColors, nearPlaneFor } from '/js/scenery-core.js';
|
||||||
|
|
||||||
const GEOMETRY_MAGIC = 0x42473031; // "10GB"
|
const GEOMETRY_MAGIC = 0x42473031; // "10GB"
|
||||||
const MAX_PARALLEL_FETCHES = 6;
|
const MAX_PARALLEL_FETCHES = 6;
|
||||||
@@ -499,6 +499,7 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl
|
|||||||
scene.add(sun, sun.target);
|
scene.add(sun, sun.target);
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 10000);
|
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 10000);
|
||||||
|
let framedNear = 0.1; // the near plane framing set
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
|
|
||||||
@@ -557,7 +558,7 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl
|
|||||||
const size = Math.max(box.getSize(new THREE.Vector3()).length(), minSize);
|
const size = Math.max(box.getSize(new THREE.Vector3()).length(), minSize);
|
||||||
controls.target.copy(center);
|
controls.target.copy(center);
|
||||||
camera.position.copy(center).add(new THREE.Vector3(size * 0.6, size * 0.75, size * 0.6));
|
camera.position.copy(center).add(new THREE.Vector3(size * 0.6, size * 0.75, size * 0.6));
|
||||||
camera.near = size / 500;
|
camera.near = framedNear = size / 500;
|
||||||
camera.far = size * 50;
|
camera.far = size * 50;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
@@ -750,6 +751,13 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl
|
|||||||
if (onTick) onTick(dt);
|
if (onTick) onTick(dt);
|
||||||
updateBubbles(now);
|
updateBubbles(now);
|
||||||
controls.update();
|
controls.update();
|
||||||
|
// The near plane follows how far out the camera is (depth precision for the scenery far away), but never
|
||||||
|
// past what framing the models set, so close-ups of small models keep working
|
||||||
|
const near = Math.min(nearPlaneFor(camera.position.distanceTo(controls.target)), framedNear * 20);
|
||||||
|
if (Math.abs(near - camera.near) / camera.near > 0.15) {
|
||||||
|
camera.near = near;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export function parseModel(buffer) {
|
|||||||
mesh.uvs = new Float32Array(buffer, offset, n * 2);
|
mesh.uvs = new Float32Array(buffer, offset, n * 2);
|
||||||
offset += n * 8;
|
offset += n * 8;
|
||||||
}
|
}
|
||||||
|
if (entry.uv2) {
|
||||||
|
mesh.uvs2 = new Float32Array(buffer, offset, n * 2);
|
||||||
|
offset += n * 8;
|
||||||
|
}
|
||||||
if (entry.colors) {
|
if (entry.colors) {
|
||||||
mesh.colors = new Uint8Array(buffer, offset, n * 4);
|
mesh.colors = new Uint8Array(buffer, offset, n * 4);
|
||||||
offset += n * 4;
|
offset += n * 4;
|
||||||
@@ -63,12 +67,13 @@ export function shaderOf(manifest, asset, mesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NifFile::eShaderLook bits
|
// NifFile::eShaderLook bits
|
||||||
export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATERIAL_COLOR: 8 };
|
export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATERIAL_COLOR: 8, TWO_LAYERS_BLENDED: 16, TWO_LAYERS_ADDED: 32 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {lit, texture,
|
* How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {lit, texture,
|
||||||
* vertexColors, material} — whether the scene's sun and ambient light it, its texture and vertex colors are used, and
|
* vertexColors, material, layers} — whether the scene's sun and ambient light it, its texture and vertex colors are
|
||||||
* whether its NiMaterialProperty colors are (only fixed function and the "Material" shaders use them). Null without
|
* used, whether its NiMaterialProperty colors are (only fixed function and the "Material" shaders use them), and how
|
||||||
|
* a two layer shader puts its dark texture with the base one ('blended', 'added' or null). Null without
|
||||||
* lighting in the manifest (older servers), for the viewer's own lights.
|
* lighting in the manifest (older servers), for the viewer's own lights.
|
||||||
*/
|
*/
|
||||||
export function gameLook(manifest, asset, mesh) {
|
export function gameLook(manifest, asset, mesh) {
|
||||||
@@ -81,7 +86,8 @@ export function gameLook(manifest, asset, mesh) {
|
|||||||
texture: !(bits & SHADER_LOOK.NO_TEXTURE),
|
texture: !(bits & SHADER_LOOK.NO_TEXTURE),
|
||||||
// Fixed function reads them as NiVertexColorProperty says; the shaders always do, unless they have none
|
// Fixed function reads them as NiVertexColorProperty says; the shaders always do, unless they have none
|
||||||
vertexColors: !!(mesh.colors && !(bits & SHADER_LOOK.NO_VERTEX_COLORS) && (!fixedFunction || mesh.vertexColors !== 0)),
|
vertexColors: !!(mesh.colors && !(bits & SHADER_LOOK.NO_VERTEX_COLORS) && (!fixedFunction || mesh.vertexColors !== 0)),
|
||||||
material: fixedFunction || !!(bits & SHADER_LOOK.MATERIAL_COLOR)
|
material: fixedFunction || !!(bits & SHADER_LOOK.MATERIAL_COLOR),
|
||||||
|
layers: bits & SHADER_LOOK.TWO_LAYERS_BLENDED ? 'blended' : bits & SHADER_LOOK.TWO_LAYERS_ADDED ? 'added' : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +101,7 @@ export function mergeMeshes(meshes) {
|
|||||||
for (const mesh of meshes) {
|
for (const mesh of meshes) {
|
||||||
if (!mesh.vertices || !mesh.indices.length) continue;
|
if (!mesh.vertices || !mesh.indices.length) continue;
|
||||||
const key = JSON.stringify([mesh.texture, mesh.diffuse, mesh.emissive, mesh.alpha, mesh.blend, mesh.test, mesh.doubleSided,
|
const key = JSON.stringify([mesh.texture, mesh.diffuse, mesh.emissive, mesh.alpha, mesh.blend, mesh.test, mesh.doubleSided,
|
||||||
mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors, mesh.shaderTag]);
|
mesh.vertexColors, mesh.clampU, mesh.clampV, !!mesh.normals, !!mesh.uvs, !!mesh.colors, mesh.shaderTag, mesh.darkTexture, !!mesh.uvs2]);
|
||||||
if (!groups.has(key)) groups.set(key, []);
|
if (!groups.has(key)) groups.set(key, []);
|
||||||
groups.get(key).push(mesh);
|
groups.get(key).push(mesh);
|
||||||
}
|
}
|
||||||
@@ -107,6 +113,7 @@ export function mergeMeshes(meshes) {
|
|||||||
const out = { ...first, vertices, positions: new Float32Array(vertices * 3) };
|
const out = { ...first, vertices, positions: new Float32Array(vertices * 3) };
|
||||||
if (first.normals) out.normals = new Int8Array(vertices * 3);
|
if (first.normals) out.normals = new Int8Array(vertices * 3);
|
||||||
if (first.uvs) out.uvs = new Float32Array(vertices * 2);
|
if (first.uvs) out.uvs = new Float32Array(vertices * 2);
|
||||||
|
if (first.uvs2) out.uvs2 = new Float32Array(vertices * 2);
|
||||||
if (first.colors) out.colors = new Uint8Array(vertices * 4);
|
if (first.colors) out.colors = new Uint8Array(vertices * 4);
|
||||||
out.indices = vertices > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
|
out.indices = vertices > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
|
||||||
let v = 0, i = 0;
|
let v = 0, i = 0;
|
||||||
@@ -114,6 +121,7 @@ export function mergeMeshes(meshes) {
|
|||||||
out.positions.set(m.positions, v * 3);
|
out.positions.set(m.positions, v * 3);
|
||||||
if (out.normals) out.normals.set(m.normals, v * 3);
|
if (out.normals) out.normals.set(m.normals, v * 3);
|
||||||
if (out.uvs) out.uvs.set(m.uvs, v * 2);
|
if (out.uvs) out.uvs.set(m.uvs, v * 2);
|
||||||
|
if (out.uvs2) out.uvs2.set(m.uvs2, v * 2);
|
||||||
if (out.colors) out.colors.set(m.colors, v * 4);
|
if (out.colors) out.colors.set(m.colors, v * 4);
|
||||||
for (let k = 0; k < m.indices.length; k++) out.indices[i + k] = m.indices[k] + v;
|
for (let k = 0; k < m.indices.length; k++) out.indices[i + k] = m.indices[k] + v;
|
||||||
v += m.vertices;
|
v += m.vertices;
|
||||||
@@ -382,3 +390,13 @@ export function loadedScenes(scenes, scene) {
|
|||||||
if (entry) for (const n of entry.neighbours) loaded.add(n);
|
if (entry) for (const n of entry.neighbours) loaded.add(n);
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The camera's near plane for a view `distance` from what it looks at. A depth buffer's precision goes with
|
||||||
|
* near / distance², so a fixed small near plane (0.5) makes coplanar pieces fight (ground overlays, floor rings, road
|
||||||
|
* pieces) once the camera is far out; the game's own camera stays close to the player. A four hundredth
|
||||||
|
* of the distance keeps close-ups working and far views steady.
|
||||||
|
*/
|
||||||
|
export function nearPlaneFor(distance) {
|
||||||
|
return Math.min(20, Math.max(0.5, distance / 400));
|
||||||
|
}
|
||||||
|
|||||||
@@ -165,6 +165,23 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
#ifdef USE_MAP
|
#ifdef USE_MAP
|
||||||
diffuseColor.rgb *= texture2D( map, vMapUv ).rgb;
|
diffuseColor.rgb *= texture2D( map, vMapUv ).rgb;
|
||||||
#endif
|
#endif
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Two layer shaders: the dark texture (its own UV set) under the base one by the vertex alpha, or the two added
|
||||||
|
const TWO_LAYERS_BLENDED_FRAGMENT = `
|
||||||
|
#ifdef USE_MAP
|
||||||
|
#ifdef USE_COLOR_ALPHA
|
||||||
|
float layerMix = vColor.a;
|
||||||
|
#else
|
||||||
|
float layerMix = 1.0;
|
||||||
|
#endif
|
||||||
|
diffuseColor.rgb *= mix( texture2D( darkMap, vUvDark ).rgb, texture2D( map, vMapUv ).rgb, layerMix );
|
||||||
|
#endif
|
||||||
|
`;
|
||||||
|
const TWO_LAYERS_ADDED_FRAGMENT = `
|
||||||
|
#ifdef USE_MAP
|
||||||
|
diffuseColor *= texture2D( map, vMapUv ) * layerWeights.x + texture2D( darkMap, vUvDark ) * layerWeights.y;
|
||||||
|
#endif
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The texture alpha mode's change to a fragment shader
|
// The texture alpha mode's change to a fragment shader
|
||||||
@@ -235,14 +252,27 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
* mapping, the zone's sun and ambient light per vertex when the shader is lit, the material's color only when the
|
* mapping, the zone's sun and ambient light per vertex when the shader is lit, the material's color only when the
|
||||||
* shader reads it.
|
* shader reads it.
|
||||||
*/
|
*/
|
||||||
function gameMaterial(options, mesh, alphaMode, look) {
|
function gameMaterial(options, mesh, alphaMode, look, darkMap = null) {
|
||||||
const material = new THREE.MeshBasicMaterial({
|
const material = new THREE.MeshBasicMaterial({
|
||||||
...options,
|
...options,
|
||||||
color: look.material ? options.color : new THREE.Color(1, 1, 1)
|
color: look.material ? options.color : new THREE.Color(1, 1, 1)
|
||||||
});
|
});
|
||||||
material.toneMapped = false;
|
material.toneMapped = false;
|
||||||
|
const layers = darkMap ? look.layers : null;
|
||||||
|
// TwoLayersAdded_PS: base * material diffuse red + dark * material diffuse green (their animations)
|
||||||
|
const weights = new THREE.Vector2(mesh.diffuse[0], mesh.diffuse[1]);
|
||||||
material.onBeforeCompile = (shader) => {
|
material.onBeforeCompile = (shader) => {
|
||||||
if (options.map) textureAlphaPatch(shader, alphaMode);
|
if (layers) {
|
||||||
|
shader.uniforms.darkMap = { value: darkMap };
|
||||||
|
shader.uniforms.layerWeights = { value: weights };
|
||||||
|
shader.vertexShader = 'attribute vec2 uvDark;\nvarying vec2 vUvDark;\n' +
|
||||||
|
shader.vertexShader.replace('#include <uv_vertex>', '#include <uv_vertex>\n\tvUvDark = uvDark;');
|
||||||
|
shader.fragmentShader = 'uniform sampler2D darkMap;\nuniform vec2 layerWeights;\nvarying vec2 vUvDark;\n' + shader.fragmentShader
|
||||||
|
.replace('#include <map_fragment>', layers === 'blended' ? TWO_LAYERS_BLENDED_FRAGMENT : TWO_LAYERS_ADDED_FRAGMENT)
|
||||||
|
.replace('#include <color_fragment>', layers === 'blended' ? '#ifdef USE_COLOR_ALPHA\n\tdiffuseColor.rgb *= vColor.rgb;\n#endif' : '#include <color_fragment>');
|
||||||
|
} else if (options.map) {
|
||||||
|
textureAlphaPatch(shader, alphaMode);
|
||||||
|
}
|
||||||
if (!look.lit) return;
|
if (!look.lit) return;
|
||||||
Object.assign(shader.uniforms, gameLights);
|
Object.assign(shader.uniforms, gameLights);
|
||||||
shader.vertexShader = 'uniform vec3 gameLightColor;\nuniform vec3 gameAmbient;\nuniform vec3 gameLightVec;\nvarying vec3 vGameLight;\n' +
|
shader.vertexShader = 'uniform vec3 gameLightColor;\nuniform vec3 gameAmbient;\nuniform vec3 gameLightVec;\nvarying vec3 vGameLight;\n' +
|
||||||
@@ -250,17 +280,19 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
shader.fragmentShader = 'varying vec3 vGameLight;\n' +
|
shader.fragmentShader = 'varying vec3 vGameLight;\n' +
|
||||||
shader.fragmentShader.replace('#include <aomap_fragment>', '#include <aomap_fragment>\n\treflectedLight.indirectDiffuse *= vGameLight;');
|
shader.fragmentShader.replace('#include <aomap_fragment>', '#include <aomap_fragment>\n\treflectedLight.indirectDiffuse *= vGameLight;');
|
||||||
};
|
};
|
||||||
material.customProgramCacheKey = () => 'game:' + (options.map ? alphaMode : '') + ':' + look.lit;
|
material.customProgramCacheKey = () => 'game:' + (options.map ? alphaMode : '') + ':' + look.lit + ':' + layers;
|
||||||
return material;
|
return material;
|
||||||
}
|
}
|
||||||
|
|
||||||
function materialOf(mesh, map, forSky, alphaMode = 'opacity', look = null) {
|
function materialOf(mesh, map, forSky, alphaMode = 'opacity', look = null, darkMap = null) {
|
||||||
// Nearly everything in the game's files has alpha blending switched on; it only shows where something is see-
|
// Nearly everything in the game's files has alpha blending switched on; it only shows where something is see-
|
||||||
// through: the material, a vertex or the texture (only when the object's shader uses the texture's alpha as
|
// through: the material, a vertex or the texture (only when the object's shader uses the texture's alpha as
|
||||||
// opacity). Blended meshes still write depth, as Gamebryo's default does.
|
// opacity). Blended meshes still write depth, as Gamebryo's default does.
|
||||||
const vertexColors = usesVertexColors(mesh, look);
|
const vertexColors = usesVertexColors(mesh, look);
|
||||||
let vertexAlpha = false;
|
let vertexAlpha = false;
|
||||||
if (vertexColors) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250;
|
// A two layer blend reads the vertex alpha as the mix of its textures, not as opacity
|
||||||
|
const layersBlended = !!(darkMap && look.layers === 'blended');
|
||||||
|
if (vertexColors && !layersBlended) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250;
|
||||||
const textureAlpha = alphaMode === 'opacity' && !!(map && map.userData.alpha);
|
const textureAlpha = alphaMode === 'opacity' && !!(map && map.userData.alpha);
|
||||||
// The game's shaders take alpha from the vertex colors and texture only; NiMaterialProperty's is for fixed function
|
// The game's shaders take alpha from the vertex colors and texture only; NiMaterialProperty's is for fixed function
|
||||||
const materialAlpha = look && !look.material ? 1 : mesh.alpha;
|
const materialAlpha = look && !look.material ? 1 : mesh.alpha;
|
||||||
@@ -275,7 +307,7 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
map: map || null
|
map: map || null
|
||||||
};
|
};
|
||||||
if (forSky) return useTextureAlpha(new THREE.MeshBasicMaterial({ ...options, depthWrite: false, fog: false }), alphaMode);
|
if (forSky) return useTextureAlpha(new THREE.MeshBasicMaterial({ ...options, depthWrite: false, fog: false }), alphaMode);
|
||||||
if (look) return gameMaterial(options, mesh, alphaMode, look);
|
if (look) return gameMaterial(options, mesh, alphaMode, look, darkMap);
|
||||||
const material = new THREE.MeshStandardMaterial({ ...options, roughness: 0.85, metalness: 0 });
|
const material = new THREE.MeshStandardMaterial({ ...options, roughness: 0.85, metalness: 0 });
|
||||||
material.emissive.setRGB(mesh.emissive[0], mesh.emissive[1], mesh.emissive[2], THREE.SRGBColorSpace);
|
material.emissive.setRGB(mesh.emissive[0], mesh.emissive[1], mesh.emissive[2], THREE.SRGBColorSpace);
|
||||||
return useTextureAlpha(material, alphaMode);
|
return useTextureAlpha(material, alphaMode);
|
||||||
@@ -290,18 +322,21 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
if (!mesh.vertices || !mesh.indices.length) continue;
|
if (!mesh.vertices || !mesh.indices.length) continue;
|
||||||
// The sky keeps its own unlit look
|
// The sky keeps its own unlit look
|
||||||
const look = forSky ? null : gameLook(manifest, asset, mesh);
|
const look = forSky ? null : gameLook(manifest, asset, mesh);
|
||||||
let map = null;
|
const textureOf = async (slot, clampU, clampV) => {
|
||||||
if (mesh.texture >= 0 && mesh.uv && (!look || look.texture)) {
|
const texture = await loadTexture(asset, slot, model.header.textures[slot], lod);
|
||||||
const texture = await loadTexture(asset, mesh.texture, model.header.textures[mesh.texture], lod);
|
if (!texture) return null;
|
||||||
if (texture) {
|
const map = texture.clone(); // shares the image; wrapping differs per mesh
|
||||||
map = texture.clone(); // shares the image; wrapping differs per mesh
|
map.wrapS = clampU ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
|
||||||
map.wrapS = mesh.clampU ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
|
map.wrapT = clampV ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
|
||||||
map.wrapT = mesh.clampV ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
|
map.needsUpdate = true;
|
||||||
map.needsUpdate = true;
|
return map;
|
||||||
}
|
};
|
||||||
}
|
const map = mesh.texture >= 0 && mesh.uv && (!look || look.texture) ? await textureOf(mesh.texture, mesh.clampU, mesh.clampV) : null;
|
||||||
|
// A two layer shader's second texture, on its own UV set
|
||||||
|
const darkMap = look && look.layers && map && mesh.darkTexture >= 0 && mesh.uvs2 ? await textureOf(mesh.darkTexture, false, false) : null;
|
||||||
const geometry = geometryOf(mesh, look);
|
const geometry = geometryOf(mesh, look);
|
||||||
parts.push({ geometry, material: materialOf(mesh, map, forSky, textureAlphaMode(manifest, asset, mesh), look) });
|
if (darkMap) geometry.setAttribute('uvDark', new THREE.BufferAttribute(mesh.uvs2, 2));
|
||||||
|
parts.push({ geometry, material: materialOf(mesh, map, forSky, textureAlphaMode(manifest, asset, mesh), look, darkMap) });
|
||||||
}
|
}
|
||||||
const min = model.header.min, max = model.header.max;
|
const min = model.header.min, max = model.header.max;
|
||||||
const radius = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2;
|
const radius = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2;
|
||||||
@@ -343,6 +378,7 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress
|
|||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (hidden && !part.ghost) part.ghost = new THREE.MeshBasicMaterial({ color: HIDDEN_COLOR, transparent: true, opacity: 0.35, depthWrite: false, side: THREE.DoubleSide });
|
if (hidden && !part.ghost) part.ghost = new THREE.MeshBasicMaterial({ color: HIDDEN_COLOR, transparent: true, opacity: 0.35, depthWrite: false, side: THREE.DoubleSide });
|
||||||
const mesh = new THREE.InstancedMesh(part.geometry, hidden ? part.ghost : part.material, cellInstances.length);
|
const mesh = new THREE.InstancedMesh(part.geometry, hidden ? part.ghost : part.material, cellInstances.length);
|
||||||
|
mesh.userData.asset = asset; // which of the manifest's models it is, for picking
|
||||||
cellInstances.forEach((instance, i) => {
|
cellInstances.forEach((instance, i) => {
|
||||||
position.set(instance.x, instance.y, instance.z);
|
position.set(instance.x, instance.y, instance.z);
|
||||||
rotation.set(instance.qx, instance.qy, instance.qz, instance.qw);
|
rotation.set(instance.qx, instance.qy, instance.qz, instance.qw);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|||||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||||
import { buildTerrainChunks, TERRAIN_LOOKS } from '/js/lddviewer.js';
|
import { buildTerrainChunks, TERRAIN_LOOKS } from '/js/lddviewer.js';
|
||||||
import { createScenery } from '/js/scenery.js';
|
import { createScenery } from '/js/scenery.js';
|
||||||
|
import { nearPlaneFor } from '/js/scenery-core.js';
|
||||||
import { Track, replayPosition, trailSegments, heatFrames, heatLevel, heatColor, formatSpan, coreBounds, isPlaceholderTerrain } from '/js/world3d-core.js';
|
import { Track, replayPosition, trailSegments, heatFrames, heatLevel, heatColor, formatSpan, coreBounds, isPlaceholderTerrain } from '/js/world3d-core.js';
|
||||||
|
|
||||||
const LIVE_DELAY = 1.2; // seconds live players are drawn behind the newest report
|
const LIVE_DELAY = 1.2; // seconds live players are drawn behind the newest report
|
||||||
@@ -76,6 +77,14 @@ const scenery = createScenery({
|
|||||||
onScenes: renderScenes
|
onScenes: renderScenes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The near plane follows how far out the camera is, so far views keep their depth precision (no fighting surfaces)
|
||||||
|
function fitNearPlane(cam, target) {
|
||||||
|
const near = nearPlaneFor(cam.position.distanceTo(target));
|
||||||
|
if (Math.abs(near - cam.near) / cam.near < 0.15) return;
|
||||||
|
cam.near = near;
|
||||||
|
cam.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- scenes: which of the zone's scenes the scenery shows (scenery.js setSceneMode) ----
|
// ---- scenes: which of the zone's scenes the scenery shows (scenery.js setSceneMode) ----
|
||||||
|
|
||||||
function renderScenes(info) {
|
function renderScenes(info) {
|
||||||
@@ -1112,6 +1121,7 @@ function animate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
controls.update();
|
controls.update();
|
||||||
|
fitNearPlane(camera, controls.target);
|
||||||
scenery.update(dt);
|
scenery.update(dt);
|
||||||
flairs.update(dt);
|
flairs.update(dt);
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
|
|||||||
@@ -213,6 +213,56 @@ TEST(NifFileTests, PassesPropertiesDownTheTree) {
|
|||||||
EXPECT_TRUE(m.doubleSided);
|
EXPECT_TRUE(m.doubleSided);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two layer shaders use NiTexturingProperty's dark texture too, each texture on the UV set its flags name
|
||||||
|
TEST(NifFileTests, ReadsTheDarkTextureAndEachTexturesUvSet) {
|
||||||
|
NifBuilder nif;
|
||||||
|
const auto snow = nif.String("snow.dds"), rock = nif.String("rock.dds");
|
||||||
|
const auto source = [&nif](int32_t name) {
|
||||||
|
return nif.Add("NiSourceTexture", Net().Put<uint8_t>(1).Put(name).Put<int32_t>(-1).Floats({ 0, 0, 0 }).Put<uint8_t>(1).Put<uint8_t>(1).Put<uint8_t>(0));
|
||||||
|
};
|
||||||
|
const auto base = source(snow), dark = source(rock);
|
||||||
|
// Base on UV set 1 with a texture transform (32 bytes to skip), dark on UV set 0
|
||||||
|
auto texturing = Net().Put<uint16_t>(0).Put<uint32_t>(9).Put<uint8_t>(1).Put(base).Put<uint16_t>(0x3201).Put<uint8_t>(1);
|
||||||
|
texturing.Raw(std::string(32, '\0'));
|
||||||
|
texturing.Put<uint8_t>(1).Put(dark).Put<uint16_t>(0x3200).Put<uint8_t>(0);
|
||||||
|
for (int slot = 2; slot < 9; slot++) texturing.Put<uint8_t>(0);
|
||||||
|
texturing.Put<uint32_t>(0);
|
||||||
|
const auto property = nif.Add("NiTexturingProperty", texturing);
|
||||||
|
// A triangle with two UV sets: set 0 all (0.25, 0.75), set 1 all (0.5, 0.5)
|
||||||
|
Bytes data;
|
||||||
|
data.Put<int32_t>(0).Put<uint16_t>(3).Put<uint8_t>(0).Put<uint8_t>(0).Put<uint8_t>(1);
|
||||||
|
for (int i = 0; i < 3; i++) data.Floats({ static_cast<float>(i == 1), static_cast<float>(i == 2), 0.0f });
|
||||||
|
data.Put<uint16_t>(2).Put<uint8_t>(0).Floats({ 0, 0, 0, 1 }).Put<uint8_t>(0);
|
||||||
|
for (int i = 0; i < 3; i++) data.Floats({ 0.25f, 0.75f });
|
||||||
|
for (int i = 0; i < 3; i++) data.Floats({ 0.5f, 0.5f });
|
||||||
|
data.Put<uint16_t>(0).Put<int32_t>(-1).Put<uint16_t>(1).Put<uint32_t>(3).Put<uint8_t>(1).Put<uint16_t>(0).Put<uint16_t>(1).Put<uint16_t>(2).Put<uint16_t>(0);
|
||||||
|
const auto root = nif.Add("NiNode", {});
|
||||||
|
const auto shape = nif.Add("NiTriShape", {});
|
||||||
|
const auto shapeData = nif.Add("NiTriShapeData", data);
|
||||||
|
nif.Set(root, Node(Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, {}), { shape }));
|
||||||
|
nif.Set(shape, Geometry(Av(0, { 0, 0, 0 }, IDENTITY, 1.0f, { property }), shapeData));
|
||||||
|
std::string error;
|
||||||
|
const auto model = NifFile::Parse(nif.Build({ root }), 0, error);
|
||||||
|
ASSERT_TRUE(model) << error;
|
||||||
|
ASSERT_EQ(model->meshes.size(), 1u);
|
||||||
|
const auto& mesh = model->meshes[0];
|
||||||
|
EXPECT_EQ(mesh.material.texture, "snow.dds");
|
||||||
|
EXPECT_EQ(mesh.material.darkTexture, "rock.dds");
|
||||||
|
ASSERT_EQ(mesh.uvs.size(), 6u);
|
||||||
|
EXPECT_FLOAT_EQ(mesh.uvs[0], 0.5f); // the base texture's set 1
|
||||||
|
ASSERT_EQ(mesh.uvs2.size(), 6u);
|
||||||
|
EXPECT_FLOAT_EQ(mesh.uvs2[1], 0.75f); // the dark texture's set 0
|
||||||
|
|
||||||
|
// The browser gets the dark texture and its UVs
|
||||||
|
const auto encoded = NifFile::Encode(*model, { "mesh/snow.dds" }, { "mesh/rock.dds" });
|
||||||
|
uint32_t length{};
|
||||||
|
std::memcpy(&length, encoded.data(), 4);
|
||||||
|
const auto header = nlohmann::json::parse(encoded.substr(4, length));
|
||||||
|
EXPECT_EQ(header["textures"], nlohmann::json::array({ "mesh/snow.dds", "mesh/rock.dds" }));
|
||||||
|
EXPECT_EQ(header["meshes"][0]["darkTexture"], 1);
|
||||||
|
EXPECT_EQ(header["meshes"][0]["uv2"], true);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NifFileTests, TurnsStripsIntoTriangles) {
|
TEST(NifFileTests, TurnsStripsIntoTriangles) {
|
||||||
NifBuilder nif;
|
NifBuilder nif;
|
||||||
const auto root = nif.Add("NiNode", {});
|
const auto root = nif.Add("NiNode", {});
|
||||||
@@ -434,6 +484,8 @@ TEST(WorldSceneTests, LightsAZoneAsMostOfItsObjectsAre) {
|
|||||||
|
|
||||||
TEST(NifFileTests, KnowsWhatEachShaderLeavesOut) {
|
TEST(NifFileTests, KnowsWhatEachShaderLeavesOut) {
|
||||||
EXPECT_EQ(NifFile::ShaderLookFor(38), 0); // Basic VC: lit, textured, vertex colors
|
EXPECT_EQ(NifFile::ShaderLookFor(38), 0); // Basic VC: lit, textured, vertex colors
|
||||||
|
EXPECT_EQ(NifFile::ShaderLookFor(94), 0); // "Basic" draws with vertex colors too (Nimbus Station's pines)
|
||||||
|
EXPECT_EQ(NifFile::ShaderLookFor(84), NifFile::UNLIT); // Opaque NL VC NoFog
|
||||||
EXPECT_EQ(NifFile::ShaderLookFor(NifFile::LEGO_SHADER), 0);
|
EXPECT_EQ(NifFile::ShaderLookFor(NifFile::LEGO_SHADER), 0);
|
||||||
EXPECT_EQ(NifFile::ShaderLookFor(-1), 0); // fixed function is lit by Gamebryo
|
EXPECT_EQ(NifFile::ShaderLookFor(-1), 0); // fixed function is lit by Gamebryo
|
||||||
EXPECT_EQ(NifFile::ShaderLookFor(33), NifFile::UNLIT | NifFile::NO_TEXTURE); // Basic NL VC NT
|
EXPECT_EQ(NifFile::ShaderLookFor(33), NifFile::UNLIT | NifFile::NO_TEXTURE); // Basic NL VC NT
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ same(S.textureAlphaMode(manifest, 1, { shaderTag: 1 }), 'decal', 'LEGO part text
|
|||||||
same(S.textureAlphaMode(manifest, 1, { shaderTag: 30 }), 'opacity', 'Basic VC part texture alpha');
|
same(S.textureAlphaMode(manifest, 1, { shaderTag: 30 }), 'opacity', 'Basic VC part texture alpha');
|
||||||
|
|
||||||
// Lit, textured, vertex colors, no material colors: Basic VC
|
// Lit, textured, vertex colors, no material colors: Basic VC
|
||||||
same(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), { lit: true, texture: true, vertexColors: true, material: false }, 'Basic VC');
|
same(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), { lit: true, texture: true, vertexColors: true, material: false, layers: null }, 'Basic VC');
|
||||||
// Vertex colors are read even when NiVertexColorProperty ignores them, but only if the mesh has some
|
// Vertex colors are read even when NiVertexColorProperty ignores them, but only if the mesh has some
|
||||||
same(S.gameLook(manifest, 0, { ...colored, vertexColors: 0 }).vertexColors, true, 'shader reads vertex colors');
|
same(S.gameLook(manifest, 0, { ...colored, vertexColors: 0 }).vertexColors, true, 'shader reads vertex colors');
|
||||||
same(S.gameLook(manifest, 0, {}).vertexColors, false, 'mesh without vertex colors');
|
same(S.gameLook(manifest, 0, {}).vertexColors, false, 'mesh without vertex colors');
|
||||||
same(S.gameLook(manifest, 3, colored), { lit: false, texture: false, vertexColors: true, material: false }, 'Basic NL VC NT');
|
same(S.gameLook(manifest, 3, colored), { lit: false, texture: false, vertexColors: true, material: false, layers: null }, 'Basic NL VC NT');
|
||||||
// Fixed function: NiVertexColorProperty and the material decide
|
// Fixed function: NiVertexColorProperty and the material decide
|
||||||
same(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), { lit: true, texture: true, vertexColors: false, material: true }, 'fixed function');
|
same(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), { lit: true, texture: true, vertexColors: false, material: true, layers: null }, 'fixed function');
|
||||||
// Without the zone's lighting the viewer lights scenery itself
|
// Without the zone's lighting the viewer lights scenery itself
|
||||||
same(S.gameLook({ ...manifest, lighting: null }, 0, colored), null, 'no lighting');
|
same(S.gameLook({ ...manifest, lighting: null }, 0, colored), null, 'no lighting');
|
||||||
|
|
||||||
@@ -57,6 +57,15 @@ same([...S.loadedScenes(scenes, 0)], [0], 'loaded in the global scene');
|
|||||||
// A run longer than the map stops at its end
|
// A run longer than the map stops at its end
|
||||||
same(S.decodeSceneMap({ chunks: [{ x: 0, z: 0, maxX: 1, maxZ: 1, size: 1, runs: runs([9, 4]) }] }).chunks[0].cells.length, 1, 'runs clipped');
|
same(S.decodeSceneMap({ chunks: [{ x: 0, z: 0, maxX: 1, maxZ: 1, size: 1, runs: runs([9, 4]) }] }).chunks[0].cells.length, 1, 'runs clipped');
|
||||||
|
|
||||||
|
// Two layer shaders
|
||||||
|
const layered = { ...manifest, shaders: [106, 107], shaderLooks: { 106: S.SHADER_LOOK.TWO_LAYERS_BLENDED, 107: S.SHADER_LOOK.TWO_LAYERS_ADDED } };
|
||||||
|
same(S.gameLook(layered, 0, colored).layers, 'blended', 'two layers blended');
|
||||||
|
same(S.gameLook(layered, 1, colored).layers, 'added', 'two layers added');
|
||||||
|
same(S.gameLook(manifest, 0, colored).layers, null, 'one layer');
|
||||||
|
|
||||||
|
// The near plane grows with the distance, within limits
|
||||||
|
same([S.nearPlaneFor(10), S.nearPlaneFor(2000), S.nearPlaneFor(100000)], [0.5, 5, 20], 'near plane');
|
||||||
|
|
||||||
if (failures) {
|
if (failures) {
|
||||||
console.error(`${failures} failed`);
|
console.error(`${failures} failed`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user