diff --git a/ZenRen/Shaders/mainPass.hlsl b/ZenRen/Shaders/mainPass.hlsl index e7bdc55..0086c47 100644 --- a/ZenRen/Shaders/mainPass.hlsl +++ b/ZenRen/Shaders/mainPass.hlsl @@ -37,7 +37,7 @@ struct VS_OUT float2 uvBaseColor : TEXCOORD0; float3 uvLightmap : TEXCOORD1; float4 color : COLOR; - float light : LIGHT_INTENSITY; + float3 light : LIGHT_INTENSITY; //float4 normal : NORMAL; float4 position : SV_POSITION; }; @@ -74,17 +74,17 @@ VS_OUT VS_Main(VS_IN input) lightReceivedRatio = (lightNormalDotProduct + lightRatioAt90) / (1 + lightRatioAt90); } - float ambientLight2 = 0.14f; // TODO why is ambientLight even customizable outside of shader? needs to be balanced with lightRatioAt90 anyway - lightReceivedRatio = (lightReceivedRatio + ambientLight2) / (1 + ambientLight2); + float ambientLight = 0.14f; // TODO why is ambientLight even customizable outside of shader? needs to be balanced with lightRatioAt90 anyway + lightReceivedRatio = (lightReceivedRatio + ambientLight) / (1 + ambientLight); //float lightReceivedRatio = max(0, lightNormalDotProduct + 0.3f) + ambientLight; float sunStrength = 1.7f; float staticStrength = 2.f; - float staticLightAverage = (input.colLight.r + input.colLight.g + input.colLight.b) / 3.0f; - output.light = ((lightReceivedRatio * sunStrength) * 0.5f) + ((staticLightAverage * staticStrength) * 0.5f); + //float staticLightAverage = (input.colLight.r + input.colLight.g + input.colLight.b) / 3.0f; + output.light = (((float3) lightReceivedRatio * sunStrength) * 0.5f) + ((input.colLight * staticStrength) * 0.5f); } else { - output.light = 1; + output.light = (float3) 1; } output.position = mul(viewPosition, projectionMatrix); @@ -104,15 +104,6 @@ VS_OUT VS_Main(VS_IN input) // Pixel Shader //-------------------------------------------------------------------------------------- -float CalcMipLevel(float2 texCoord) -{ - float2 dx = ddx(texCoord); - float2 dy = ddy(texCoord); - float delta_max_sqr = max(dot(dx, dx), dot(dy, dy)); - - return max(0.0, 0.5 * log2(delta_max_sqr)); -} - Texture2D baseColor : register(s0); Texture2DArray lightmaps : register(s1); SamplerState SampleType : register(s0); @@ -122,19 +113,41 @@ struct PS_IN float2 uvBaseColor : TEXCOORD0; float3 uvLightmap : TEXCOORD1; float4 color : COLOR; - float light : LIGHT_INTENSITY; + float3 light : LIGHT_INTENSITY; //int indexLightmap : INDEX_LIGHTMAP; //float4 normal : NORMAL; }; +float CalcMipLevel(float2 texCoord) +{ + float2 dx = ddx(texCoord); + float2 dy = ddy(texCoord); + float delta_max_sqr = max(dot(dx, dx), dot(dy, dy)); + + return max(0.0, 0.5 * log2(delta_max_sqr)); +} + +float4 SampleLightmap(float3 uvLightmap) +{ + // TODO + // - We should probably use a LOD/mipmap bias, not a fixed mimap level, does SampleLevel do that? + // - How does Gothic get the smoothed look? any mipmap multisampling trickery? + + //return lightmaps.Sample(SampleType, uvLightmap); + return lightmaps.SampleLevel(SampleType, uvLightmap, 0.5f); +} + float4 PS_Main(PS_IN input) : SV_TARGET { // light color - float4 lightColor = float4(float3(1, 1, 1) * input.light, 1); + float4 lightColor = float4(input.light, 1); // albedo color float4 albedoColor; if (!outputDirectEnabled || outputDirectType == FLAG_OUTPUT_DIRECT_DIFFUSE) { + if (input.uvLightmap.z >= 0) /* dynamic branch */ { + lightColor = SampleLightmap(input.uvLightmap); + } albedoColor = baseColor.Sample(SampleType, input.uvBaseColor); // Alpha Cutoff @@ -146,7 +159,7 @@ float4 PS_Main(PS_IN input) : SV_TARGET // since non-blended transparency fades out on higher mips more than it should (potentially leading to disappearing vegetation), correct it float texWidth; float texHeight; - baseColor.GetDimensions(texWidth, texHeight); + baseColor.GetDimensions(texWidth, texHeight);// TODO this info could be passed with cb float distAlphaMipScale = 0.25f; albedoColor.a *= 1 + CalcMipLevel(input.uvBaseColor * float2(texWidth, texHeight)) * distAlphaMipScale; } @@ -161,9 +174,9 @@ float4 PS_Main(PS_IN input) : SV_TARGET // if sharpening is used, the alphaCutoff its pretty much irrelevant here (we could just use 0.5) clip(albedoColor.a < alphaCutoff ? -1 : 1); } - else if (outputDirectType == FLAG_OUTPUT_DIRECT_LIGHTMAP) { + else if (outputDirectType == FLAG_OUTPUT_DIRECT_SOLID || outputDirectType == FLAG_OUTPUT_DIRECT_LIGHTMAP) { if (input.uvLightmap.z >= 0) /* dynamic branch */ { - albedoColor = lightmaps.Sample(SampleType, input.uvLightmap); + albedoColor = SampleLightmap(input.uvLightmap); } else { albedoColor = input.color; diff --git a/ZenRen/src/renderer/PipelineWorld.cpp b/ZenRen/src/renderer/PipelineWorld.cpp index 3bbffa8..b62a476 100644 --- a/ZenRen/src/renderer/PipelineWorld.cpp +++ b/ZenRen/src/renderer/PipelineWorld.cpp @@ -243,8 +243,7 @@ namespace renderer::world { Texture* texture = new Texture(d3d, lightmap.ddsRaw, false, "lightmap_xxx"); debugTextures.push_back(texture); } - // TODO it is totally unclear if these should be loaded as sRGB or linear! - lightmapTexArray = createShaderTexArray(d3d, ddsRaws, 256, 256, false); + lightmapTexArray = createShaderTexArray(d3d, ddsRaws, 256, 256, true); } world.meshes.clear(); diff --git a/ZenRen/src/renderer/Renderer.h b/ZenRen/src/renderer/Renderer.h index dda31e9..d87452d 100644 --- a/ZenRen/src/renderer/Renderer.h +++ b/ZenRen/src/renderer/Renderer.h @@ -49,6 +49,14 @@ namespace renderer D3DXCOLOR colLightStatic; }; + struct Light { + // TODO falloff and type surely play a role + VEC3 pos; + bool isStatic; + D3DXCOLOR color; + float range; + }; + struct InMemoryTexFile { int32_t width; int32_t height; diff --git a/ZenRen/src/renderer/RendererCommon.h b/ZenRen/src/renderer/RendererCommon.h index dce4a96..e8d8abe 100644 --- a/ZenRen/src/renderer/RendererCommon.h +++ b/ZenRen/src/renderer/RendererCommon.h @@ -121,8 +121,13 @@ namespace renderer const VEC_VERTEX_DATA& get(const std::unordered_map& meshData) const { return meshData.find(*this->mat)->second; } - const VERTEX_POS& getPos(const std::unordered_map& meshData) const { - return meshData.find(*this->mat)->second.vecPos[this->vertIndex]; + const std::array getPos(const std::unordered_map& meshData) const { + auto& vertData = meshData.find(*this->mat)->second.vecPos; + return { vertData[this->vertIndex], vertData[this->vertIndex + 1], vertData[this->vertIndex + 2] }; + } + const std::array getOther(const std::unordered_map& meshData) const { + auto& vertData = meshData.find(*this->mat)->second.vecNormalUv; + return { vertData[this->vertIndex], vertData[this->vertIndex + 1], vertData[this->vertIndex + 2] }; } }; diff --git a/ZenRen/src/renderer/Texture.cpp b/ZenRen/src/renderer/Texture.cpp index 9876835..97beec5 100644 --- a/ZenRen/src/renderer/Texture.cpp +++ b/ZenRen/src/renderer/Texture.cpp @@ -29,7 +29,7 @@ namespace renderer { const TexMetadata& metadata = image->GetMetadata(); if (metadata.width != width || metadata.height != height) { - // resize dicards any images after the first (resulting file will have no mipmaps)! + // resize discards any images after the first (resulting file will have no mipmaps)! ScratchImage imageResized; auto hr = Resize(*image->GetImage(0, 0, 0), width, height, DirectX::TEX_FILTER_DEFAULT, imageResized); throwOnError(hr, name); @@ -81,6 +81,11 @@ namespace renderer { } ID3D11ShaderResourceView* createShaderTexArray(D3d d3d, std::vector>& ddsRaws, int32_t width, int32_t height, bool sRgb, bool noMip) { + // SRGB: + // Lightmaps come in the format DXGI_FORMAT_B5G6R5_UNORM (85), which is a 16bpp format. + // There is not SRGB variant for this format, so if we use it as is, the CREATETEX_FORCE_SRGB flag will fail silently. + // To fix that, we convert all 16BPP formats to more standard formats on DDS conversion to make sRGB flag work in all cases. + std::vector imageOwners; std::vector images; @@ -90,27 +95,24 @@ namespace renderer { for (auto& ddsRaw : ddsRaws) { std::string name = "lightmap_" + std::format("{:03}", i); DirectX::ScratchImage* image = new DirectX::ScratchImage(); - auto hr = DirectX::LoadFromDDSMemory(ddsRaw.data(), ddsRaw.size(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, &metadata, *image); + auto hr = DirectX::LoadFromDDSMemory(ddsRaw.data(), ddsRaw.size(), DirectX::DDS_FLAGS::DDS_FLAGS_NO_16BPP, &metadata, *image); throwOnError(hr, name); resizeIfOtherSize(image, width, height, name); if (!noMip) { - // TODO it is unclear if Mimaps are needed since the lightmaps details are extremely low frequency and much likely will never noticably alias unless running at extremely low resolution createMipmapsIfMissing(image, name); + metadata = image->GetMetadata();// update metadata.mipLevels } imageOwners.push_back(image); - images.push_back(*image->GetImages()); + for (int m = 0; m < metadata.mipLevels; m++) { + images.push_back(*image->GetImage(m, 0, 0)); + } i++; } + metadata.arraySize = i; - metadata.arraySize = images.size(); - - // TODO - // It is unclear if MipMaps work here. - // We should instead write Image to DDS raw memory (see https://stackoverflow.com/questions/76144325/convert-any-input-dds-into-another-dds-format-in-directxtex) - // and then load raw memory with CreateDDSTextureFromMemory (see https://github.com/Microsoft/DirectXTK/wiki/DDSTextureLoader) - + // TODO Maybe we should just create a DX Texture object and do the SRV ourselves. ID3D11ShaderResourceView* resourceView; auto sRgbFlag = sRgb ? DirectX::CREATETEX_FORCE_SRGB : DirectX::CREATETEX_IGNORE_SRGB; auto hr = DirectX::CreateShaderResourceViewEx( @@ -127,6 +129,7 @@ namespace renderer { } void createSetSrv(D3d d3d, const ScratchImage& image, const std::string& name, bool sRgb, ID3D11ShaderResourceView** ppSrv) { + // TODO Maybe we should just create a DX Texture object and do the SRV ourselves. auto sRgbFlag = sRgb ? DirectX::CREATETEX_FORCE_SRGB : DirectX::CREATETEX_IGNORE_SRGB; auto hr = DirectX::CreateShaderResourceViewEx( d3d.device, image.GetImages(), image.GetImageCount(), image.GetMetadata(), diff --git a/ZenRen/src/renderer/loader/StaticLightFromGroundFace.cpp b/ZenRen/src/renderer/loader/StaticLightFromGroundFace.cpp index 25d62c1..1c6d44a 100644 --- a/ZenRen/src/renderer/loader/StaticLightFromGroundFace.cpp +++ b/ZenRen/src/renderer/loader/StaticLightFromGroundFace.cpp @@ -140,7 +140,7 @@ namespace renderer::loader return colorAverage; } - std::optional getLightStaticAtPos(const XMVECTOR pos, const unordered_map& meshData, const SpatialCache& spatialCache) + std::optional getGroundFaceAtPos(const XMVECTOR pos, const unordered_map& meshData, const VertLookupTree& vertLookup) { VEC3 pos3 = toVec3(pos); vector vertKeys; @@ -148,7 +148,7 @@ namespace renderer::loader vertKeys = rayDownIntersectedNaive(meshData, pos3, 100); } else { - vertKeys = rayDownIntersected(spatialCache, pos3, 100); + vertKeys = rayDownIntersected(vertLookup, pos3, 100); } vector belowVerts; for (auto& vertKey : vertKeys) { @@ -162,11 +162,7 @@ namespace renderer::loader return std::nullopt; } else { - D3DXCOLOR staticLight = interpolateColor(pos3, meshData, vertKeys[closestIndex]); - if (tintVobStaticLight) { - staticLight = D3DXCOLOR((staticLight.r / 3.f) * 2.f, staticLight.g, staticLight.b, staticLight.a); - } - return staticLight; + return vertKeys[closestIndex]; } } } \ No newline at end of file diff --git a/ZenRen/src/renderer/loader/StaticLightFromGroundFace.h b/ZenRen/src/renderer/loader/StaticLightFromGroundFace.h index 0fba832..a96d862 100644 --- a/ZenRen/src/renderer/loader/StaticLightFromGroundFace.h +++ b/ZenRen/src/renderer/loader/StaticLightFromGroundFace.h @@ -5,6 +5,7 @@ namespace renderer::loader { - std::optional getLightStaticAtPos(const DirectX::XMVECTOR pos, const std::unordered_map& meshData, const SpatialCache& spatialCache); + D3DXCOLOR interpolateColor(const VEC3& pos, const std::unordered_map& meshData, const VertKey& vertKey); + std::optional getGroundFaceAtPos(const DirectX::XMVECTOR pos, const std::unordered_map& meshData, const VertLookupTree& vertLookup); } diff --git a/ZenRen/src/renderer/loader/VertPosLookup.cpp b/ZenRen/src/renderer/loader/VertPosLookup.cpp index 4cddf79..2809743 100644 --- a/ZenRen/src/renderer/loader/VertPosLookup.cpp +++ b/ZenRen/src/renderer/loader/VertPosLookup.cpp @@ -23,14 +23,14 @@ namespace renderer::loader maxY = std::max(maxY, vert.y); maxZ = std::max(maxZ, vert.z); } - return OrthoTree::BoundingBoxND<3, float>{ {minX, minY, minZ}, {maxX, maxY, maxZ} }; + return OrthoTree::BoundingBoxND<3, float>{ {minX, minY, minZ}, {maxX, maxY, maxZ} }; } - SpatialCache createSpatialCache(const unordered_map& meshData) + VertLookupTree createVertLookup(const unordered_map& meshData) { vector bboxes; - SpatialCache result; + VertLookupTree result; uint32_t bbIndex = 0; for (auto& it : meshData) { auto& mat = it.first; @@ -49,7 +49,7 @@ namespace renderer::loader return result; } - vector rayDownIntersected(const SpatialCache& cache, const VEC3& pos, float searchSizeY) + vector rayDownIntersected(const VertLookupTree& lookup, const VEC3& pos, float searchSizeY) { auto searchBox = OrthoBoundingBox3D{ { pos.x - rayIntersectTolerance, pos.y - searchSizeY, pos.z - rayIntersectTolerance }, @@ -57,14 +57,14 @@ namespace renderer::loader }; constexpr bool shouldFullyContain = false; - auto intersectedBoxesSorted = cache.tree.RangeSearch(searchBox); + auto intersectedBoxesSorted = lookup.tree.RangeSearch(searchBox); // TODO while RayIntersectedAll should be equivalent from what i understand, it is missing most bboxes that RangeSearch finds //auto intersectedBoxesSorted = cache.tree.RayIntersectedAll({ pos.x, pos.y, pos.z }, { 0, -1, 0 }, rayIntersectTolerance, searchSizeY); vector result; for (auto id : intersectedBoxesSorted) { - const auto& vertKey = cache.bboxIndexToVert.find(id)->second; + const auto& vertKey = lookup.bboxIndexToVert.find(id)->second; result.push_back(vertKey); } return result; diff --git a/ZenRen/src/renderer/loader/VertPosLookup.h b/ZenRen/src/renderer/loader/VertPosLookup.h index 67359c9..1827b36 100644 --- a/ZenRen/src/renderer/loader/VertPosLookup.h +++ b/ZenRen/src/renderer/loader/VertPosLookup.h @@ -13,13 +13,13 @@ namespace renderer::loader // we don't have to provide them separately on each search by storing them in SpatialCache. using OrthoOctree = OrthoTree::TreeBoxContainerND<3, 2, float>; - struct SpatialCache { + struct VertLookupTree { std::unordered_map bboxIndexToVert; OrthoOctree tree; }; - SpatialCache createSpatialCache(const std::unordered_map& meshData); - std::vector rayDownIntersected(const SpatialCache& cache, const VEC3& pos, float searchSizeY); + VertLookupTree createVertLookup(const std::unordered_map& meshData); + std::vector rayDownIntersected(const VertLookupTree& lookup, const VEC3& pos, float searchSizeY); std::vector rayDownIntersectedNaive(const std::unordered_map& meshData, const VEC3& pos, float searchSizeY); } diff --git a/ZenRen/src/renderer/loader/ZenLoader.cpp b/ZenRen/src/renderer/loader/ZenLoader.cpp index cb2fcf5..3753564 100644 --- a/ZenRen/src/renderer/loader/ZenLoader.cpp +++ b/ZenRen/src/renderer/loader/ZenLoader.cpp @@ -34,6 +34,8 @@ namespace renderer::loader { bool debugInstanceMeshBbox = false; bool debugInstanceMeshBboxCenter = false; + bool debugTintVobStaticLight = false; + bool debugStaticLights = false; XMVECTOR bboxCenter(const array& bbox) { return 0.5f * (toXM4Pos(bbox[0]) + toXM4Pos(bbox[1])); @@ -85,16 +87,30 @@ namespace renderer::loader { return true; } - void flattenVobTree(vector& vobs, vector& target) + void flattenVobTree(const vector& vobs, vector& target, std::function filter) { - for (ZenLoad::zCVobData& vob : vobs) { - if (!vob.visual.empty() && vob.visual.find(".3DS") != string::npos) { + for (const ZenLoad::zCVobData& vob : vobs) { + if (filter(vob)) { target.push_back(&vob); } - flattenVobTree(vob.childVobs, target); + flattenVobTree(vob.childVobs, target, filter); } } + void getVobsWithVisuals(const vector& vobs, vector& target) + { + const auto filter = [&](const ZenLoad::zCVobData& vob) { return !vob.visual.empty() && vob.visual.find(".3DS") != string::npos; }; + return flattenVobTree(vobs, target, filter); + } + + void getVobStaticLights(const vector& vobs, vector& target) + { + const auto filter = [&](const ZenLoad::zCVobData& vob) { + return vob.vobType == ZenLoad::zCVobData::EVobType::VT_zCVobLight && vob.zCVobLight.lightStatic; + }; + return flattenVobTree(vobs, target, filter); + } + inline std::ostream& operator <<(std::ostream& os, const D3DXCOLOR& that) { return os << "[R:" << that.r << " G:" << that.g << " B:" << that.b << " A:" << that.a << "]"; @@ -108,17 +124,88 @@ namespace renderer::loader { }; } - vector loadVobs(vector& rootVobs, const unordered_map& worldMeshData) { + vector loadLights(vector& rootVobs) + { + vector lights; + vector vobs; + getVobStaticLights(rootVobs, vobs); + + for (auto vobPtr : vobs) { + ZenLoad::zCVobData vob = *vobPtr; + Light light = { + toVec3(toXM4Pos(vob.position) * 0.01f), + vob.zCVobLight.lightStatic, + D3DXCOLOR(vob.zCVobLight.color), + vob.zCVobLight.range * 0.01f, + }; + + lights.push_back(light); + } + return lights; + } + + struct LightLookupTree { + OrthoOctree tree; + }; + + LightLookupTree createLightLookup(const vector& lights) + { + vector bboxes; + for (auto& light : lights) { + auto pos = light.pos; + auto range = light.range; + bboxes.push_back({ + {pos.x - range, pos.y - range, pos.z - range}, + {pos.x + range, pos.y + range, pos.z + range} + }); + } + LightLookupTree result; + result.tree = OrthoOctree(bboxes + , 8 // max depth + , std::nullopt // user-provided bounding Box for all + , 10 // max element in a node; 10 works well for us and has been observed to work well in general according to lib author (Github) + ); + return result; + } + + D3DXCOLOR getLightAtPos(XMVECTOR posXm, const vector& lights, const LightLookupTree& lightLookup) { + auto pos = toVec3(posXm); + float rayIntersectTolerance = 0.1f; + auto searchBox = OrthoBoundingBox3D{ + { pos.x - rayIntersectTolerance, pos.y - rayIntersectTolerance, pos.z - rayIntersectTolerance }, + { pos.x + rayIntersectTolerance, pos.y + rayIntersectTolerance, pos.z + rayIntersectTolerance } + }; + + constexpr bool shouldFullyContain = false; + auto intersectedBoxes = lightLookup.tree.RangeSearch(searchBox); + + D3DXCOLOR color = D3DXCOLOR(0.f, 0.f, 0.f, 1.f); + + for (auto boxIndex : intersectedBoxes) { + auto& light = lights[boxIndex]; + XMVECTOR lightPos = toXM4Pos(light.pos); + float dist = XMVectorGetX(XMVector3Length(lightPos - posXm)); + float weight = 0; + if (dist < light.range) { + weight = 1.f - (dist / light.range);// TODO this assumes linear falloff for all light types, which is very likely wrong + color += (light.color * weight); + } + } + return color; + } + + vector loadVobs(vector& rootVobs, const unordered_map& worldMeshData, const vector& lightsStatic, const bool isOutdoorLevel) { vector statics; - vector vobs; - flattenVobTree(rootVobs, vobs); + vector vobs; + getVobsWithVisuals(rootVobs, vobs); int32_t resolvedStaticLight = 0; int32_t totalDurationMicros = 0; int32_t maxDurationMicros = 0; - const auto spatialCache = createSpatialCache(worldMeshData); + const auto worldFaceLookup = createVertLookup(worldMeshData); + const auto lightStaticLookup = createLightLookup(lightsStatic); for (auto vobPtr : vobs) { ZenLoad::zCVobData vob = *vobPtr; @@ -150,17 +237,43 @@ namespace renderer::loader { // TODO check if would should receive static light from groundPoly (static flag in vob?) // TODO is default static vob color? const auto now = std::chrono::high_resolution_clock::now(); - auto colLight = getLightStaticAtPos(bboxCenter(instance.bbox), worldMeshData, spatialCache); - if (colLight.has_value()) { - instance.colLightStatic = colLight.value(); + + // TODO if indoor or ground poly has lightmap, use lights, otherwise use ground face! + D3DXCOLOR colLight; + const XMVECTOR center = bboxCenter(instance.bbox); + const std::optional vertKey = getGroundFaceAtPos(center, worldMeshData, worldFaceLookup); + + bool hasLightmap = false; + if (!isOutdoorLevel) { + hasLightmap = true; + } + else if (vertKey.has_value()) { + const auto& other = vertKey.value().getOther(worldMeshData); + if (other[0].uvLightmap.i != -1) { + hasLightmap = true; + } + } + + if (hasLightmap) { + colLight = getLightAtPos(bboxCenter(instance.bbox), lightsStatic, lightStaticLookup); resolvedStaticLight++; } else { - // TODO set default light values - instance.colLightStatic = D3DXCOLOR(0.63f, 0.63f, 0.63f, 1);// fallback lightness of (160, 160, 160) + if (vertKey.has_value()) { + colLight = interpolateColor(toVec3(center), worldMeshData, vertKey.value()); + resolvedStaticLight++; + } + else { + colLight = D3DXCOLOR(0.63f, 0.63f, 0.63f, 1);// fallback lightness of (160, 160, 160) + } } - const auto duration = std::chrono::high_resolution_clock::now() - now; + if (debugTintVobStaticLight) { + colLight = D3DXCOLOR((colLight.r / 3.f) * 2.f, colLight.g, colLight.b, colLight.a); + } + instance.colLightStatic = colLight; + + const auto duration = std::chrono::high_resolution_clock::now() - now; const auto durationMicros = static_cast (duration / std::chrono::microseconds(1)); totalDurationMicros += durationMicros; maxDurationMicros = std::max(maxDurationMicros, durationMicros); @@ -204,7 +317,11 @@ namespace renderer::loader { unordered_map worldMeshData; loadWorldMesh(worldMeshData, parser.getWorldMesh()); - vector vobs = loadVobs(world.rootVobs, worldMeshData); + vector lightsStatic = loadLights(world.rootVobs); + + auto props = world.properties; + bool isOutdoorLevel = world.bspTree.mode == ZenLoad::zCBspTreeData::TreeMode::Outdoor; + vector vobs = loadVobs(world.rootVobs, worldMeshData, lightsStatic, isOutdoorLevel); LOG(INFO) << "Zen loaded!"; @@ -220,6 +337,12 @@ namespace renderer::loader { } } + if (debugStaticLights) { + for (auto& light : lightsStatic) { + loadPointDebugVisual(staticMeshData, light.pos); + } + } + LOG(INFO) << "Meshes loaded!"; return {