From 6f0fd4f9db7f2ea03fb3219af3013d09e7966d5a Mon Sep 17 00:00:00 2001 From: SirLynix Date: Wed, 1 Apr 2026 22:05:38 +0200 Subject: [PATCH 01/23] wip --- .gitignore | 1 + assets/shaders/BlockPBR.nzsl | 414 +++++++++--------- assets/shaders/PlanetAtmosphere.nzsl | 7 +- include/ClientLib/ClientAssetCookRegistry.hpp | 75 ++++ include/ClientLib/ClientAssetCookRegistry.inl | 7 + include/ClientLib/ClientAssetCooker.hpp | 42 ++ include/ClientLib/ClientAssetCooker.inl | 11 + .../ClientAssetLibraryAppComponent.hpp | 13 +- .../ClientAssetLibraryAppComponent.inl | 30 ++ include/ClientLib/ClientBlockLibrary.hpp | 16 +- include/ClientLib/ClientBlockLibrary.inl | 16 +- include/ClientLib/ClientChunkEntities.hpp | 3 +- include/ClientLib/ClientFramePipeline.hpp | 37 ++ include/ClientLib/ClientFramePipeline.inl | 12 + .../Components/VisualEntityComponent.inl | 2 +- include/CommonLib/BlockLibrary.hpp | 18 +- include/CommonLib/BlockLibrary.inl | 5 + include/CommonLib/Chunk.hpp | 8 +- include/CommonLib/DeformedChunk.hpp | 49 --- include/CommonLib/DeformedChunk.inl | 55 --- include/CommonLib/NetworkReactor.hpp | 2 +- include/CommonLib/Planet.hpp | 13 +- .../Scripting/MathScriptingLibrary.hpp | 1 + .../CommonLib/Scripting/ScriptingUtils.inl | 109 +++++ .../Utility/SignedDistanceFunctions.hpp | 3 + .../Utility/SignedDistanceFunctions.inl | 6 + scripts/planets/alice.lua | 119 +++-- scripts/planets/bob.lua | 101 ++++- scripts/planets/bob_pure.lua | 134 ++++++ scripts/planets/test.lua | 66 +++ scripts/planets/torus.lua | 108 +++++ src/ClientLib/BlockSelectionBar.cpp | 8 +- src/ClientLib/ClientAssetCookRegistry.cpp | 121 +++++ src/ClientLib/ClientAssetCooker.cpp | 275 ++++++++++++ src/ClientLib/ClientBlockLibrary.cpp | 217 +++++++-- src/ClientLib/ClientChunkEntities.cpp | 190 ++++---- src/ClientLib/ClientFramePipeline.cpp | 13 + src/ClientLib/ClientSessionHandler.cpp | 14 +- .../AtmosphereScatteringPipelinePass.cpp | 2 +- src/CommonLib/BlockLibrary.cpp | 104 ++--- src/CommonLib/Chunk.cpp | 87 +--- src/CommonLib/DeformedChunk.cpp | 107 ----- src/CommonLib/NetworkReactor.cpp | 4 +- src/CommonLib/Planet.cpp | 173 +++++++- .../Scripting/ChunkScriptingLibrary.cpp | 7 +- .../Scripting/MathScriptingLibrary.cpp | 31 +- src/CommonLib/Scripting/ScriptingContext.cpp | 12 +- src/CommonLib/Scripting/ScriptingUtils.cpp | 6 +- src/CommonLib/SurfaceNetsChunk.cpp | 26 +- src/Game/GameAppComponent.cpp | 64 ++- src/Game/GameConfigAppComponent.cpp | 2 +- src/Game/States/GameState.cpp | 2 +- src/Game/States/GameState.hpp | 1 - src/Game/States/PlanetEditorState.hpp | 4 +- src/Server/main.cpp | 2 +- .../Systems/NetworkedEntitiesSystem.cpp | 2 - xmake.lua | 5 +- 57 files changed, 2113 insertions(+), 849 deletions(-) create mode 100644 include/ClientLib/ClientAssetCookRegistry.hpp create mode 100644 include/ClientLib/ClientAssetCookRegistry.inl create mode 100644 include/ClientLib/ClientAssetCooker.hpp create mode 100644 include/ClientLib/ClientAssetCooker.inl create mode 100644 include/ClientLib/ClientFramePipeline.hpp create mode 100644 include/ClientLib/ClientFramePipeline.inl delete mode 100644 include/CommonLib/DeformedChunk.hpp delete mode 100644 include/CommonLib/DeformedChunk.inl create mode 100644 scripts/planets/bob_pure.lua create mode 100644 scripts/planets/test.lua create mode 100644 scripts/planets/torus.lua create mode 100644 src/ClientLib/ClientAssetCookRegistry.cpp create mode 100644 src/ClientLib/ClientAssetCooker.cpp create mode 100644 src/ClientLib/ClientFramePipeline.cpp delete mode 100644 src/CommonLib/DeformedChunk.cpp diff --git a/.gitignore b/.gitignore index b2e929e2..c9e34884 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ assets/* !assets/*.passlist !assets/shaders +cache/ saves/ gameconfig.lua serverconfig.lua diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 692e4fd7..21557aca 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -15,46 +15,28 @@ option DistanceDepth: bool = false; option ShadowPass: bool = false; // Basic material options -option HasBaseColorTexture: bool = false; -option HasAlphaTexture: bool = false; option AlphaTest: bool = false; -// Physically-based material options -option HasDetailTexture: bool = false; -option HasEmissiveTexture: bool = false; -option HasHeightTexture: bool = false; -option HasMetallicTexture: bool = false; -option HasNormalTexture: bool = false; -option HasRoughnessTexture: bool = false; -option HasSpecularTexture: bool = false; - -const InvalidLoc = u32.Max; - -// Billboard related options -option Billboard: bool = false; -option BillboardCenterLocation: u32 = InvalidLoc; -option BillboardColorLocation: u32 = InvalidLoc; -option BillboardSizeRotLocation: u32 = InvalidLoc; - -// Vertex declaration related options -option VertexColorLoc: u32 = InvalidLoc; -option VertexNormalLoc: u32 = InvalidLoc; -option VertexPositionLoc: u32 = InvalidLoc; -option VertexTangentLoc: u32 = InvalidLoc; -option VertexUvLoc: u32 = InvalidLoc; - -option VertexJointIndicesLoc: u32 = InvalidLoc; -option VertexJointWeightsLoc: u32 = InvalidLoc; - +// Physical option MaxLightCount: u32 = 3; -const HasNormal = (VertexNormalLoc != InvalidLoc); -const HasVertexColor = (VertexColorLoc != InvalidLoc); -const HasColor = (HasVertexColor || Billboard); -const HasTangent = (VertexTangentLoc != InvalidLoc && false); -const HasUV = (VertexUvLoc != InvalidLoc); -const HasNormalMapping = HasNormalTexture && HasNormal && HasTangent && !DepthPass; -const HasSkinning = (VertexJointIndicesLoc != InvalidLoc && VertexJointWeightsLoc != InvalidLoc); +[layout(std430)] +struct BlockData +{ + baseColorFallback: vec4[f32], + baseColorMapIndices: vec2[i32], + normalMapIndices: vec2[i32], + ambientOcclusionHeightMapIndices: vec2[i32], + roughnessMetalnessMapIndices: vec2[i32], + metalness: f32, + roughness: f32 +} + +[layout(std430)] +struct GlobalBlockData +{ + blocks: dyn_array[BlockData] +} [layout(std140)] struct MaterialSettings @@ -71,6 +53,9 @@ struct MaterialSettings [tag("BaseColor")] BaseColor: vec4[f32], + + [tag("TriplanarOffset")] + TriplanarOffset: vec3[f32] } // TODO: Add enums @@ -80,18 +65,16 @@ const SpotLight = 2; [tag("Material")] [auto_binding] -external +external MaterialData { [tag("Settings")] settings: uniform[MaterialSettings], - [tag("BaseColorMap")] MaterialBaseColorMap: sampler2D_array[f32], - [tag("AlphaMap")] MaterialAlphaMap: sampler2D_array[f32], - [tag("DetailMap")] MaterialDetailMap: sampler2D_array[f32], - [tag("EmissiveMap")] MaterialEmissiveMap: sampler2D_array[f32], - [tag("HeightMap")] MaterialHeightMap: sampler2D_array[f32], - [tag("MetallicMap")] MaterialMetallicMap: sampler2D_array[f32], - [tag("NormalMap")] MaterialNormalMap: sampler2D_array[f32], - [tag("RoughnessMap")] MaterialRoughnessMap: sampler2D_array[f32], - [tag("SpecularMap")] MaterialSpecularMap: sampler2D_array[f32], + [tag("GlobalBlockData")] globalBlockData: storage[GlobalBlockData], + + // TODO: Turn them into an array of texture + [tag("BlockTexture1")] blockTexture1: sampler2D_array[f32], + [tag("BlockTexture2")] blockTexture2: sampler2D_array[f32], + [tag("BlockTexture3")] blockTexture3: sampler2D_array[f32], + [tag("BlockTexture4")] blockTexture4: sampler2D_array[f32] } [tag("Engine")] @@ -112,10 +95,9 @@ external struct VertOut { [location(0)] worldPos: vec3[f32], - [location(1), cond(HasUV)] uv: vec3[f32], - [location(2), cond(HasColor)] color: vec4[f32], - [location(3), cond(HasNormal)] normal: vec3[f32], - [location(4), cond(HasNormalMapping)] tangent: vec3[f32], + [location(1)] triplanarPos: vec3[f32], + [location(2), interp(flat)] blockIndex: u32, + [location(3)] normal: vec3[f32], [builtin(position)] position: vec4[f32], } @@ -129,91 +111,198 @@ struct FragOut [builtin(frag_depth), cond(DistanceDepth)] fragdepth: f32, } -[export] -fn ComputeColor(input: VertOut) -> vec4[f32] +fn SampleBlock(uv: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] { - let color = settings.BaseColor; + // FIXME: Replace int to float conversion by bitcast for performance reasons + if (texIndices.x == 0) + return MaterialData.blockTexture1.Sample(vec3[f32](uv, f32(texIndices.y))); + else if (texIndices.x == 1) + return MaterialData.blockTexture2.Sample(vec3[f32](uv, f32(texIndices.y))); + else if (texIndices.x == 2) + return MaterialData.blockTexture3.Sample(vec3[f32](uv, f32(texIndices.y))); + else + return MaterialData.blockTexture4.Sample(vec3[f32](uv, f32(texIndices.y))); +} - const if (HasUV) - color.a *= TextureOverlay.Sample(input.uv.xy).r; +fn TriplanarSample(normal: vec3[f32], uvX: vec2[f32], uvY: vec2[f32], uvZ: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] +{ + let x = SampleBlock(uvX, texIndices); + let y = SampleBlock(uvY, texIndices); + let z = SampleBlock(uvZ, texIndices); - const if (HasColor) - color *= input.color; + let m = pow(abs(normal), (8.0).xxx); + let textureColor = (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z); + return textureColor; +} - const if (HasBaseColorTexture) - { - // Triplanar mapping - let x = MaterialBaseColorMap.Sample(vec3[f32](input.worldPos.yz, input.uv.z)); - let y = MaterialBaseColorMap.Sample(vec3[f32](input.worldPos.zx, input.uv.z)); - let z = MaterialBaseColorMap.Sample(vec3[f32](input.worldPos.xy, input.uv.z)); +fn ParallaxMapping(texCoords: vec2[f32], heightMapIndices: vec2[i32], viewDir: vec3[f32]) -> vec2[f32] +{ + let height = SampleBlock(texCoords, heightMapIndices).y; + return texCoords - viewDir.xy / viewDir.z * (height * 2.0); + + // number of depth layers + /*const minLayers = 8.0; + const maxLayers = 32.0; + const heightScale = 8.0; + + let numLayers = lerp(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir))); + // calculate the size of each layer + let layerDepth = 1.0 / numLayers; + // depth of current layer + let currentLayerDepth = 0.0; + // the amount to shift the texture coordinates per layer (from vector P) + let P = viewDir.xy / viewDir.z * heightScale; + let deltaTexCoords = P / numLayers; + + // get initial values + let currentTexCoords = texCoords; + let currentDepthMapValue = MaterialData.blockTexture.Sample(vec3[f32](currentTexCoords, heightMapIndex)).r; + + while(currentLayerDepth < currentDepthMapValue) + { + // shift texture coordinates along direction of P + currentTexCoords -= deltaTexCoords; + // get depthmap value at current texture coordinates + currentDepthMapValue = MaterialData.blockTexture.Sample(vec3[f32](currentTexCoords, heightMapIndex)).r; + // get depth of next layer + currentLayerDepth += layerDepth; + } + + // get texture coordinates before collision (reverse operations) + let prevTexCoords = currentTexCoords + deltaTexCoords; + + // get depth after and before collision for linear interpolation + let afterDepth = currentDepthMapValue - currentLayerDepth; + let beforeDepth = MaterialData.blockTexture.Sample(vec3[f32](prevTexCoords, heightMapIndex)).r - currentLayerDepth + layerDepth; + + // interpolation of texture coordinates + let weight = afterDepth / (afterDepth - beforeDepth); + let finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight); + + return finalTexCoords;*/ +} - let m = pow(abs(input.normal), (4.0).xxx); - let textureColor = (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z); +[export] +fn ComputeColor(input: VertOut) -> vec4[f32] +{ + let color = MaterialData.settings.BaseColor; - color *= textureColor; - } + let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - const if (HasAlphaTexture) + if (blockData.baseColorMapIndices.x >= 0) { - // Triplanar mapping - let x = MaterialAlphaMap.Sample(vec3[f32](input.worldPos.yz, input.uv.z)); - let y = MaterialAlphaMap.Sample(vec3[f32](input.worldPos.zx, input.uv.z)); - let z = MaterialAlphaMap.Sample(vec3[f32](input.worldPos.xy, input.uv.z)); - - let m = pow(abs(input.normal), (4.0).xxx); - let textureColor = (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z); + let uvX = input.triplanarPos.zy; + let uvY = input.triplanarPos.xz; + let uvZ = input.triplanarPos.xy; - color.w *= textureColor.x; + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); } const if (AlphaTest) { - if (color.w < settings.AlphaThreshold) + if (color.w < MaterialData.settings.AlphaThreshold) discard; } return color; } +fn UnpackNormal(texData: vec2[f32]) -> vec3[f32] +{ + let normal = vec3[f32](texData.x * 2.0 - 1.0, texData.y * 2.0 - 1.0, 0.0); + normal.z = sqrt(1.0 - normal.x - normal.y); + return normal; + //return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; +} + [export] -fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] +fn ComputeLighting(input: VertOut) -> vec4[f32] { + let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; + let lightRadiance = vec3[f32](0.0, 0.0, 0.0); let eyeVec = normalize(viewerData.eyePosition - input.worldPos); - let uv = input.uv; + let uvX = input.triplanarPos.zy; + let uvY = input.triplanarPos.xz; + let uvZ = input.triplanarPos.xy; - let normal: vec3[f32]; - const if (HasNormalMapping) + let tnormalX = UnpackNormal(SampleBlock(uvX, blockData.normalMapIndices).xy); + let tnormalY = UnpackNormal(SampleBlock(uvY, blockData.normalMapIndices).xy); + let tnormalZ = UnpackNormal(SampleBlock(uvZ, blockData.normalMapIndices).xy); + + let axisSign = sign(input.normal); + + // Construct tangent to world matrices for each axis + let tangentX = normalize(cross(input.normal, vec3(0.0, axisSign.x, 0.0))); + let bitangentX = normalize(cross(tangentX, input.normal)) * axisSign.x; + let tbnX = mat3(tangentX, bitangentX, input.normal); + + let tangentY = normalize(cross(input.normal, vec3(0.0, 0.0, axisSign.y))); + let bitangentY = normalize(cross(tangentY, input.normal)) * axisSign.y; + let tbnY = mat3(tangentY, bitangentY, input.normal); + + let tangentZ = normalize(cross(input.normal, vec3(0.0, -axisSign.z, 0.0))); + let bitangentZ = normalize(cross(tangentZ, input.normal)) * axisSign.z; + let tbnZ = mat3(tangentZ, bitangentZ, input.normal); + + /*if (blockData.heightMapIndex >= 0.0) + { + let viewDirWS = normalize(viewerData.eyePosition - input.worldPos); + + uvX = ParallaxMapping(uvX, blockData.heightMapIndex, tbnX * viewDirWS); + uvY = ParallaxMapping(uvY, blockData.heightMapIndex, tbnY * viewDirWS); + uvZ = ParallaxMapping(uvZ, blockData.heightMapIndex, tbnZ * viewDirWS); + }*/ + + let color = blockData.baseColorFallback; + if (blockData.baseColorMapIndices.x >= 0) + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + + const if (AlphaTest) { - let N = normalize(input.normal); - let T = normalize(input.tangent); - let B = cross(N, T); - let tbnMatrix = mat3[f32](T, B, N); + if (color.w < MaterialData.settings.AlphaThreshold) + discard; + } - normal = (MaterialNormalMap.Sample(uv).xyz * 2.0 - (1.0).rrr); - normal = normalize(tbnMatrix * normal); + let normal: vec3[f32]; + if (blockData.normalMapIndices.x >= 0) + { + // https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a + let blend = pow(abs(input.normal), (16.0).rrr); + blend /= dot(blend, (1.0).rrr); + + // Apply tangent to world matrix and triblend + // Using clamp() because the cross products may be NANs + normal = normalize( + clamp(tbnX * tnormalX, (-1.0).rrr, (1.0).rrr) * blend.x + + clamp(tbnY * tnormalY, (-1.0).rrr, (1.0).rrr) * blend.y + + clamp(tbnZ * tnormalZ, (-1.0).rrr, (1.0).rrr) * blend.z + ); } else normal = normalize(input.normal); let albedo = color.xyz; - let metallic = 0.0; - let roughness = 1.0; - const if (HasDetailTexture) - { - let detail = MaterialDetailMap.Sample(uv).rgb; - roughness = 1.0 - detail.r; - metallic = detail.y; - } + let metallic: f32; + /*if (blockData.metalnessMapIndex >= 0.0) + metallic = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.metalnessMapIndex).x; + else*/ + metallic = blockData.metalness; - const if (HasMetallicTexture) - metallic = MaterialMetallicMap.Sample(uv).x; + let roughness: f32; + /*if (blockData.roughnessMapIndex >= 0.0) + roughness = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMapIndex).x; + else*/ + roughness = blockData.roughness; - const if (HasRoughnessTexture) - roughness = MaterialRoughnessMap.Sample(uv).x; + let ao: f32; + /*if (blockData.ambientOcclusionMapIndex >= 0.0) + ao = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionMapIndex).x; + else*/ + ao = 1.0; let F0 = vec3[f32](0.04, 0.04, 0.04); F0 = albedo * metallic + F0 * (1.0 - metallic); @@ -270,21 +359,18 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] lightRadiance += shadowFactor * radiance; } - let ambient = (0.05).rrr * albedo; + let ambient = (0.05).rrr * albedo * ao; let finalColor = ambient + lightRadiance; finalColor = finalColor / (finalColor + vec3[f32](1.0, 1.0, 1.0)); - return finalColor; + return vec4[f32](finalColor, color.a); } [export, entry(frag), cond(!DepthPass)] fn FragMain(input: VertOut) -> FragOut { - let color = ComputeColor(input); - - const if (HasNormal) - color.rgb = ComputeLighting(color.rgb, input); + let color = ComputeLighting(input); let output: FragOut; output.RenderTarget0 = color; @@ -323,128 +409,38 @@ fn dummy() {} //< dummy // Vertex stage struct VertIn { - [location(VertexPositionLoc)] + [location(0)] pos: vec3[f32], - [cond(HasVertexColor), location(VertexColorLoc)] - color: vec4[f32], - - [cond(HasUV), location(VertexUvLoc)] - uv: vec3[f32], - - [cond(HasNormal), location(VertexNormalLoc)] + [location(1)] normal: vec3[f32], - [cond(HasTangent), location(VertexTangentLoc)] - tangent: vec3[f32], - - [cond(HasSkinning), location(VertexJointIndicesLoc)] - jointIndices: vec4[i32], - - [cond(HasSkinning), location(VertexJointWeightsLoc)] - jointWeights: vec4[f32], - - [cond(Billboard), location(BillboardCenterLocation)] - billboardCenter: vec3[f32], - - [cond(Billboard), location(BillboardSizeRotLocation)] - billboardSizeRot: vec4[f32], //< width,height,sin,cos - - [cond(Billboard), location(BillboardColorLocation)] - billboardColor: vec4[f32] + [location(2)] + blockIndex: u32, } -[entry(vert), cond(Billboard)] -fn VertBillboard(input: VertIn) -> VertOut -{ - let size = input.billboardSizeRot.xy; - let sinCos = input.billboardSizeRot.zw; - - let rotatedPosition = vec2[f32]( - input.pos.x * sinCos.y - input.pos.y * sinCos.x, - input.pos.y * sinCos.y + input.pos.x * sinCos.x - ); - rotatedPosition *= size; - - let cameraRight = vec3[f32](viewerData.viewMatrix[0][0], viewerData.viewMatrix[1][0], viewerData.viewMatrix[2][0]); - let cameraUp = vec3[f32](viewerData.viewMatrix[0][1], viewerData.viewMatrix[1][1], viewerData.viewMatrix[2][1]); - - let vertexPos = input.billboardCenter; - vertexPos += cameraRight * rotatedPosition.x; - vertexPos += cameraUp * rotatedPosition.y; - - let output: VertOut; - output.position = viewerData.viewProjMatrix * instanceData.worldMatrix * vec4[f32](vertexPos, 1.0); - - const if (HasColor) - output.color = input.billboardColor; - - const if (HasUV) - output.uv = input.pos.xy + vec2[f32](0.5, 0.5); - - return output; -} - -[entry(vert), cond(!Billboard)] +[entry(vert)] fn VertMain(input: VertIn) -> VertOut { - let pos: vec3[f32]; - const if (HasNormal) let normal: vec3[f32]; - - const if (HasSkinning) - { - let jointMatrices = array[mat4[f32]]( - skeletalData.jointMatrices[input.jointIndices[0]], - skeletalData.jointMatrices[input.jointIndices[1]], - skeletalData.jointMatrices[input.jointIndices[2]], - skeletalData.jointMatrices[input.jointIndices[3]] - ); - - const if (HasNormal) - { - let skinningOutput = SkinLinearPositionNormal(jointMatrices, input.jointWeights, input.pos, input.normal); - pos = skinningOutput.position; - normal = skinningOutput.normal; - } - else - { - let skinningOutput = SkinLinearPosition(jointMatrices, input.jointWeights, input.pos); - pos = skinningOutput.position; - } - } - else - { - pos = input.pos; - const if (HasNormal) - normal = input.normal; - } + let pos = input.pos; + let normal = input.normal; const if (ShadowPass) { - pos *= settings.ShadowPosScale; - const if (HasNormal) - pos -= input.normal * settings.ShadowMapNormalOffset; + pos *= MaterialData.settings.ShadowPosScale; + pos -= input.normal * MaterialData.settings.ShadowMapNormalOffset; } let worldPosition = instanceData.worldMatrix * vec4[f32](pos, 1.0); + let rotationMatrix = transpose(inverse(mat3[f32](instanceData.worldMatrix))); + let output: VertOut; output.worldPos = worldPosition.xyz; output.position = viewerData.viewProjMatrix * worldPosition; - - let rotationMatrix = transpose(inverse(mat3[f32](instanceData.worldMatrix))); - - const if (HasColor) - output.color = input.color; - - const if (HasNormal) - output.normal = rotationMatrix * input.normal; - - const if (HasUV) - output.uv = input.uv; - - const if (HasNormalMapping) - output.tangent = rotationMatrix * input.tangent; + output.triplanarPos = MaterialData.settings.TriplanarOffset + input.pos; + output.normal = rotationMatrix * input.normal; + output.blockIndex = input.blockIndex; return output; } diff --git a/assets/shaders/PlanetAtmosphere.nzsl b/assets/shaders/PlanetAtmosphere.nzsl index 19d7bdd4..03b06ce6 100644 --- a/assets/shaders/PlanetAtmosphere.nzsl +++ b/assets/shaders/PlanetAtmosphere.nzsl @@ -77,9 +77,6 @@ fn main(input: VertOut) -> FragOut let cameraPos = passData.viewerPosition; - let planetDims = (80.0).xxx; - let planetCornerRadius = 16.0; - let color = colorTexture.Sample(input.uv).rgb; let depth = depthTexture.Sample(input.uv).x; @@ -153,9 +150,11 @@ fn calculate_scattering( absorption_falloff: f32, // how fast the absorption falls off from the absorption height steps_i: i32, // the amount of steps along the 'primary' ray, more looks better but slower steps_l: i32 // the amount of steps along the light ray, more looks better but slower -) -> vec3[f32] { +) -> vec3[f32] +{ // add an offset to the camera position, so that the atmosphere is in the correct position start -= planet_position; + // calculate the start and end position of the ray, as a distance along the ray // we do this with an AABB intersect let ray_length = aabbIntersect(start, dir, planet_dims + atmosphereMaxHeight.xxx); diff --git a/include/ClientLib/ClientAssetCookRegistry.hpp b/include/ClientLib/ClientAssetCookRegistry.hpp new file mode 100644 index 00000000..31ec4711 --- /dev/null +++ b/include/ClientLib/ClientAssetCookRegistry.hpp @@ -0,0 +1,75 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP +#define TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + class TSOM_CLIENTLIB_API ClientAssetCookRegistry + { + public: + struct BlockEntry; + + ClientAssetCookRegistry() = default; + ClientAssetCookRegistry(const ClientAssetCookRegistry&) = delete; + ClientAssetCookRegistry(ClientAssetCookRegistry&&) = default; + ~ClientAssetCookRegistry() = default; + + void AddBlock(std::string blockName, BlockEntry blockEntry); + + const BlockEntry& GetBlock(std::string_view blockName) const; + + bool SaveToFile(const std::filesystem::path& path) const; + + ClientAssetCookRegistry& operator=(const ClientAssetCookRegistry&) = delete; + ClientAssetCookRegistry& operator=(ClientAssetCookRegistry&&) = default; + + static std::optional LoadFromContent(std::string_view content); + static std::optional LoadFromFile(const std::filesystem::path& path); + + enum class TextureType + { + None = -1, + BC1, + BC3, + BC4, + BC5, + + Max = BC5 + }; + + struct Texture + { + TextureType type = TextureType::None; + std::string path; + }; + + struct BlockEntry + { + Nz::Color baseColorFallback; + Texture ambientOcclusionHeightTexture; + Texture baseColorTexture; + Texture normalMapTexture; + Texture roughnessMetalnessTexture; + }; + + private: + std::unordered_map, std::equal_to<>> m_blockEntries; + }; +} + +#include + +#endif // TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP diff --git a/include/ClientLib/ClientAssetCookRegistry.inl b/include/ClientLib/ClientAssetCookRegistry.inl new file mode 100644 index 00000000..aa43beb0 --- /dev/null +++ b/include/ClientLib/ClientAssetCookRegistry.inl @@ -0,0 +1,7 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +namespace tsom +{ +} diff --git a/include/ClientLib/ClientAssetCooker.hpp b/include/ClientLib/ClientAssetCooker.hpp new file mode 100644 index 00000000..56b034cc --- /dev/null +++ b/include/ClientLib/ClientAssetCooker.hpp @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP +#define TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP + +#include +#include + +namespace Nz +{ + class ApplicationBase; +} + +namespace tsom +{ + class ClientBlockLibrary; + + class TSOM_CLIENTLIB_API ClientAssetCooker + { + public: + inline ClientAssetCooker(Nz::ApplicationBase& app); + ClientAssetCooker(const ClientAssetCooker&) = delete; + ClientAssetCooker(ClientAssetCooker&&) = default; + ~ClientAssetCooker() = default; + + Nz::Result Cook(ClientBlockLibrary& blockLibrary); + + ClientAssetCooker& operator=(const ClientAssetCooker&) = delete; + ClientAssetCooker& operator=(ClientAssetCooker&&) = default; + + private: + Nz::ApplicationBase& m_app; + }; +} + +#include + +#endif // TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP diff --git a/include/ClientLib/ClientAssetCooker.inl b/include/ClientLib/ClientAssetCooker.inl new file mode 100644 index 00000000..e0613dd3 --- /dev/null +++ b/include/ClientLib/ClientAssetCooker.inl @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +namespace tsom +{ + inline ClientAssetCooker::ClientAssetCooker(Nz::ApplicationBase& app) : + m_app(app) + { + } +} diff --git a/include/ClientLib/ClientAssetLibraryAppComponent.hpp b/include/ClientLib/ClientAssetLibraryAppComponent.hpp index de8cf59a..dfa673c9 100644 --- a/include/ClientLib/ClientAssetLibraryAppComponent.hpp +++ b/include/ClientLib/ClientAssetLibraryAppComponent.hpp @@ -14,6 +14,7 @@ namespace Nz { class Font; + class Material; class Model; class TextureAsset; } @@ -29,18 +30,26 @@ namespace tsom ~ClientAssetLibraryAppComponent() = default; inline std::shared_ptr GetFont(std::string_view name) const; + inline std::shared_ptr GetMaterial(std::string_view name) const; inline std::shared_ptr GetModel(std::string_view name) const; inline std::shared_ptr GetTexture(std::string_view name) const; + inline std::shared_ptr QueryFont(std::string_view name) const; + inline std::shared_ptr QueryMaterial(std::string_view name) const; + inline std::shared_ptr QueryModel(std::string_view name) const; + inline std::shared_ptr QueryTexture(std::string_view name) const; + inline void RegisterFont(std::string name, std::shared_ptr font); - inline void RegisterModel(std::string name, std::shared_ptr font); - inline void RegisterTexture(std::string name, std::shared_ptr font); + inline void RegisterMaterial(std::string name, std::shared_ptr material); + inline void RegisterModel(std::string name, std::shared_ptr model); + inline void RegisterTexture(std::string name, std::shared_ptr texture); ClientAssetLibraryAppComponent& operator=(const ClientAssetLibraryAppComponent&) = delete; ClientAssetLibraryAppComponent& operator=(ClientAssetLibraryAppComponent&&) = delete; private: Nz::ObjectLibrary m_fontLibrary; + Nz::ObjectLibrary m_materialLibrary; Nz::ObjectLibrary m_modelLibrary; Nz::ObjectLibrary m_textureLibrary; }; diff --git a/include/ClientLib/ClientAssetLibraryAppComponent.inl b/include/ClientLib/ClientAssetLibraryAppComponent.inl index 2604c61e..8a7cd39a 100644 --- a/include/ClientLib/ClientAssetLibraryAppComponent.inl +++ b/include/ClientLib/ClientAssetLibraryAppComponent.inl @@ -9,6 +9,11 @@ namespace tsom return m_fontLibrary.Get(name); } + inline std::shared_ptr ClientAssetLibraryAppComponent::GetMaterial(std::string_view name) const + { + return m_materialLibrary.Get(name); + } + inline std::shared_ptr ClientAssetLibraryAppComponent::GetModel(std::string_view name) const { return m_modelLibrary.Get(name); @@ -18,12 +23,37 @@ namespace tsom { return m_textureLibrary.Get(name); } + + inline std::shared_ptr ClientAssetLibraryAppComponent::QueryFont(std::string_view name) const + { + return m_fontLibrary.Query(name); + } + + inline std::shared_ptr ClientAssetLibraryAppComponent::QueryMaterial(std::string_view name) const + { + return m_materialLibrary.Query(name); + } + + inline std::shared_ptr ClientAssetLibraryAppComponent::QueryModel(std::string_view name) const + { + return m_modelLibrary.Query(name); + } + + inline std::shared_ptr ClientAssetLibraryAppComponent::QueryTexture(std::string_view name) const + { + return m_textureLibrary.Query(name); + } inline void ClientAssetLibraryAppComponent::RegisterFont(std::string name, std::shared_ptr font) { m_fontLibrary.Register(std::move(name), std::move(font)); } + inline void ClientAssetLibraryAppComponent::RegisterMaterial(std::string name, std::shared_ptr material) + { + m_materialLibrary.Register(std::move(name), std::move(material)); + } + inline void ClientAssetLibraryAppComponent::RegisterModel(std::string name, std::shared_ptr model) { m_modelLibrary.Register(std::move(name), std::move(model)); diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index ce0f503a..288741f9 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -9,11 +9,14 @@ #include #include +#include namespace Nz { class ApplicationBase; + class RenderBuffer; class RenderDevice; + class Texture; class TextureAsset; } @@ -25,19 +28,18 @@ namespace tsom inline ClientBlockLibrary(Nz::ApplicationBase& applicationBase); ~ClientBlockLibrary() = default; - void BuildTexture(); + void BuildTexture(Nz::RenderDevice& renderDevice); - inline const std::shared_ptr& GetBaseColorTexture() const; - inline const std::shared_ptr& GetDetailTexture() const; - inline const std::shared_ptr& GetNormalTexture() const; + inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; + inline const std::shared_ptr& GetGlobalBlockBuffer() const; inline const std::shared_ptr& GetPreviewTexture(BlockIndex blockIndex) const; private: - std::shared_ptr m_baseColorTexture; - std::shared_ptr m_detailTexture; - std::shared_ptr m_normalTexture; + std::shared_ptr m_globalBlockBuffer; std::vector> m_previewTextures; + Nz::EnumArray> m_blockTextures; Nz::ApplicationBase& m_applicationBase; + void* m_globalBlockBufferPtr; }; } diff --git a/include/ClientLib/ClientBlockLibrary.inl b/include/ClientLib/ClientBlockLibrary.inl index 77b79262..5af2d746 100644 --- a/include/ClientLib/ClientBlockLibrary.inl +++ b/include/ClientLib/ClientBlockLibrary.inl @@ -5,7 +5,8 @@ namespace tsom { inline ClientBlockLibrary::ClientBlockLibrary(Nz::ApplicationBase& applicationBase) : - m_applicationBase(applicationBase) + m_applicationBase(applicationBase), + m_globalBlockBufferPtr(nullptr) { // HAAAAAAAAAAAAAAAAAAAAX // Re-enable collisions for forcefield to allow clients to remove them (player collisions are only handled on the server for now) @@ -13,19 +14,14 @@ namespace tsom m_blocks[idx].hasCollisions = true; } - inline const std::shared_ptr& ClientBlockLibrary::GetBaseColorTexture() const + inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const { - return m_baseColorTexture; + return m_blockTextures[textureType]; } - inline const std::shared_ptr& ClientBlockLibrary::GetDetailTexture() const + inline const std::shared_ptr& ClientBlockLibrary::GetGlobalBlockBuffer() const { - return m_detailTexture; - } - - inline const std::shared_ptr& ClientBlockLibrary::GetNormalTexture() const - { - return m_normalTexture; + return m_globalBlockBuffer; } inline const std::shared_ptr& ClientBlockLibrary::GetPreviewTexture(BlockIndex blockIndex) const diff --git a/include/ClientLib/ClientChunkEntities.hpp b/include/ClientLib/ClientChunkEntities.hpp index 50df9053..f8d53d8a 100644 --- a/include/ClientLib/ClientChunkEntities.hpp +++ b/include/ClientLib/ClientChunkEntities.hpp @@ -28,8 +28,7 @@ namespace tsom { Nz::Vector3f position; Nz::Vector3f normal; - Nz::Vector3f uvw; - Nz::Vector3f tangent; + Nz::UInt32 blockIndex; }; class ConfigFile; diff --git a/include/ClientLib/ClientFramePipeline.hpp b/include/ClientLib/ClientFramePipeline.hpp new file mode 100644 index 00000000..dd81297c --- /dev/null +++ b/include/ClientLib/ClientFramePipeline.hpp @@ -0,0 +1,37 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_CLIENTLIB_CLIENTFRAMEPIPELINE_HPP +#define TSOM_CLIENTLIB_CLIENTFRAMEPIPELINE_HPP + +#include +#include + +namespace tsom +{ + class ClientBlockLibrary; + + class TSOM_CLIENTLIB_API ClientFramePipeline : public Nz::DefaultFramePipeline + { + public: + inline ClientFramePipeline(Nz::ElementRendererRegistry& elementRegistry, ClientBlockLibrary& blockLibrary); + ClientFramePipeline(const ClientFramePipeline&) = delete; + ClientFramePipeline(ClientFramePipeline&&) = delete; + ~ClientFramePipeline() = default; + + void Render(Nz::RenderResources& renderResources) override; + + ClientFramePipeline& operator=(const ClientFramePipeline&) = delete; + ClientFramePipeline& operator=(ClientFramePipeline&&) = delete; + + private: + ClientBlockLibrary& m_blockLibrary; + }; +} + +#include + +#endif // TSOM_CLIENTLIB_CLIENTFRAMEPIPELINE_HPP diff --git a/include/ClientLib/ClientFramePipeline.inl b/include/ClientLib/ClientFramePipeline.inl new file mode 100644 index 00000000..52af6f1b --- /dev/null +++ b/include/ClientLib/ClientFramePipeline.inl @@ -0,0 +1,12 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +namespace tsom +{ + inline ClientFramePipeline::ClientFramePipeline(Nz::ElementRendererRegistry& elementRegistry, ClientBlockLibrary& blockLibrary) : + DefaultFramePipeline(elementRegistry), + m_blockLibrary(blockLibrary) + { + } +} diff --git a/include/ClientLib/Components/VisualEntityComponent.inl b/include/ClientLib/Components/VisualEntityComponent.inl index c7096345..9a85f8bf 100644 --- a/include/ClientLib/Components/VisualEntityComponent.inl +++ b/include/ClientLib/Components/VisualEntityComponent.inl @@ -4,7 +4,7 @@ namespace tsom { - EntityOwnerComponent::~EntityOwnerComponent() + inline EntityOwnerComponent::~EntityOwnerComponent() { for (entt::handle& handle : m_entities) { diff --git a/include/CommonLib/BlockLibrary.hpp b/include/CommonLib/BlockLibrary.hpp index e8bb862c..911152e6 100644 --- a/include/CommonLib/BlockLibrary.hpp +++ b/include/CommonLib/BlockLibrary.hpp @@ -32,6 +32,7 @@ namespace tsom ~BlockLibrary() = default; inline const BlockData& GetBlockData(BlockIndex blockIndex) const; + inline const std::vector& GetBlocks() const; inline const LayerData& GetLayerData(std::size_t layerIndex) const; inline BlockIndex GetBlockIndex(std::string_view blockName) const; inline bool IsValidBlock(BlockIndex blockIndex) const; @@ -44,32 +45,29 @@ namespace tsom { std::string_view layerName = "default"; std::string basePath; - std::string baseBackPath; - std::string baseDownPath; - std::string baseFrontPath; - std::string baseLeftPath; - std::string baseRightPath; - std::string baseSidePath; - std::string baseUpPath; bool hasCollisions = true; bool isDoubleSided = false; bool isSmooth = false; bool isTransparent = false; float density = 1.0f; + float metalness = 0.0f; //< Used if texture is not available float permeability = 0.f; + float roughness = 1.0f; //< Used if texture is not available }; struct BlockData { std::size_t layerIndex; std::string name; - Nz::EnumArray texIndices; + std::string basePath; bool hasCollisions; bool isDoubleSided; bool isTransparent; bool isSmooth; float density; + float metalness = 0.0f; //< Used if texture is not available float permeability; + float roughness = 1.0f; //< Used if texture is not available }; struct LayerInfo @@ -95,13 +93,9 @@ namespace tsom BlockLibrary& operator=(const BlockLibrary&) = delete; BlockLibrary& operator=(BlockLibrary&&) = delete; - private: - unsigned int RegisterTexture(std::string&& texturePath); - protected: tsl::hopscotch_map, std::equal_to<>> m_blockIndices; tsl::hopscotch_map, std::equal_to<>> m_layerIndices; - tsl::hopscotch_map, std::equal_to<>> m_textureIndices; std::vector m_blocks; std::vector m_layers; }; diff --git a/include/CommonLib/BlockLibrary.inl b/include/CommonLib/BlockLibrary.inl index 39acd7bc..44cf6a74 100644 --- a/include/CommonLib/BlockLibrary.inl +++ b/include/CommonLib/BlockLibrary.inl @@ -9,6 +9,11 @@ namespace tsom return m_blocks[blockIndex]; } + inline auto BlockLibrary::GetBlocks() const -> const std::vector& + { + return m_blocks; + } + inline auto BlockLibrary::GetLayerData(std::size_t layerIndex) const -> const LayerData& { return m_layers[layerIndex]; diff --git a/include/CommonLib/Chunk.hpp b/include/CommonLib/Chunk.hpp index 0f92d6a3..9242b5e0 100644 --- a/include/CommonLib/Chunk.hpp +++ b/include/CommonLib/Chunk.hpp @@ -72,10 +72,6 @@ namespace tsom virtual Nz::EnumArray ComputeBlockCorners(const Nz::Vector3ui& indices) const; virtual std::optional ComputeHitCoordinates(const Nz::Vector3f& hitPos, const Nz::Vector3f& hitNormal, const Nz::Collider3D& collider, std::uint32_t hitSubshapeId) const = 0; - virtual void DeformNormals(Nz::SparsePtr normals, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const; - virtual void DeformNormalsAndTangents(Nz::SparsePtr normals, Nz::SparsePtr tangents, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const; - virtual bool DeformPositions(Nz::SparsePtr positions, std::size_t positionCount) const; - virtual void Deserialize(Nz::ByteStream& byteStream); inline std::span GetActiveLayers() const; @@ -144,11 +140,9 @@ namespace tsom struct VertexAttributes { Nz::UInt32 firstIndex; - Nz::SparsePtr color; Nz::SparsePtr position; Nz::SparsePtr normal; - Nz::SparsePtr tangent; - Nz::SparsePtr uv; + Nz::SparsePtr blockIndex; }; protected: diff --git a/include/CommonLib/DeformedChunk.hpp b/include/CommonLib/DeformedChunk.hpp deleted file mode 100644 index 3147757f..00000000 --- a/include/CommonLib/DeformedChunk.hpp +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) -// This file is part of the "This Space Of Mine" project -// For conditions of distribution and use, see copyright notice in LICENSE - -#pragma once - -#ifndef TSOM_COMMONLIB_DEFORMEDCHUNK_HPP -#define TSOM_COMMONLIB_DEFORMEDCHUNK_HPP - -#include -#include - -namespace tsom -{ - class DeformedChunk : public Chunk - { - public: - inline DeformedChunk(const BlockLibrary& blockLibrary, ChunkContainer& owner, const ChunkIndices& indices, const Nz::Vector3ui& size, float cellSize, const Nz::Vector3f& deformationCenter, float deformationRadius); - DeformedChunk(const DeformedChunk&) = delete; - DeformedChunk(DeformedChunk&&) = delete; - ~DeformedChunk() = default; - - std::pair, Nz::Vector3f> BuildBlockCollider(const Nz::Vector3ui& blockIndices, float scale = 1.f) const override; - std::shared_ptr BuildCollider(std::size_t layerIndex) const override; - - std::optional ComputeHitCoordinates(const Nz::Vector3f& hitPos, const Nz::Vector3f& hitNormal, const Nz::Collider3D& collider, std::uint32_t hitSubshapeId) const override; - Nz::EnumArray ComputeBlockCorners(const Nz::Vector3ui& indices) const override; - - void DeformNormals(Nz::SparsePtr normals, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const override; - void DeformNormalsAndTangents(Nz::SparsePtr normals, Nz::SparsePtr tangents, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const override; - bool DeformPositions(Nz::SparsePtr positions, std::size_t positionCount) const override; - - inline void UpdateDeformationRadius(float deformationRadius); - - DeformedChunk& operator=(const DeformedChunk&) = delete; - DeformedChunk& operator=(DeformedChunk&&) = delete; - - static Nz::Vector3f DeformPosition(const Nz::Vector3f& position, const Nz::Vector3f& deformationCenter, float deformationRadius); - static Nz::Quaternionf GetNormalDeformation(const Nz::Vector3f& position, const Nz::Vector3f& faceNormal, const Nz::Vector3f& deformationCenter, float deformationRadius); - - private: - Nz::Vector3f m_deformationCenter; - float m_deformationRadius; - }; -} - -#include - -#endif // TSOM_COMMONLIB_DEFORMEDCHUNK_HPP diff --git a/include/CommonLib/DeformedChunk.inl b/include/CommonLib/DeformedChunk.inl deleted file mode 100644 index 2a33e4bf..00000000 --- a/include/CommonLib/DeformedChunk.inl +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) -// This file is part of the "This Space Of Mine" project -// For conditions of distribution and use, see copyright notice in LICENSE - -#include - -namespace tsom -{ - inline DeformedChunk::DeformedChunk(const BlockLibrary& blockLibrary, ChunkContainer& owner, const ChunkIndices& indices, const Nz::Vector3ui& size, float cellSize, const Nz::Vector3f& deformationCenter, float deformationRadius) : - Chunk(blockLibrary, owner, indices, size, cellSize), - m_deformationCenter(deformationCenter), - m_deformationRadius(deformationRadius) - { - SetPerFaceCollision(); - } - - inline void DeformedChunk::UpdateDeformationRadius(float deformationRadius) - { - m_deformationRadius = deformationRadius; - } - - inline Nz::Vector3f DeformedChunk::DeformPosition(const Nz::Vector3f& position, const Nz::Vector3f& deformationCenter, float deformationRadius) - { - float distToCenter = std::max({ - std::abs(position.x - deformationCenter.x), - std::abs(position.y - deformationCenter.y), - std::abs(position.z - deformationCenter.z), - }); - - float innerReductionSize = std::max(distToCenter - deformationRadius, 0.f); - Nz::Boxf innerBox(deformationCenter - Nz::Vector3f(innerReductionSize), Nz::Vector3f(innerReductionSize * 2.f)); - - Nz::Vector3f innerPos = Nz::Vector3f::Clamp(position, innerBox.GetMinimum(), innerBox.GetMaximum()); - Nz::Vector3f normal = Nz::Vector3f::Normalize(position - innerPos); - - return innerPos + normal * std::min(deformationRadius, distToCenter); - } - - inline Nz::Quaternionf DeformedChunk::GetNormalDeformation(const Nz::Vector3f& position, const Nz::Vector3f& faceNormal, const Nz::Vector3f& deformationCenter, float deformationRadius) - { - float distToCenter = std::max({ - std::abs(position.x - deformationCenter.x), - std::abs(position.y - deformationCenter.y), - std::abs(position.z - deformationCenter.z), - }); - - float innerReductionSize = std::max(distToCenter - deformationRadius, 0.f); - Nz::Boxf innerBox(deformationCenter - Nz::Vector3f(innerReductionSize), Nz::Vector3f(innerReductionSize * 2.f)); - - Nz::Vector3f innerPos = Nz::Vector3f::Clamp(position, innerBox.GetMinimum(), innerBox.GetMaximum()); - Nz::Vector3f normal = Nz::Vector3f::Normalize(position - innerPos); - - return Nz::Quaternionf::RotationBetween(faceNormal, normal); - } -} diff --git a/include/CommonLib/NetworkReactor.hpp b/include/CommonLib/NetworkReactor.hpp index 35595e64..76edfc36 100644 --- a/include/CommonLib/NetworkReactor.hpp +++ b/include/CommonLib/NetworkReactor.hpp @@ -36,7 +36,7 @@ namespace tsom NetworkReactor(NetworkReactor&&) = delete; ~NetworkReactor(); - std::size_t ConnectTo(Nz::IpAddress address, Nz::UInt32 data = 0); + std::size_t ConnectTo(const Nz::IpAddress& address, Nz::UInt32 data = 0); void DisconnectPeer(std::size_t peerId, Nz::UInt32 data = 0, DisconnectionType type = DisconnectionType::Normal); inline std::size_t GetIdOffset() const; diff --git a/include/CommonLib/Planet.hpp b/include/CommonLib/Planet.hpp index 603a3679..71afe62c 100644 --- a/include/CommonLib/Planet.hpp +++ b/include/CommonLib/Planet.hpp @@ -77,11 +77,22 @@ namespace tsom NazaraSlot(Chunk, OnReset, onReset); }; + struct ChunkGenerator + { + ChunkGenerator(Nz::ApplicationBase& app) : + scriptingContext(app) + { + } + + ScriptingContext scriptingContext; + sol::protected_function generationFunction; + }; + std::mutex m_chunkLayerAddedSignalMutex; std::mutex m_chunkLayerRemovedSignalMutex; std::mutex m_chunkUpdatedSignalMutex; tsl::hopscotch_map m_chunks; - Nz::ThreadLocalData m_scriptingContexts; + Nz::ThreadLocalData m_chunkGenerators; Nz::ApplicationBase& m_app; float m_cornerRadius; float m_gravity; diff --git a/include/CommonLib/Scripting/MathScriptingLibrary.hpp b/include/CommonLib/Scripting/MathScriptingLibrary.hpp index 4927b92c..9d3ab633 100644 --- a/include/CommonLib/Scripting/MathScriptingLibrary.hpp +++ b/include/CommonLib/Scripting/MathScriptingLibrary.hpp @@ -31,6 +31,7 @@ namespace tsom template void RegisterEulerAngles(sol::state& state, const char* name); void RegisterPerlinNoise(sol::state& state); template void RegisterQuaternion(sol::state& state, const char* name); + void RegisterSignedDistance(sol::state& state); void RegisterTime(sol::state& state); template void RegisterVector2(sol::state& state, const char* name); template void RegisterVector3(sol::state& state, const char* name); diff --git a/include/CommonLib/Scripting/ScriptingUtils.inl b/include/CommonLib/Scripting/ScriptingUtils.inl index ab6b73c3..5e3f252a 100644 --- a/include/CommonLib/Scripting/ScriptingUtils.inl +++ b/include/CommonLib/Scripting/ScriptingUtils.inl @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include namespace tsom { @@ -182,3 +185,109 @@ namespace tsom return Wrapper::Wrap(std::move(funcPtr)); } } + +namespace sol +{ + template + struct lua_size> : std::integral_constant {}; + + template + struct lua_size> : std::integral_constant {}; + + template + struct lua_size> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + + template + inline Nz::Rect sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table rect = sol::stack::get(L, absoluteIndex); + T x = rect["x"]; + T y = rect["y"]; + T width = rect["width"]; + T height = rect["height"]; + + tracking.use(1); + + return Nz::Rect(x, y, width, height); + } + + template + Nz::Vector2 sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table rect = sol::stack::get(L, absoluteIndex); + T x = rect["x"]; + T y = rect["y"]; + + tracking.use(1); + + return Nz::Vector2(x, y); + } + + template + Nz::Vector3 sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table rect = sol::stack::get(L, absoluteIndex); + T x = rect["x"]; + T y = rect["y"]; + T z = rect["z"]; + + tracking.use(1); + + return Nz::Vector3(x, y, z); + } + + + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::Rect& rect) + { + lua_createtable(L, 0, 4); + luaL_setmetatable(L, "rect"); + sol::stack_table vec(L); + vec["x"] = rect.x; + vec["y"] = rect.y; + vec["width"] = rect.width; + vec["height"] = rect.height; + + return 1; + } + + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector2& v) + { + lua_createtable(L, 0, 2); + luaL_setmetatable(L, "vec2"); + sol::stack_table vec(L); + vec["x"] = v.x; + vec["y"] = v.y; + + return 1; + } + + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector3& v) + { + lua_createtable(L, 0, 3); + luaL_setmetatable(L, "vec3"); + sol::stack_table vec(L); + vec["x"] = v.x; + vec["y"] = v.y; + vec["z"] = v.z; + + return 1; + } +} diff --git a/include/CommonLib/Utility/SignedDistanceFunctions.hpp b/include/CommonLib/Utility/SignedDistanceFunctions.hpp index 60821f53..d57d062f 100644 --- a/include/CommonLib/Utility/SignedDistanceFunctions.hpp +++ b/include/CommonLib/Utility/SignedDistanceFunctions.hpp @@ -7,11 +7,14 @@ #ifndef TSOM_COMMONLIB_UTILITY_SIGNEDDISTANCEFUNCTIONS_HPP #define TSOM_COMMONLIB_UTILITY_SIGNEDDISTANCEFUNCTIONS_HPP +#include #include namespace tsom { + // https://iquilezles.org/articles/distfunctions/ inline float sdRoundBox(const Nz::Vector3f& pos, const Nz::Vector3f& halfDims, float cornerRadius); + inline float sdTorus(const Nz::Vector3f& pos, const Nz::Vector2f& dims); } #include diff --git a/include/CommonLib/Utility/SignedDistanceFunctions.inl b/include/CommonLib/Utility/SignedDistanceFunctions.inl index 4c24542a..0534d1ae 100644 --- a/include/CommonLib/Utility/SignedDistanceFunctions.inl +++ b/include/CommonLib/Utility/SignedDistanceFunctions.inl @@ -11,4 +11,10 @@ namespace tsom float insideDistance = std::min(std::max({ edgeDistance.x, edgeDistance.y, edgeDistance.z }), 0.f); return outsideDistance + insideDistance - cornerRadius; } + + inline float sdTorus(const Nz::Vector3f& pos, const Nz::Vector2f& dims) + { + Nz::Vector2f q(Nz::Vector2f(pos.x, pos.z).GetLength() - dims.x, pos.y); + return q.GetLength() - dims.y; + } } diff --git a/scripts/planets/alice.lua b/scripts/planets/alice.lua index 03483e1a..7061ab7d 100644 --- a/scripts/planets/alice.lua +++ b/scripts/planets/alice.lua @@ -1,69 +1,108 @@ +-- Do not touch to this 2 variables local perlin = PerlinNoise() local chunksize = 32 -local scale = 0.02 -local freespace = 30 -return function (chunk, seed, chunkcount) +local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things +local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight + +return function (chunk, seed, chunkDims) perlin:reseed(seed) - math.randomseed(seed) + + local blockSize = chunk:GetBlockSize() local blockLibrary = chunk:GetBlockLibrary() local blockCount = chunk:GetBlockCount() - local empty = blockLibrary:GetBlockIndex("empty") - local dirt = blockLibrary:GetBlockIndex("dirt") - local grass = blockLibrary:GetBlockIndex("grass") - local snow = blockLibrary:GetBlockIndex("snow") - local stone = blockLibrary:GetBlockIndex("stone") - local stoneMossy = blockLibrary:GetBlockIndex("stone_mossy") + local emptyBlock = blockLibrary:GetBlockIndex("empty") + local debugBlock = blockLibrary:GetBlockIndex("debug") + local dirtBlock = blockLibrary:GetBlockIndex("dirt") + local grassBlock = blockLibrary:GetBlockIndex("grass") + local hullBlock = blockLibrary:GetBlockIndex("hull") + local snowBlock = blockLibrary:GetBlockIndex("snow") + local stoneBlock = blockLibrary:GetBlockIndex("stone") + local stoneMossyBlock = blockLibrary:GetBlockIndex("stone_mossy") + local forcefieldBlock = blockLibrary:GetBlockIndex("forcefield") + local planksBlock = blockLibrary:GetBlockIndex("planks") + local stoneBricksBlock = blockLibrary:GetBlockIndex("stone_bricks") + local copperBlock = blockLibrary:GetBlockIndex("copper_block") + local glassBlock = blockLibrary:GetBlockIndex("glass") local planet = chunk:GetContainer() local chunkIndices = chunk:GetIndices() - - local maxHeight = (Vec3i(chunkcount.x, chunkcount.y, chunkcount.z) + Vec3i(1)) / 2 - maxHeight = maxHeight * chunksize - + + local maxHeight = (chunksize * chunkDims.x)/2 * blockSize; + local maxGenerationHeight = maxHeight - minGrenerationFreeHeight + local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions + + local terrainVariation1Scale = 0.06 * baseHeight + local terrainVariation2Scale = 0.16 * baseHeight + local moutainScale = 0.035 * baseHeight + local spikeScale = 0.2 * baseHeight + local caveScale = 0.06 -- Other scale unit + local content = {} for z = 0, chunksize - 1 do for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) - local depth = math.min( - maxHeight.x - math.abs(blockPos.x), - maxHeight.y - math.abs(blockPos.y), - maxHeight.z - math.abs(blockPos.z) - ) + local blockPosScaled = Vec3f(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosNorm, distToCenter = blockPosScaled:GetNormal() + --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) + distToCenter = SignedDistance.RoundBox(blockPosScaled, Vec3f(baseHeight), 16.0) - if (depth < freespace) then - table.insert(content, empty) + if distToCenter > baseFreeHeight then + table.insert(content, emptyBlock) goto continue end - depth = depth - freespace + local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) - local presence = perlin:normalizedOctave3D_01(blockPos.x * scale, blockPos.y * scale, blockPos.z * scale, 4, 0.5) - if depth < 20 then - presence = presence * math.max(depth / 20.0, 1.0) - end - - presence = presence + depth / math.max(maxHeight.x, maxHeight.y, maxHeight.z) - - local blockIndex - if presence > 0.6 then - if depth < 6 * 2 then - blockIndex = snow - elseif depth <= 18 * 2 then - blockIndex = dirt + if distToCenter <= -32.0 then + if blockPresence >= 0.3 and blockPresence <= 0.7 then + if distToCenter <= -5 then + table.insert(content, stoneBlock) + else + table.insert(content, dirtBlock) + end else - blockIndex = math.random() > 0.1 and stone or stoneMossy + table.insert(content, stoneBlock) end else - blockIndex = empty + local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) + local mountainous + if baseMountainous < 0.6 then + mountainous = 0 + elseif baseMountainous < 0.8 then + mountainous = 5*baseMountainous-3 + else + mountainous = 1 + end + + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) + local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) + + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) + + local height = heightVariation1 + heightVariation2 + + if distToCenter <= height then + if distToCenter >= height then + table.insert(content, stoneMossyBlock) + elseif mountainous > 0.5 and heightVariation2 > 0.5 then + table.insert(content, snowBlock) + elseif mountainous > 0.1 then + table.insert(content, stoneBlock) + elseif baseMountainous < 0.4 then + table.insert(content, grassBlock) + else + table.insert(content, dirtBlock) + end + else + table.insert(content, emptyBlock) + end end - - table.insert(content, blockIndex) - + ::continue:: end end diff --git a/scripts/planets/bob.lua b/scripts/planets/bob.lua index 79cbc2da..039fbb7d 100644 --- a/scripts/planets/bob.lua +++ b/scripts/planets/bob.lua @@ -5,9 +5,75 @@ local chunksize = 32 local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight -return function (chunk, seed) +local Vec3mt = CreateMetatable("vec3") +Vec3mt.__index = Vec3mt + +function Vec3mt:__add(vec) + return Vec3(self.x + vec.x, self.y + vec.y, self.z + vec.z) +end + +function Vec3mt:__sub(vec) + return Vec3(self.x - vec.x, self.y - vec.y, self.z - vec.z) +end + +function Vec3mt:__mul(vec) + if type(vec) == "number" then + return Vec3(self.x * vec, self.y * vec, self.z * vec) + else + return Vec3(self.x * vec.x, self.y * vec.y, self.z * vec.z) + end +end + +function Vec3mt:__div(vec) + if type(vec) == "number" then + return Vec3(self.x / vec, self.y / vec, self.z / vec) + else + return Vec3(self.x / vec.x, self.y / vec.y, self.z / vec.z) + end +end + +function Vec3mt:GetAbs() + return Vec3(math.abs(self.x), math.abs(self.y), math.abs(self.z)) +end + +function Vec3mt:GetLength() + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) +end + +function Vec3mt:GetNormal() + local length = self:GetLength() + return Vec3(self.x / length, self.y / length, self.z / length), length +end + +function Vec3mt:Maximize(vec) + return Vec3(math.max(self.x, vec.x), math.max(self.y, vec.y), math.max(self.z, vec.z)) +end + +function Vec3mt:Minimize(vec) + return Vec3(math.min(self.x, vec.x), math.min(self.y, vec.y), math.min(self.z, vec.z)) +end + +function Vec3(x, y, z) + return setmetatable({x = x, y = y or x, z = z or x}, Vec3mt) +end + +local function GetBlockIndices(chunkIndices, blockIndices) + local x = chunkIndices.x * chunksize + blockIndices.x - chunksize * 0.5 + local y = chunkIndices.y * chunksize + blockIndices.z - chunksize * 0.5 + local z = chunkIndices.z * chunksize + blockIndices.y - chunksize * 0.5 + return Vec3(x, y, z) +end + +local function sdRoundBox(pos, dims, cornerRadius) + local q = pos:GetAbs() - dims + Vec3(cornerRadius) + return q:Maximize(Vec3(0, 0, 0)):GetLength() + math.min(math.max(q.x, math.max(q.y, q.z)), 0.0) - cornerRadius +end + +return function (chunk, seed, chunkDims) perlin:reseed(seed) + local blockSize = chunk:GetBlockSize() + local blockLibrary = chunk:GetBlockLibrary() local blockCount = chunk:GetBlockCount() @@ -16,7 +82,6 @@ return function (chunk, seed) local dirtBlock = blockLibrary:GetBlockIndex("dirt") local grassBlock = blockLibrary:GetBlockIndex("grass") local hullBlock = blockLibrary:GetBlockIndex("hull") - local hull2Block = blockLibrary:GetBlockIndex("hull2") local snowBlock = blockLibrary:GetBlockIndex("snow") local stoneBlock = blockLibrary:GetBlockIndex("stone") local stoneMossyBlock = blockLibrary:GetBlockIndex("stone_mossy") @@ -29,37 +94,39 @@ return function (chunk, seed) local planet = chunk:GetContainer() local chunkIndices = chunk:GetIndices() - local maxHeight = (chunksize * planet:GetChunkCount()^(1/3))/2; + local maxHeight = (chunksize * chunkDims.x)/2 * blockSize; local maxGenerationHeight = maxHeight - minGrenerationFreeHeight local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions local terrainVariation1Scale = 0.06 * baseHeight local terrainVariation2Scale = 0.16 * baseHeight - local moutainScale = 0.03 * baseHeight + local moutainScale = 0.035 * baseHeight local spikeScale = 0.2 * baseHeight - local caveScale = 0.06 -- Other scale unit + local caveScale = 0.06 -- Other scale unit local content = {} for z = 0, chunksize - 1 do for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do - local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) - local blockPosNorm, distToCenter = Vec3f(blockPos.x * 1.0, blockPos.y * 1.0, blockPos.z * 1.0):GetNormal() - distToCenter = math.max(math.abs(blockPos.x + 0.5), math.abs(blockPos.y + 0.5), math.abs(blockPos.z + 0.5)) - - if distToCenter > maxGenerationHeight then + local blockPos = GetBlockIndices(chunkIndices, Vec3(x, y, z)) + local blockPosScaled = Vec3(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosNorm, distToCenter = blockPosScaled:GetNormal() + --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) + distToCenter = sdRoundBox(blockPosScaled, Vec3(baseHeight), 16.0) + + if distToCenter > baseFreeHeight then table.insert(content, emptyBlock) goto continue end + + local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) - local blockPresence = perlin:normalizedOctave3D_01(blockPos.x * caveScale, blockPos.y * caveScale, blockPos.z * caveScale, 4, 0.1) - - if distToCenter <= baseHeight then + if distToCenter <= -32.0 then if blockPresence >= 0.3 and blockPresence <= 0.7 then - if distToCenter <= baseHeight-5 then + if distToCenter <= -5 then table.insert(content, stoneBlock) - elseif distToCenter <= baseHeight then + else table.insert(content, dirtBlock) end else @@ -67,6 +134,7 @@ return function (chunk, seed) end else local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) + local mountainous if baseMountainous < 0.6 then mountainous = 0 elseif baseMountainous < 0.8 then @@ -79,6 +147,7 @@ return function (chunk, seed) local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) + local spikeHeight if baseSpikeHeight < 0.7 then spikeHeight = 0 elseif baseSpikeHeight < 0.9 then @@ -88,7 +157,7 @@ return function (chunk, seed) end spikeHeight = (1-mountainous) * spikeHeight * 20 - local height = baseHeight + heightVariation1 + heightVariation2 + spikeHeight + local height = heightVariation1 + heightVariation2 + spikeHeight if distToCenter <= height then if distToCenter >= height - spikeHeight then diff --git a/scripts/planets/bob_pure.lua b/scripts/planets/bob_pure.lua new file mode 100644 index 00000000..c1f0d3fb --- /dev/null +++ b/scripts/planets/bob_pure.lua @@ -0,0 +1,134 @@ +-- Do not touch to this 2 variables +local perlin = PerlinNoise() +local chunksize = 32 + +local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things +local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight + +local Vec3mt = {} +Vec3mt.__index = Vec3mt + +function Vec3(x, y, z) + return setmetatable({x = x, y = y, z = z}, Vec3mt) +end + +function Vec3mt:GetNormal() + local length = math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + return Vec3(self.x / length, self.y / length, self.z / length), length +end + +return function (chunk, seed, chunkDims) + perlin:reseed(seed) + + local blockSize = chunk:GetBlockSize() + + local blockLibrary = chunk:GetBlockLibrary() + local blockCount = chunk:GetBlockCount() + + local emptyBlock = blockLibrary:GetBlockIndex("empty") + local debugBlock = blockLibrary:GetBlockIndex("debug") + local dirtBlock = blockLibrary:GetBlockIndex("dirt") + local grassBlock = blockLibrary:GetBlockIndex("grass") + local hullBlock = blockLibrary:GetBlockIndex("hull") + local hull2Block = blockLibrary:GetBlockIndex("hull2") + local snowBlock = blockLibrary:GetBlockIndex("snow") + local stoneBlock = blockLibrary:GetBlockIndex("stone") + local stoneMossyBlock = blockLibrary:GetBlockIndex("stone_mossy") + local forcefieldBlock = blockLibrary:GetBlockIndex("forcefield") + local planksBlock = blockLibrary:GetBlockIndex("planks") + local stoneBricksBlock = blockLibrary:GetBlockIndex("stone_bricks") + local copperBlock = blockLibrary:GetBlockIndex("copper_block") + local glassBlock = blockLibrary:GetBlockIndex("glass") + + local planet = chunk:GetContainer() + local chunkIndices = chunk:GetIndices() + + local maxHeight = (chunksize * chunkDims.x)/2 * blockSize; + local maxGenerationHeight = maxHeight - minGrenerationFreeHeight + local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions + + local terrainVariation1Scale = 0.06 * baseHeight + local terrainVariation2Scale = 0.16 * baseHeight + local moutainScale = 0.035 * baseHeight + local spikeScale = 0.2 * baseHeight + local caveScale = 0.06 -- Other scale unit + + local content = {} + + for z = 0, chunksize - 1 do + for y = 0, chunksize - 1 do + for x = 0, chunksize - 1 do + local blockPos = planet:GetBlockIndices(chunkIndices, Vec3(x, y, z)) + local blockPosScaled = Vec3(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosNorm, distToCenter = blockPosScaled:GetNormal() + --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) + distToCenter = SignedDistance.RoundBox(blockPosScaled, Vec3(baseHeight), 16.0) + + if distToCenter > baseFreeHeight then + table.insert(content, emptyBlock) + goto continue + end + + local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) + + if distToCenter <= -32.0 then + if blockPresence >= 0.3 and blockPresence <= 0.7 then + if distToCenter <= -5 then + table.insert(content, stoneBlock) + else + table.insert(content, dirtBlock) + end + else + table.insert(content, stoneBlock) + end + else + local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) + local mountainous + if baseMountainous < 0.6 then + mountainous = 0 + elseif baseMountainous < 0.8 then + mountainous = 5*baseMountainous-3 + else + mountainous = 1 + end + + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) + local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) + + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) + local spikeHeight + if baseSpikeHeight < 0.7 then + spikeHeight = 0 + elseif baseSpikeHeight < 0.9 then + spikeHeight = 5*baseSpikeHeight-3.5 + else + spikeHeight = 1 + end + spikeHeight = (1-mountainous) * spikeHeight * 20 + + local height = heightVariation1 + heightVariation2 + spikeHeight + + if distToCenter <= height then + if distToCenter >= height - spikeHeight then + table.insert(content, stoneMossyBlock) + elseif mountainous > 0.5 and heightVariation2 > 0.5 then + table.insert(content, snowBlock) + elseif mountainous > 0.1 then + table.insert(content, stoneBlock) + elseif baseMountainous < 0.4 then + table.insert(content, grassBlock) + else + table.insert(content, dirtBlock) + end + else + table.insert(content, emptyBlock) + end + end + + ::continue:: + end + end + end + + return content +end diff --git a/scripts/planets/test.lua b/scripts/planets/test.lua new file mode 100644 index 00000000..275cc1c6 --- /dev/null +++ b/scripts/planets/test.lua @@ -0,0 +1,66 @@ +-- Do not touch to this 2 variables +local perlin = PerlinNoise() +local chunksize = 32 + +local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things +local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight + +local abs = math.abs +local max = math.max +local min = math.min + +return function (chunk, seed, chunkDims) + perlin:reseed(seed) + + local blockLibrary = chunk:GetBlockLibrary() + local blockCount = chunk:GetBlockCount() + + local emptyBlock = blockLibrary:GetBlockIndex("empty") + local debugBlock = blockLibrary:GetBlockIndex("debug") + local dirtBlock = blockLibrary:GetBlockIndex("dirt") + local grassBlock = blockLibrary:GetBlockIndex("grass") + local hullBlock = blockLibrary:GetBlockIndex("hull") + local hull2Block = blockLibrary:GetBlockIndex("hull2") + local snowBlock = blockLibrary:GetBlockIndex("snow") + local stoneBlock = blockLibrary:GetBlockIndex("stone") + local stoneMossyBlock = blockLibrary:GetBlockIndex("stone_mossy") + local forcefieldBlock = blockLibrary:GetBlockIndex("forcefield") + local planksBlock = blockLibrary:GetBlockIndex("planks") + local stoneBricksBlock = blockLibrary:GetBlockIndex("stone_bricks") + local copperBlock = blockLibrary:GetBlockIndex("copper_block") + local glassBlock = blockLibrary:GetBlockIndex("glass") + + local planet = chunk:GetContainer() + local chunkIndices = chunk:GetIndices() + + local maxHeight = (chunksize * chunkDims.x)/2 / 4; + local maxGenerationHeight = maxHeight - minGrenerationFreeHeight + local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions + + local terrainVariation1Scale = 0.06 * baseHeight + local terrainVariation2Scale = 0.16 * baseHeight + local moutainScale = 0.03 * baseHeight + local spikeScale = 0.2 * baseHeight + local caveScale = 0.06 / 2 -- Other scale unit + + local content = {} + local roundBox = SignedDistance.RoundBox + for z = 0, chunksize - 1 do + for y = 0, chunksize - 1 do + for x = 0, chunksize - 1 do + local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) + local blockPosNorm, distToCenter = Vec3f(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5):GetNormal() + --distToCenter = math.max(math.abs(blockPos.x * 0.25 + 0.5), math.abs(blockPos.y * 0.25 + 0.5), math.abs(blockPos.z * 0.25 + 0.5)) + --distToCenter = roundBox(Vec3f(blockPos.x * 0.25, blockPos.z * 0.25, blockPos.y * 0.25), Vec3f(baseHeight, baseHeight, baseHeight), 16.0) + + if distToCenter > 60 then + table.insert(content, emptyBlock) + else + table.insert(content, dirtBlock) + end + end + end + end + + chunk:Reset(content) +end diff --git a/scripts/planets/torus.lua b/scripts/planets/torus.lua new file mode 100644 index 00000000..1c347cf6 --- /dev/null +++ b/scripts/planets/torus.lua @@ -0,0 +1,108 @@ +local perlin = PerlinNoise() +local chunksize = 32 +local scale = 0.02-- / 4 +local freespace = 30 + +-- https://iquilezles.org/articles/distfunctions/ +local function sdTorus(p, t) + local q = Vec2f(Vec2f(p.x, p.z):GetLength() - t.x, p.y) + return q:GetLength() - t.y +end + +local function sdOctahedron(p, s) + p = Vec3f(math.abs(p.x), math.abs(p.y), math.abs(p.z)) + return (p.x+p.y+p.z-s)*0.57735027; +end + +--[[ +float sdCappedCylinder( vec3 p, float r, float h ) +{ + vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h); + return min(max(d.x,d.y),0.0) + length(max(d,0.0)); +} +]] + +local function sdCappedCylinder(p, r, h) + local d = Vec2f(Vec2f(p.x, p.z):GetLength(), math.abs(p.y)) - Vec2f(r, h) + d.x = math.max(d.x, 0.0) + d.y = math.max(d.y, 0.0) + return math.min(math.max(d.x, d.y),0.0) + d:GetLength() +end + + +return function (chunk, seed, chunkcount) + perlin:reseed(seed) + math.randomseed(seed) + + local blockLibrary = chunk:GetBlockLibrary() + local blockCount = chunk:GetBlockCount() + + local empty = blockLibrary:GetBlockIndex("empty") + local dirt = blockLibrary:GetBlockIndex("dirt") + local grass = blockLibrary:GetBlockIndex("grass") + local snow = blockLibrary:GetBlockIndex("snow") + local stone = blockLibrary:GetBlockIndex("stone") + local stoneMossy = blockLibrary:GetBlockIndex("stone_mossy") + + local planet = chunk:GetContainer() + local chunkIndices = chunk:GetIndices() + + local maxHeight = (Vec3i(chunkcount.x, chunkcount.y, chunkcount.z) + Vec3i(1)) / 2 + maxHeight = maxHeight * chunksize + + local content = {} + + for z = 0, chunksize - 1 do + for y = 0, chunksize - 1 do + for x = 0, chunksize - 1 do + local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) + --[[local depth = math.min( + maxHeight.x - math.abs(blockPos.x), + maxHeight.y - math.abs(blockPos.y), + maxHeight.z - math.abs(blockPos.z) + )]] + --[[local depth = maxHeight.x - math.max(0, sdTorus(blockPos, Vec2f(20, 50))) + + if (depth < freespace) then + table.insert(content, empty) + goto continue + end + + depth = depth - freespace + --depth = depth * 0.25 + + local presence = perlin:normalizedOctave3D_01(blockPos.x * scale, blockPos.y * scale, blockPos.z * scale, 4, 0.5) + if depth < 20 then + presence = presence * math.max(depth / 20.0, 1.0) + end + + presence = presence + depth / math.max(maxHeight.x, maxHeight.y, maxHeight.z) + + local blockIndex + if presence > 0.6 then + if depth < 6 * 2 then + blockIndex = snow + elseif depth <= 18 * 2 then + blockIndex = dirt + else + blockIndex = math.random() > 0.1 and stone or stoneMossy + end + else + blockIndex = empty + end + + table.insert(content, blockIndex)]] + + local dist = sdTorus(blockPos, Vec2f(50, 20)) + if dist < 0 then + table.insert(content, dirt) + else + table.insert(content, empty) + end + + ::continue:: + end + end + end + chunk:Reset(content) +end diff --git a/src/ClientLib/BlockSelectionBar.cpp b/src/ClientLib/BlockSelectionBar.cpp index 95507e78..411a022a 100644 --- a/src/ClientLib/BlockSelectionBar.cpp +++ b/src/ClientLib/BlockSelectionBar.cpp @@ -4,29 +4,29 @@ #include #include +#include #include #include #include namespace tsom { - constexpr std::array s_selectableBlocks = { "dirt", "grass", "stone", "snow", "stone_bricks", "planks", "debug", "hull", "forcefield", "copper_block", "glass" }; + constexpr std::array s_selectableBlocks = { "dirt", "grass", "stone", "snow", "stone_bricks", "planks", "debug", "forcefield", "copper_block", "glass", "bark", "cliff_rocks", "rock", "wood_floor", "white_bricks", "gold", "metal", "metal_plates", "brickswall", "floor_tiles"}; BlockSelectionBar::BlockSelectionBar(Nz::BaseWidget* parent, const ClientBlockLibrary& blockLibrary) : BaseWidget(parent), m_selectedIndex(0), m_blockLibrary(blockLibrary) { - const auto& blockColorMap = m_blockLibrary.GetBaseColorTexture(); - for (std::string_view blockName : s_selectableBlocks) { bool active = m_selectedIndex == m_inventorySprites.size(); BlockIndex blockIndex = m_blockLibrary.GetBlockIndex(blockName); + NazaraAssertMsg(blockIndex != InvalidBlockIndex, "%s is not a valid block name", blockName.data()); std::shared_ptr slotMat = Nz::MaterialInstance::Instantiate(Nz::MaterialType::Basic); - slotMat->SetTextureProperty("BaseColorMap", m_blockLibrary.GetPreviewTexture(blockIndex)); + //slotMat->SetTextureProperty("BaseColorMap", m_blockLibrary.GetPreviewTexture(blockIndex)); Nz::ImageWidget* imageWidget = Add(slotMat); imageWidget->SetColor((active) ? Nz::Color::White() : Nz::Color::sRGBToLinear(Nz::Color::Gray())); diff --git a/src/ClientLib/ClientAssetCookRegistry.cpp b/src/ClientLib/ClientAssetCookRegistry.cpp new file mode 100644 index 00000000..0adbb7f0 --- /dev/null +++ b/src/ClientLib/ClientAssetCookRegistry.cpp @@ -0,0 +1,121 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include + +namespace nlohmann +{ + template + void from_json(const BasicJsonType& j, Nz::Color& color) + { + j.at("r").get_to(color.r); + j.at("g").get_to(color.g); + j.at("b").get_to(color.b); + color.a = j.value("a", 1.0f); + } + + NLOHMANN_JSON_SERIALIZE_ENUM(tsom::ClientAssetCookRegistry::TextureType, { + {tsom::ClientAssetCookRegistry::TextureType::None, "none"}, + {tsom::ClientAssetCookRegistry::TextureType::BC1, "bc1"}, + {tsom::ClientAssetCookRegistry::TextureType::BC3, "bc3"}, + {tsom::ClientAssetCookRegistry::TextureType::BC4, "bc4"}, + {tsom::ClientAssetCookRegistry::TextureType::BC5, "bc5"}, + }) + + template + void from_json(const BasicJsonType& j, tsom::ClientAssetCookRegistry::Texture& texture) + { + j.at("path").get_to(texture.path); + j.at("type").get_to(texture.type); + } + + template + void to_json(BasicJsonType& j, const Nz::Color& color) + { + j = BasicJsonType{ + {"r", color.r}, + {"g", color.g}, + {"b", color.b} + }; + + if (Nz::NumberEquals(color.a, 1.0f)) + j["a"] = color.a; + } + + template + void to_json(BasicJsonType& j, const tsom::ClientAssetCookRegistry::Texture& texture) + { + j = BasicJsonType{ + {"path", texture.path}, + {"type", texture.type} + }; + } +} + +namespace tsom +{ + void ClientAssetCookRegistry::AddBlock(std::string blockName, BlockEntry blockEntry) + { + NazaraAssertMsg(!m_blockEntries.contains(blockName), "block %s is already present", blockName.c_str()); + m_blockEntries.emplace(std::move(blockName), std::move(blockEntry)); + } + + auto ClientAssetCookRegistry::GetBlock(std::string_view blockName) const -> const BlockEntry& + { + return Nz::Retrieve(m_blockEntries, blockName); + } + + bool ClientAssetCookRegistry::SaveToFile(const std::filesystem::path& path) const + { + nlohmann::ordered_json blockEntries; + for (const auto& [blockName, blockEntry] : m_blockEntries) + { + nlohmann::ordered_json blockEntryDoc; + blockEntryDoc["ambientOcclusionHeightTexture"] = blockEntry.ambientOcclusionHeightTexture; + blockEntryDoc["baseColorFallback"] = blockEntry.baseColorFallback; + blockEntryDoc["baseColorTexture"] = blockEntry.baseColorTexture; + blockEntryDoc["normalMapTexture"] = blockEntry.normalMapTexture; + blockEntryDoc["roughnessMetalnessTexture"] = blockEntry.roughnessMetalnessTexture; + + blockEntries[blockName] = std::move(blockEntryDoc); + } + + nlohmann::ordered_json doc; + doc["blocks"] = std::move(blockEntries); + + std::string content = doc.dump(1, '\t'); + + return Nz::File::WriteWhole(path, content.data(), content.size()); + } + + std::optional ClientAssetCookRegistry::LoadFromContent(std::string_view content) + { + nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); + + ClientAssetCookRegistry cookRegistry; + for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) + { + cookRegistry.AddBlock(blockName, BlockEntry { + .baseColorFallback = blockEntryDoc["baseColorFallback"], + .ambientOcclusionHeightTexture = blockEntryDoc["ambientOcclusionHeightTexture"], + .baseColorTexture = blockEntryDoc["baseColorTexture"], + .normalMapTexture = blockEntryDoc["normalMapTexture"], + .roughnessMetalnessTexture = blockEntryDoc["roughnessMetalnessTexture"], + }); + } + + return std::move(cookRegistry); + } + + std::optional ClientAssetCookRegistry::LoadFromFile(const std::filesystem::path& path) + { + std::optional> contentOpt = Nz::File::ReadWhole(path); + if (!contentOpt) + return std::nullopt; + + return LoadFromContent(std::string_view(reinterpret_cast(contentOpt->data()), contentOpt->size())); + } +} diff --git a/src/ClientLib/ClientAssetCooker.cpp b/src/ClientLib/ClientAssetCooker.cpp new file mode 100644 index 00000000..f4c50124 --- /dev/null +++ b/src/ClientLib/ClientAssetCooker.cpp @@ -0,0 +1,275 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + namespace + { + enum class TextureType + { + AmbientOcclusion, + BaseColor, + Height, + Metalness, + Normal, + Roughness, + + Max = Roughness + }; + + enum class CookedTextureType + { + BaseColor, + Normal, + MaterialData + }; + } + + Nz::Result ClientAssetCooker::Cook(ClientBlockLibrary& blockLibrary) + { + auto& fs = m_app.GetComponent(); + + const auto& blocks = blockLibrary.GetBlocks(); + + std::filesystem::path cacheDir = Nz::Utf8Path("cache"); + std::filesystem::path cookedAssetsDir = Nz::Utf8Path("CookedAssets"); + std::filesystem::path blockDir = Nz::Utf8Path("Blocks"); + + std::filesystem::path cookedAssetsPath = cacheDir / cookedAssetsDir; + std::filesystem::path cookedBlockPath = cookedAssetsPath / blockDir; + if (!std::filesystem::is_directory(cookedBlockPath)) + std::filesystem::create_directories(cookedBlockPath); + + constexpr Nz::UInt32 imageSize = 2048; + + ClientAssetCookRegistry registry; + + for (const ClientBlockLibrary::BlockData& blockData : blocks) + { + ClientAssetCookRegistry::BlockEntry blockEntry; + + Nz::EnumArray> streams; + streams[TextureType::BaseColor] = fs.GetFile(fmt::format("assets/{}.png", blockData.basePath)); + streams[TextureType::AmbientOcclusion] = fs.GetFile(fmt::format("assets/{}_ao.png", blockData.basePath)); + streams[TextureType::Height] = fs.GetFile(fmt::format("assets/{}_height.png", blockData.basePath)); + streams[TextureType::Metalness] = fs.GetFile(fmt::format("assets/{}_metallic.png", blockData.basePath)); + streams[TextureType::Normal] = fs.GetFile(fmt::format("assets/{}_normal.png", blockData.basePath)); + streams[TextureType::Roughness] = fs.GetFile(fmt::format("assets/{}_roughness.png", blockData.basePath)); + + // Handle color map + if (streams[TextureType::BaseColor]) + { + if (!streams[TextureType::BaseColor]) + return Nz::Err(fmt::format("failed to open {} base color", blockData.name)); + + std::shared_ptr baseColor = Nz::Image::LoadFromStream(*streams[TextureType::BaseColor], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::RGBA8 }); + if (!baseColor) + return Nz::Err(fmt::format("failed to load {} base color", blockData.name)); + + blockEntry.baseColorFallback = baseColor->ComputeAverageColor(); + spdlog::debug("{} base color map average color: {};{};{};{}", blockData.name, blockEntry.baseColorFallback.r, blockEntry.baseColorFallback.g, blockEntry.baseColorFallback.b, blockEntry.baseColorFallback.a); + + if (baseColor->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) + { + spdlog::warn("{} base color has an unexpected size", blockData.name); + baseColor->Resize(imageSize, imageSize); + } + + baseColor->GenerateMipmaps(); + + std::filesystem::path colorFilename = Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name)); + + Nz::Image compressedBaseColor; + if (baseColor->HasAlpha()) + { + // Compress using BC3 + // TODO: Detect 1bit alpha + spdlog::debug("{} base color map has alpha", blockData.name); + + compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC3(*baseColor); + blockEntry.baseColorTexture = { ClientAssetCookRegistry::TextureType::BC3, Nz::PathToString(blockDir / colorFilename) }; + } + else + { + // Compress using BC1 + spdlog::debug("{} base color map has no alpha", blockData.name); + + compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC1(*baseColor); + blockEntry.baseColorTexture = { ClientAssetCookRegistry::TextureType::BC1, Nz::PathToString(blockDir / colorFilename) }; + } + + std::filesystem::path targetBaseColorPath = cookedBlockPath / Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name)); + if (!compressedBaseColor.SaveToFile(cookedBlockPath / colorFilename)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetBaseColorPath))); + } + + // Handle normal maps + if (streams[TextureType::Normal]) + { + std::shared_ptr normalMap = Nz::Image::LoadFromStream(*streams[TextureType::Normal], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::RGBA8 }); + if (!normalMap) + return Nz::Err(fmt::format("failed to load {} normal map", blockData.name)); + + if (normalMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) + return Nz::Err(fmt::format("{} normal map has an unexpected size", blockData.name)); + + Nz::Image cookedNormalMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, imageSize, imageSize); + + const Nz::UInt8* sourcePixels = normalMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedNormalMap.GetPixels(); + for (std::size_t y = 0; y < imageSize; ++y) + { + for (std::size_t x = 0; x < imageSize; ++x) + { + if (sourcePixels[2] < 127) + spdlog::debug("{} normal map {};{} has Z value < 127: {}", blockData.name, x, y, sourcePixels[2]); + + cookedPixels[0] = sourcePixels[0]; + cookedPixels[1] = sourcePixels[1]; + + sourcePixels += 4; + cookedPixels += 2; + } + } + + cookedNormalMap.GenerateMipmaps(); + + cookedNormalMap = Nz::ImageCompressor::RG8ToBC5(cookedNormalMap); + + std::filesystem::path normalFilename = Nz::Utf8Path(fmt::format("{}_normal.dds", blockData.name)); + blockEntry.normalMapTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / normalFilename) }; + + std::filesystem::path targetPath = cookedBlockPath / normalFilename; + if (!cookedNormalMap.SaveToFile(targetPath)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); + } + + // Roughness/metalness + if (streams[TextureType::Roughness]) + { + std::shared_ptr roughnessMap = Nz::Image::LoadFromStream(*streams[TextureType::Roughness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!roughnessMap) + return Nz::Err(fmt::format("failed to load {} roughness map", blockData.name)); + + if (roughnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) + return Nz::Err(fmt::format("{} roughness map has an unexpected size", blockData.name)); + + std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness_metalness.dds", blockData.name)); + + std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; + + if (streams[TextureType::Metalness]) + { + // BC5 roughness/metalness + std::shared_ptr metalnessMap = Nz::Image::LoadFromStream(*streams[TextureType::Metalness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!metalnessMap) + return Nz::Err(fmt::format("failed to load {} metalness map", blockData.name)); + + if (metalnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) + return Nz::Err(fmt::format("{} metalness map has an unexpected size", blockData.name)); + + Nz::Image cookedRoughnessMetalnessMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, imageSize, imageSize); + + const Nz::UInt8* roughnessPixels = roughnessMap->GetConstPixels(); + const Nz::UInt8* metalnessPixels = metalnessMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedRoughnessMetalnessMap.GetPixels(); + for (std::size_t y = 0; y < imageSize; ++y) + { + for (std::size_t x = 0; x < imageSize; ++x) + { + cookedPixels[0] = *roughnessPixels++; + cookedPixels[1] = *metalnessPixels++; + + cookedPixels += 2; + } + } + + cookedRoughnessMetalnessMap.GenerateMipmaps(); + + cookedRoughnessMetalnessMap = Nz::ImageCompressor::RG8ToBC5(cookedRoughnessMetalnessMap); + blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; + + if (!cookedRoughnessMetalnessMap.SaveToFile(targetPath)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); + } + else + { + // BC4 roughness + Nz::Image cookedRoughnessMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, imageSize, imageSize); + + const Nz::UInt8* sourcePixels = roughnessMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedRoughnessMap.GetPixels(); + for (std::size_t y = 0; y < imageSize; ++y) + { + for (std::size_t x = 0; x < imageSize; ++x) + *cookedPixels++ = *sourcePixels++; + } + + cookedRoughnessMap.GenerateMipmaps(); + + cookedRoughnessMap = Nz::ImageCompressor::R8ToBC4(cookedRoughnessMap); + blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC4, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; + + if (!cookedRoughnessMap.SaveToFile(targetPath)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); + } + } + else if (streams[TextureType::Metalness]) + { + spdlog::warn("{} block has no roughness map but has a metalness map, this is unexpected, no roughness-metalness map will be cooked"); + } + + // Ambient Occlusion (+ Heightmap once used) + if (streams[TextureType::AmbientOcclusion]) + { + // BC4 + std::shared_ptr aoMap = Nz::Image::LoadFromStream(*streams[TextureType::AmbientOcclusion], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!aoMap) + return Nz::Err(fmt::format("failed to load {} ambient occlusion map", blockData.name)); + + if (aoMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) + return Nz::Err(fmt::format("{} ambient occlusion map has an unexpected size", blockData.name)); + + Nz::Image cookedAOMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, imageSize, imageSize); + + const Nz::UInt8* aoPixels = aoMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedAOMap.GetPixels(); + for (std::size_t y = 0; y < imageSize; ++y) + { + for (std::size_t x = 0; x < imageSize; ++x) + *cookedPixels++ = *aoPixels++; + } + + cookedAOMap = Nz::ImageCompressor::R8ToBC4(cookedAOMap); + + + std::filesystem::path aoHeightFilename = Nz::Utf8Path(fmt::format("{}_ao_height.dds", blockData.name)); + blockEntry.ambientOcclusionHeightTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / aoHeightFilename) }; + + std::filesystem::path targetPath = cookedBlockPath / aoHeightFilename; + if (!cookedAOMap.SaveToFile(targetPath)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); + } + + registry.AddBlock(blockData.name, std::move(blockEntry)); + } + + std::filesystem::path registryPath = cookedAssetsPath / Nz::Utf8Path("registry.json"); + if (!registry.SaveToFile(registryPath)) + return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(registryPath))); + + return Nz::Ok(); + } +} diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index d64af08a..dd890b55 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -3,51 +3,216 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include +#include #include #include #include #include +#include +#include +#include namespace tsom { - void ClientBlockLibrary::BuildTexture() + namespace { + enum class TextureType + { + AmbientOcclusion_Height, + BaseColor, + Normal, + Roughness_Metalness, + + Max = Roughness_Metalness + }; + + struct GlobalBlockBufferEntryOffsets + { + nzsl::FieldOffsets fieldOffsets; + std::size_t baseColorFallback; + std::size_t ambientOcclusionHeightMapIndex; + std::size_t baseColorMapIndex; + std::size_t metalness; + std::size_t normalMapIndex; + std::size_t roughness; + std::size_t roughnessMetalnessMapIndex; + }; + + struct GlobalBlockBufferOffsets + { + nzsl::FieldOffsets fieldOffsets; + std::size_t entries; + }; + + constexpr GlobalBlockBufferEntryOffsets BuildGlobalBlockBufferEntryOffsets() + { + GlobalBlockBufferEntryOffsets bufferOffsets { + nzsl::FieldOffsets(nzsl::StructLayout::Std430) + }; + + bufferOffsets.baseColorFallback = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float4); + bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.ambientOcclusionHeightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.roughnessMetalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.metalness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.roughness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + + return bufferOffsets; + } + + constexpr GlobalBlockBufferEntryOffsets s_blockBufferEntryOffsets = BuildGlobalBlockBufferEntryOffsets(); + + constexpr GlobalBlockBufferOffsets BuildGlobalBlockBufferOffsets() + { + GlobalBlockBufferOffsets bufferOffsets{ + nzsl::FieldOffsets(nzsl::StructLayout::Std430) + }; + + bufferOffsets.entries = bufferOffsets.fieldOffsets.AddStructArray(s_blockBufferEntryOffsets.fieldOffsets, 1); + + return bufferOffsets; + } + + constexpr GlobalBlockBufferOffsets s_blockBufferOffsets = BuildGlobalBlockBufferOffsets(); + } + + void ClientBlockLibrary::BuildTexture(Nz::RenderDevice& renderDevice) + { + std::size_t bufferSize = s_blockBufferOffsets.fieldOffsets.GetSize() * m_blocks.size(); + + m_globalBlockBuffer = renderDevice.InstantiateBuffer(bufferSize, Nz::BufferUsage::DeviceLocal | Nz::BufferUsage::StorageBuffer | Nz::BufferUsage::PersistentMapping); + m_globalBlockBufferPtr = m_globalBlockBuffer->Map(0, bufferSize); + auto& fs = m_applicationBase.GetComponent(); - std::size_t sliceCount = m_textureIndices.size() + 1; + std::optional cookRegistry; + fs.GetFileContent("CookedAssets/registry.json", [&](const void* ptr, Nz::UInt64 size) + { + cookRegistry = ClientAssetCookRegistry::LoadFromContent(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + return true; + }); + + if (!cookRegistry) + throw std::runtime_error("failed to load cook registry"); + + struct BlockTexture + { + Nz::EnumArray textureData; + }; + + Nz::UInt8* blockBufferPtr = static_cast(m_globalBlockBufferPtr) + s_blockBufferOffsets.entries; + + Nz::EnumArray textureCount; + textureCount.fill(0); + + Nz::UInt32 sliceCount = 0; + std::vector blockTextures; + blockTextures.reserve(m_blocks.size()); + + for (const BlockData& blockData : m_blocks) + { + const auto& cookedBlockData = cookRegistry->GetBlock(blockData.name); + + std::size_t blockIndex = blockTextures.size(); + auto& blockTexture = blockTextures.emplace_back(); + blockTexture.textureData[TextureType::AmbientOcclusion_Height] = &cookedBlockData.ambientOcclusionHeightTexture; + blockTexture.textureData[TextureType::BaseColor] = &cookedBlockData.baseColorTexture; + blockTexture.textureData[TextureType::Normal] = &cookedBlockData.normalMapTexture; + blockTexture.textureData[TextureType::Roughness_Metalness] = &cookedBlockData.roughnessMetalnessTexture; + + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorFallback) = cookedBlockData.baseColorFallback; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.normalMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.metalness) = blockData.metalness; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughness) = blockData.roughness; - constexpr std::size_t texSize = 256; // TODO: use texture size? + for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) + { + if (textureData->type != ClientAssetCookRegistry::TextureType::None) + textureCount[textureData->type]++; + } + } + + constexpr std::size_t texSize = 2048; // TODO: use texture size? + + constexpr Nz::EnumArray textureFormat = { + Nz::PixelFormat::BC1_RGBA_Unorm, + Nz::PixelFormat::BC3_Unorm, + Nz::PixelFormat::BC4_Unorm, + Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm + }; + + for (auto&& [type, sliceCount] : textureCount.iter_kv()) + { + if (sliceCount > 0) + { + m_blockTextures[type] = renderDevice.InstantiateTexture({ + .pixelFormat = textureFormat[type], + .type = Nz::ImageType::E2D_Array, + .layerCount = sliceCount, + .height = texSize, + .width = texSize + }); + } + } - Nz::Image baseColorArray(Nz::ImageType::E2D_Array, Nz::PixelFormat::RGBA8, texSize, texSize, sliceCount); - baseColorArray.Fill(Nz::Color::White()); + constexpr Nz::EnumArray textureSliceOffsets = { + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex, + s_blockBufferEntryOffsets.baseColorMapIndex, + s_blockBufferEntryOffsets.normalMapIndex, + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex + }; - Nz::Image normalArray(Nz::ImageType::E2D_Array, Nz::PixelFormat::RGBA8, texSize, texSize, sliceCount); - normalArray.Fill(Nz::Color(0.5f, 0.5f, 1.0f)); + Nz::EnumArray textureSlice; + textureSlice.fill(0); - Nz::Image detailArray(Nz::ImageType::E2D_Array, Nz::PixelFormat::RGBA8, texSize, texSize, sliceCount); - detailArray.Fill(Nz::Color(1.f, 0.f, 0.f, 1.f)); + auto UploadImage = [&](ClientAssetCookRegistry::TextureType textureType, Nz::UInt32 textureSlice, std::string_view filePath) + { + std::string assetPath = fmt::format("CookedAssets/{}", filePath); + + std::shared_ptr image = Nz::Image::LoadFromStream(*fs.GetFile(assetPath)); + if (!image) + return false; + + for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) + { + m_blockTextures[textureType]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) + { + std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); + return true; + }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(level), image->GetHeight(level), 1), level); + } - for (auto&& [texPath, texIndex] : m_textureIndices) + return true; + }; + + std::vector previewTextures(m_blocks.size()); + + Nz::UInt32 blockIndex = 0; + while (!blockTextures.empty()) { - Nz::ImageParams loadParams; - loadParams.loadFormat = Nz::PixelFormat::RGBA8; + const BlockTexture& blockTexture = blockTextures.front(); - std::shared_ptr baseColorImage = fs.Load("assets/" + texPath + ".png", loadParams); - if (baseColorImage) - baseColorArray.Copy(*baseColorImage, Nz::Boxui(baseColorImage->GetSize()), Nz::Vector3ui(0, 0, texIndex)); + for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) + { + if (textureData->type != ClientAssetCookRegistry::TextureType::None && + UploadImage(textureData->type, textureSlice[textureData->type], textureData->path)) + { + Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); + blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; - std::shared_ptr normalImage = fs.Load("assets/" + texPath + "_n.png", loadParams); - if (normalImage) - normalArray.Copy(*normalImage, Nz::Boxui(normalImage->GetSize()), Nz::Vector3ui(0, 0, texIndex)); + textureSlice[textureData->type]++; + } + } - std::shared_ptr detailImage = fs.Load("assets/" + texPath + "_s.png", loadParams); - if (detailImage) - detailArray.Copy(*detailImage, Nz::Boxui(detailImage->GetSize()), Nz::Vector3ui(0, 0, texIndex)); + blockTextures.erase(blockTextures.begin()); //< Destroy entry to free streams + blockIndex++; } - m_baseColorTexture = Nz::TextureAsset::CreateFromImage(std::move(baseColorArray), { .sRGB = true }); - m_normalTexture = Nz::TextureAsset::CreateFromImage(std::move(normalArray)); - m_detailTexture = Nz::TextureAsset::CreateFromImage(std::move(detailArray)); + /*m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); m_previewTextures.resize(m_blocks.size()); for (std::size_t blockIndex = 0; blockIndex < m_blocks.size(); ++blockIndex) @@ -56,10 +221,10 @@ namespace tsom Nz::TextureViewInfo slotTexView = { .viewType = Nz::ImageType::E2D, - .baseArrayLayer = blockData.texIndices[Direction::Up] + .baseArrayLayer = previewTextures[blockIndex] }; m_previewTextures[blockIndex] = Nz::TextureAsset::CreateView(m_baseColorTexture, slotTexView); - } + }*/ } } diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index 4b7538c0..c378ad64 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include +#include #include #include #include @@ -11,8 +12,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -21,7 +22,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -34,7 +37,65 @@ namespace tsom m_configFile(config), m_isCollisionGenerationEnabled(true) { - auto& filesystem = app.GetComponent(); + auto& clientAssets = app.GetComponent(); + + std::shared_ptr blockMaterial = clientAssets.QueryMaterial("Chunk"); + if (!blockMaterial) + { + auto& materialPassRegistry = Nz::Graphics::Instance()->GetMaterialPassRegistry(); + std::size_t depthPassIndex = materialPassRegistry.GetPassIndex("DepthPass"); + std::size_t shadowPassIndex = materialPassRegistry.GetPassIndex("ShadowPass"); + std::size_t distanceShadowPassIndex = materialPassRegistry.GetPassIndex("DistanceShadowPass"); + std::size_t forwardPassIndex = materialPassRegistry.GetPassIndex("ForwardPass"); + + Nz::MaterialSettings settings; + settings.AddValueProperty("BaseColor", Nz::Color::White()); + settings.AddValueProperty("AlphaTest", false); + settings.AddValueProperty("AlphaTestThreshold", 0.5f); + settings.AddValueProperty("ShadowMapNormalOffset", 0.f); + settings.AddValueProperty("ShadowPosScale", 1.f - 0.0025f); + settings.AddValueProperty("TriplanarOffset", Nz::Vector3f::Zero()); + settings.AddBufferProperty("GlobalBlockData"); + settings.AddTextureProperty("BlockTexture1", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture2", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture3", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture4", Nz::ImageType::E2D_Array); + + settings.AddPropertyHandler("GlobalBlockData"); + settings.AddPropertyHandler("AlphaTest"); + settings.AddPropertyHandler("BlockTexture1"); + settings.AddPropertyHandler("BlockTexture2"); + settings.AddPropertyHandler("BlockTexture3"); + settings.AddPropertyHandler("BlockTexture4"); + settings.AddPropertyHandler("BaseColor"); + settings.AddPropertyHandler("AlphaTestThreshold"); + settings.AddPropertyHandler("ShadowMapNormalOffset"); + settings.AddPropertyHandler("ShadowPosScale"); + + Nz::MaterialPass forwardPass; + forwardPass.states.depthBuffer = true; + forwardPass.states.depthCompare = Nz::RendererComparison::GreaterOrEqual; + forwardPass.shaders.push_back(std::make_shared(nzsl::ShaderStageType::Fragment | nzsl::ShaderStageType::Vertex, "TSOM.BlockPBR")); + settings.AddPass(forwardPassIndex, forwardPass); + + Nz::MaterialPass depthPass = forwardPass; + depthPass.options[nzsl::Ast::HashOption("DepthPass")] = true; + settings.AddPass(depthPassIndex, depthPass); + + Nz::MaterialPass shadowPass = depthPass; + shadowPass.options[nzsl::Ast::HashOption("ShadowPass")] = true; + shadowPass.states.depthCompare = Nz::RendererComparison::LessOrEqual; //< TODO: Reverse depth for shadow pass? + shadowPass.states.frontFace = Nz::FrontFace::Clockwise; + shadowPass.states.depthClamp = Nz::Graphics::Instance()->GetRenderDevice()->GetEnabledFeatures().depthClamping; + settings.AddPass(shadowPassIndex, shadowPass); + + Nz::MaterialPass distanceShadowPass = shadowPass; + distanceShadowPass.options[nzsl::Ast::HashOption("DistanceDepth")] = true; + settings.AddPass(distanceShadowPassIndex, distanceShadowPass); + + blockMaterial = std::make_shared(std::move(settings), "TSOM.BlockPBR"); + clientAssets.RegisterMaterial("Chunk", blockMaterial); + } Nz::TextureSamplerInfo blockSampler; blockSampler.anisotropyLevel = 16; @@ -43,69 +104,12 @@ namespace tsom blockSampler.wrapModeU = Nz::SamplerWrap::Repeat; blockSampler.wrapModeV = Nz::SamplerWrap::Repeat; - auto& materialPassRegistry = Nz::Graphics::Instance()->GetMaterialPassRegistry(); - std::size_t depthPassIndex = materialPassRegistry.GetPassIndex("DepthPass"); - std::size_t shadowPassIndex = materialPassRegistry.GetPassIndex("ShadowPass"); - std::size_t distanceShadowPassIndex = materialPassRegistry.GetPassIndex("DistanceShadowPass"); - std::size_t forwardPassIndex = materialPassRegistry.GetPassIndex("ForwardPass"); - - Nz::MaterialSettings settings; - settings.AddValueProperty("BaseColor", Nz::Color::White()); - settings.AddValueProperty("AlphaTest", false); - settings.AddValueProperty("AlphaTestThreshold", 0.5f); - settings.AddValueProperty("ShadowMapNormalOffset", 0.f); - settings.AddValueProperty("ShadowPosScale", 1.f - 0.0025f); - settings.AddTextureProperty("BaseColorMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("AlphaMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("DetailMap", Nz::ImageType::E2D_Array); - settings.AddPropertyHandler(std::make_unique("AlphaTest", "AlphaTest")); - settings.AddPropertyHandler(std::make_unique("BaseColorMap", "HasBaseColorTexture")); - settings.AddPropertyHandler(std::make_unique("AlphaMap", "HasAlphaTexture")); - settings.AddPropertyHandler(std::make_unique("BaseColor")); - settings.AddPropertyHandler(std::make_unique("AlphaTestThreshold")); - settings.AddPropertyHandler(std::make_unique("ShadowMapNormalOffset")); - settings.AddPropertyHandler(std::make_unique("ShadowPosScale")); - settings.AddTextureProperty("EmissiveMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("HeightMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("MetallicMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("NormalMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("RoughnessMap", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("SpecularMap", Nz::ImageType::E2D_Array); - settings.AddPropertyHandler(std::make_unique("DetailMap", "HasDetailTexture")); - settings.AddPropertyHandler(std::make_unique("EmissiveMap", "HasEmissiveTexture")); - settings.AddPropertyHandler(std::make_unique("HeightMap", "HasHeightTexture")); - settings.AddPropertyHandler(std::make_unique("MetallicMap", "HasMetallicTexture")); - settings.AddPropertyHandler(std::make_unique("NormalMap", "HasNormalTexture")); - settings.AddPropertyHandler(std::make_unique("RoughnessMap", "HasRoughnessTexture")); - settings.AddPropertyHandler(std::make_unique("SpecularMap", "HasSpecularTexture")); - - Nz::MaterialPass forwardPass; - forwardPass.states.depthBuffer = true; - forwardPass.states.depthCompare = Nz::RendererComparison::GreaterOrEqual; - forwardPass.shaders.push_back(std::make_shared(nzsl::ShaderStageType::Fragment | nzsl::ShaderStageType::Vertex, "TSOM.BlockPBR")); - settings.AddPass(forwardPassIndex, forwardPass); - - Nz::MaterialPass depthPass = forwardPass; - depthPass.options[nzsl::Ast::HashOption("DepthPass")] = true; - settings.AddPass(depthPassIndex, depthPass); - - Nz::MaterialPass shadowPass = depthPass; - shadowPass.options[nzsl::Ast::HashOption("ShadowPass")] = true; - shadowPass.states.depthCompare = Nz::RendererComparison::LessOrEqual; //< TODO: Reverse depth for shadow pass? - shadowPass.states.frontFace = Nz::FrontFace::Clockwise; - shadowPass.states.depthClamp = Nz::Graphics::Instance()->GetRenderDevice()->GetEnabledFeatures().depthClamping; - settings.AddPass(shadowPassIndex, shadowPass); - - Nz::MaterialPass distanceShadowPass = shadowPass; - distanceShadowPass.options[nzsl::Ast::HashOption("DistanceDepth")] = true; - settings.AddPass(distanceShadowPassIndex, distanceShadowPass); - - auto chunkMaterial = std::make_shared(std::move(settings), "TSOM.BlockPBR"); - - m_chunkMaterial = chunkMaterial->Instantiate(); - m_chunkMaterial->SetTextureProperty("BaseColorMap", blockLibrary.GetBaseColorTexture(), blockSampler); - m_chunkMaterial->SetTextureProperty("NormalMap", blockLibrary.GetNormalTexture(), blockSampler); - m_chunkMaterial->SetTextureProperty("DetailMap", blockLibrary.GetDetailTexture(), blockSampler); + m_chunkMaterial = blockMaterial->Instantiate(); + m_chunkMaterial->SetBufferProperty("GlobalBlockData", blockLibrary.GetGlobalBlockBuffer()); + m_chunkMaterial->SetTextureProperty("BlockTexture1", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1)), blockSampler); + //m_chunkMaterial->SetTextureProperty("BlockTexture2", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture3", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture4", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5)), blockSampler); m_chunkMaterial->SetValueProperty("ShadowPosScale", 1.f); m_chunkMaterial->SetValueProperty("AlphaTest", true); m_chunkMaterial->UpdatePassesStates({ "ShadowPass", "DistanceShadowPass" }, [](Nz::RenderStates& states) @@ -123,7 +127,7 @@ namespace tsom // VertexDeclaration auto NewDeclaration = [](Nz::VertexInputRate inputRate, std::initializer_list components) { - return std::make_shared(inputRate, std::move(components)); + return std::make_shared(inputRate, components); }; m_chunkVertexDeclaration = NewDeclaration(Nz::VertexInputRate::Vertex, { @@ -138,13 +142,8 @@ namespace tsom 0 }, { - Nz::VertexComponent::TexCoord, - Nz::ComponentType::Float3, - 0 - }, - { - Nz::VertexComponent::Tangent, - Nz::ComponentType::Float3, + Nz::VertexComponent::Userdata, + Nz::ComponentType::UInt1, 0 } }); @@ -170,8 +169,7 @@ namespace tsom vertices.resize(vertices.size() + 4); vertexAttributes.position = Nz::SparsePtr(&vertices[vertexAttributes.firstIndex].position, sizeof(vertices.front())); vertexAttributes.normal = Nz::SparsePtr(&vertices[vertexAttributes.firstIndex].normal, sizeof(vertices.front())); - vertexAttributes.tangent = Nz::SparsePtr(&vertices[vertexAttributes.firstIndex].tangent, sizeof(vertices.front())); - vertexAttributes.uv = Nz::SparsePtr(&vertices[vertexAttributes.firstIndex].uvw, sizeof(vertices.front())); + vertexAttributes.blockIndex = Nz::SparsePtr(&vertices[vertexAttributes.firstIndex].blockIndex, sizeof(vertices.front())); return vertexAttributes; }; @@ -195,36 +193,37 @@ namespace tsom Nz::SparsePtr normals = mapper.GetComponentPtr(Nz::VertexComponent::Normal); Nz::SparsePtr positions = mapper.GetComponentPtr(Nz::VertexComponent::Position); - // TODO: Replace by a vertex finder-like - std::map> posToVerts; - for (Nz::UInt32 i = 0; i < vertexCount; ++i) - { - Nz::Vector3i p = Nz::Vector3i(Nz::Vector3f::Apply(positions[i] * 100.f, std::roundf)); - posToVerts[p].push_back(i); - } + std::vector newNormals(vertexCount); + + Nz::SpatialSort spatialSort; + spatialSort.Append(positions, vertexCount); + + std::vector sortResult; float fLimit = smoothLimitAngle.GetCos(); - for (Nz::UInt32 i = 0; i < vertexCount; ++i) + for (Nz::UInt32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { - Nz::Vector3i p = Nz::Vector3i(Nz::Vector3f::Apply(positions[i] * 100.f, std::roundf)); + sortResult.clear(); + spatialSort.FindPositions(positions[vertexIndex], 0.01f, sortResult); - Nz::Vector3f vr = normals[i]; + Nz::Vector3f vertexNormal = normals[vertexIndex]; - auto& verticesFound = posToVerts[p]; - Nz::Vector3f pcNor; - for (Nz::UInt32 j : verticesFound) + Nz::Vector3f normalSum = vertexNormal; + for (Nz::UInt32 resultIndex : sortResult) { - Nz::Vector3f v = normals[j]; + if (vertexIndex == resultIndex) + continue; - // Check whether the angle between the two normals is not too large. - // Skip the angle check on our own normal to avoid false negatives - // (v*v is not guaranteed to be 1.0 for all unit vectors v) - if ((j == i || (Nz::Vector3f::DotProduct(v, vr) >= fLimit))) - pcNor += v; + Nz::Vector3f normal = normals[resultIndex]; + if (Nz::Vector3f::DotProduct(normal, vertexNormal) >= fLimit) + normalSum += normal; } - normals[i] = pcNor.Normalize(); + newNormals[vertexIndex] = normalSum.Normalize(); } + + for (Nz::UInt32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) + normals[vertexIndex] = newNormals[vertexIndex]; } std::shared_ptr chunkMesh = std::make_shared(); @@ -321,6 +320,7 @@ namespace tsom if (updateJob->cancelled) return; + // FIXME: If ClientChunkEntities is deleted before job finished, it can result in a crash ChunkReadLock lock(chunkPtr.get()); updateJob->mesh = BuildMesh(*chunkPtr); updateJob->jobDone++; diff --git a/src/ClientLib/ClientFramePipeline.cpp b/src/ClientLib/ClientFramePipeline.cpp new file mode 100644 index 00000000..7e5727ad --- /dev/null +++ b/src/ClientLib/ClientFramePipeline.cpp @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include + +namespace tsom +{ + void ClientFramePipeline::Render(Nz::RenderResources& renderResources) + { + DefaultFramePipeline::Render(renderResources); + } +} diff --git a/src/ClientLib/ClientSessionHandler.cpp b/src/ClientLib/ClientSessionHandler.cpp index bf83db0f..d64dd450 100644 --- a/src/ClientLib/ClientSessionHandler.cpp +++ b/src/ClientLib/ClientSessionHandler.cpp @@ -225,7 +225,7 @@ namespace tsom { for (auto entityId : entitiesDelete.entities) { - assert(m_entities[entityId]); + NazaraAssert(entityId < m_entities.size() && m_entities[entityId]); EntityData& entityData = *m_entities[entityId]; Packets::Helper::EnvironmentId environmentIndex = entityData.environmentIndex; assert(m_environments[environmentIndex]); @@ -245,7 +245,7 @@ namespace tsom { for (auto& entityStates : stateUpdate.entities) { - assert(m_entities[entityStates.entityId]); + NazaraAssert(entityStates.entityId < m_entities.size() && m_entities[entityStates.entityId]); EntityData& entityData = *m_entities[entityStates.entityId]; if (NetworkInterpolationComponent* movementInterpolation = entityData.entity.try_get()) @@ -273,15 +273,15 @@ namespace tsom void ClientSessionHandler::HandlePacket(Packets::S_EntityEnvironmentUpdate&& environmentUpdate) { - assert(m_entities[environmentUpdate.entity]); + NazaraAssert(environmentUpdate.entity < m_entities.size() && m_entities[environmentUpdate.entity]); EntityData& entityData = *m_entities[environmentUpdate.entity]; spdlog::info("Entity {} moved to environment #{} to environment #{}", environmentUpdate.entity, entityData.environmentIndex, environmentUpdate.newEnvironmentId); - assert(m_environments[entityData.environmentIndex]); + NazaraAssert(entityData.environmentIndex < m_environments.size() && m_environments[entityData.environmentIndex]); auto& oldEnvironment = *m_environments[entityData.environmentIndex]; oldEnvironment.entities.Reset(environmentUpdate.entity); - assert(m_environments[environmentUpdate.newEnvironmentId]); + NazaraAssert(environmentUpdate.newEnvironmentId < m_environments.size() && m_environments[environmentUpdate.newEnvironmentId]); auto& newEnvironment = *m_environments[environmentUpdate.newEnvironmentId]; newEnvironment.entities.UnboundedSet(environmentUpdate.entity); @@ -302,7 +302,7 @@ namespace tsom void ClientSessionHandler::HandlePacket(Packets::S_EntityProcedureCall&& procedureCall) { - assert(m_entities[procedureCall.entity]); + NazaraAssert(procedureCall.entity < m_entities.size() && m_entities[procedureCall.entity]); EntityData& entityData = *m_entities[procedureCall.entity]; auto& classInstance = entityData.entity.get(); @@ -544,7 +544,7 @@ namespace tsom else if (ShipComponent* shipComponent = entity.try_get()) environment.gravityController = shipComponent->ship.get(); - spdlog::info("Created entity {} in environment {} ({})", entityData.entityId, entityData.environmentId, entityClassName); + spdlog::info("Created entity {} in environment {} ({}), visual id: {}", entityData.entityId, entityData.environmentId, entityClassName, (std::uint32_t) visualEntity.entity()); // Since we make use of parenting for environments, we need to make replication happen in global space if (Nz::RigidBody3DComponent* rigidBody = entity.try_get()) diff --git a/src/ClientLib/Rendering/AtmosphereScatteringPipelinePass.cpp b/src/ClientLib/Rendering/AtmosphereScatteringPipelinePass.cpp index ce77a02b..9dd217db 100644 --- a/src/ClientLib/Rendering/AtmosphereScatteringPipelinePass.cpp +++ b/src/ClientLib/Rendering/AtmosphereScatteringPipelinePass.cpp @@ -224,7 +224,7 @@ namespace tsom frameData.renderResources.Execute([&](Nz::CommandBufferBuilder& builder) { builder.CopyBuffer(allocation, renderBufferView); - builder.MemoryBarrier(Nz::PipelineStage::Transfer, Nz::PipelineStage::FragmentShader, Nz::MemoryAccess::TransferWrite, Nz::MemoryAccess::UniformBufferRead); + builder.MemoryBarrier({ .srcStageMask = Nz::PipelineStage::Transfer, .dstStageMask = Nz::PipelineStage::FragmentShader, .srcAccessMask = Nz::MemoryAccess::TransferWrite, .dstAccessMask = Nz::MemoryAccess::UniformBufferRead }); }, Nz::QueueType::Transfer); frameData.renderResources.PushReleaseCallback([pool = m_passDataBufferPool, bufferIndex] diff --git a/src/CommonLib/BlockLibrary.cpp b/src/CommonLib/BlockLibrary.cpp index ec25bfbd..57a915a9 100644 --- a/src/CommonLib/BlockLibrary.cpp +++ b/src/CommonLib/BlockLibrary.cpp @@ -31,12 +31,7 @@ namespace tsom }); RegisterBlock("debug", { - .baseBackPath = "blocks/debug_back", - .baseDownPath = "blocks/debug_down", - .baseFrontPath = "blocks/debug_front", - .baseLeftPath = "blocks/debug_left", - .baseRightPath = "blocks/debug_right", - .baseUpPath = "blocks/debug_up", + .basePath = "blocks/debug_up", }); RegisterBlock("dirt", { @@ -47,9 +42,7 @@ namespace tsom }); RegisterBlock("grass", { - .basePath = "blocks/grass_top", - .baseDownPath = "blocks/dirt", - .baseSidePath = "blocks/grass_side", + .basePath = "blocks/grass", .isSmooth = true, .density = 2.0f, .permeability = 0.1f @@ -59,10 +52,6 @@ namespace tsom .basePath = "blocks/smooth_stone" }); - RegisterBlock("hull2", { - .basePath = "blocks/smooth_stone_slab_side" - }); - RegisterBlock("snow", { .basePath = "blocks/snow", .isSmooth = true, @@ -112,6 +101,49 @@ namespace tsom .isSmooth = true, .isTransparent = true, }); + + RegisterBlock("bark", { + .basePath = "blocks/bark", + .isSmooth = true + }); + + RegisterBlock("cliff_rocks", { + .basePath = "blocks/cliff_rocks", + .isSmooth = true + }); + + RegisterBlock("rock", { + .basePath = "blocks/rock", + .isSmooth = true + }); + + RegisterBlock("wood_floor", { + .basePath = "blocks/wood_floor", + }); + + RegisterBlock("white_bricks", { + .basePath = "blocks/white_bricks", + }); + + RegisterBlock("gold", { + .basePath = "blocks/gold", + }); + + RegisterBlock("metal", { + .basePath = "blocks/metal", + }); + + RegisterBlock("metal_plates", { + .basePath = "blocks/metal_plates", + }); + + RegisterBlock("brickswall", { + .basePath = "blocks/brickswall", + }); + + RegisterBlock("floor_tiles", { + .basePath = "blocks/floor_tiles", + }); } BlockIndex BlockLibrary::RegisterBlock(std::string name, BlockInfo blockInfo) @@ -124,44 +156,16 @@ namespace tsom blockData.isTransparent = blockInfo.isTransparent; blockData.isSmooth = blockInfo.isSmooth; blockData.density = blockInfo.density; + blockData.metalness = blockInfo.metalness; blockData.permeability = blockInfo.permeability; + blockData.roughness = blockInfo.roughness; blockData.name = name; + blockData.basePath = std::move(blockInfo.basePath); auto it = m_layerIndices.find(blockInfo.layerName); NazaraAssertMsg(it != m_layerIndices.end(), "Invalid layer %s", blockInfo.layerName.data()); blockData.layerIndex = Nz::SafeCaster(it->second); - unsigned int baseTexIndex; - if (!blockInfo.basePath.empty()) - baseTexIndex = RegisterTexture(std::move(blockInfo.basePath)); - else - baseTexIndex = 0; - - unsigned int baseSideTexIndex; - if (!blockInfo.baseSidePath.empty()) - baseSideTexIndex = RegisterTexture(std::move(blockInfo.baseSidePath)); - else - baseSideTexIndex = baseTexIndex; - - Nz::EnumArray dirToStr = { - &blockInfo.baseBackPath, //< Back - &blockInfo.baseDownPath, //< Down - &blockInfo.baseFrontPath, //< Front - &blockInfo.baseLeftPath, //< Left - &blockInfo.baseRightPath, //< Right - &blockInfo.baseUpPath, //< Up - }; - - for (auto&& [dir, str] : dirToStr.iter_kv()) - { - if (!str->empty()) - blockData.texIndices[dir] = RegisterTexture(std::move(*str)); - else if (dir != Direction::Up && dir != Direction::Down) - blockData.texIndices[dir] = baseSideTexIndex; - else - blockData.texIndices[dir] = baseTexIndex; - } - assert(!m_blockIndices.contains(name)); m_blockIndices.emplace(std::move(name), blockIndex); @@ -185,16 +189,4 @@ namespace tsom return layerIndex; } - - unsigned int BlockLibrary::RegisterTexture(std::string&& texturePath) - { - auto it = m_textureIndices.find(texturePath); - if (it != m_textureIndices.end()) - return it->second; - - unsigned int texIndex = Nz::SafeCast(m_textureIndices.size() + 1); // Keep slice #0 for empty - m_textureIndices.emplace(std::move(texturePath), texIndex); - - return texIndex; - } } diff --git a/src/CommonLib/Chunk.cpp b/src/CommonLib/Chunk.cpp index d3119598..cdc7ec85 100644 --- a/src/CommonLib/Chunk.cpp +++ b/src/CommonLib/Chunk.cpp @@ -45,77 +45,10 @@ namespace tsom vertexAttributes.normal[i] = faceDirection; } - if (vertexAttributes.tangent) + if (vertexAttributes.blockIndex) { - Nz::Vector3f edgeCenter = (pos[0] + pos[1]) * 0.5f; - Nz::Vector3f tangent = Nz::Vector3f::Normalize(edgeCenter - faceCenter); - for (std::size_t i = 0; i < pos.size(); ++i) - vertexAttributes.tangent[i] = tangent; - } - - if (vertexAttributes.uv) - { - Nz::Vector3f faceUp = s_dirNormals[DirectionFromNormal(Nz::Vector3f::Normalize(faceCenter - gravityCenter))]; - - // Make up the rotation from the face up to the regular up - Nz::Quaternionf upRotation = Nz::Quaternionf::RotationBetween(faceUp, Nz::Vector3f::Up()); - - // Compute texture direction based on face direction in regular orientation - Direction texDirection = DirectionFromNormal(upRotation * faceDirection); - - const auto& blockData = m_blockLibrary.GetBlockData(blockContent); - std::size_t textureIndex = blockData.texIndices[texDirection]; - - // Compute UV - float sliceIndex = textureIndex; - for (std::size_t i = 0; i < pos.size(); ++i) - { - // Get vector from center to corner (no need to normalize) and use it to compute UV - // This is similar to the way a GPU compute UV when sampling a cubemap: https://www.gamedev.net/forums/topic/687535-implementing-a-cube-map-lookup-function/5337472/ - Nz::Vector3f dir = upRotation * (pos[i] - blockCenter); - Nz::Vector3f dirAbs = dir.GetAbs(); - - float mag = 0.f; - Nz::Vector2f uv; - switch (texDirection) //< TODO: texture direction should be defined by dir to handle corners - { - case Direction::Back: - case Direction::Front: - { - mag = 0.5f / dirAbs.x; - uv = { dir.x < 0.f ? -dir.z : dir.z, -dir.y }; - break; - } - - case Direction::Down: - case Direction::Up: - { - mag = 0.5f / dirAbs.y; - uv = { dir.x, dir.y < 0.f ? -dir.z : dir.z }; - break; - } - - case Direction::Left: - case Direction::Right: - { - mag = 0.5f / dirAbs.z; - uv = { dir.z < 0.f ? dir.x : -dir.x, -dir.y }; - break; - } - } - - vertexAttributes.uv[i] = Nz::Vector3f(uv * mag + Nz::Vector2f(0.5f), sliceIndex); - } - } - - // deform positions after generating UV - if (DeformPositions(vertexAttributes.position, pos.size())) - { - if (vertexAttributes.normal && vertexAttributes.tangent) - DeformNormalsAndTangents(vertexAttributes.normal, vertexAttributes.tangent, faceDirection, vertexAttributes.position, pos.size()); - else if (vertexAttributes.normal) - DeformNormals(vertexAttributes.normal, faceDirection, vertexAttributes.position, pos.size()); + vertexAttributes.blockIndex[i] = blockContent; } }; @@ -270,22 +203,6 @@ namespace tsom return box.GetCorners(); } - void Chunk::DeformNormals(Nz::SparsePtr normals, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const - { - /* nothing to do */ - } - - void Chunk::DeformNormalsAndTangents(Nz::SparsePtr normals, Nz::SparsePtr tangents, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const - { - /* nothing to do */ - } - - bool Chunk::DeformPositions(Nz::SparsePtr /*positions*/, std::size_t /*positionCount*/) const - { - /* nothing to do */ - return false; - } - void Chunk::Deserialize(Nz::ByteStream& byteStream) { Nz::UInt32 chunkBinaryVersion; diff --git a/src/CommonLib/DeformedChunk.cpp b/src/CommonLib/DeformedChunk.cpp deleted file mode 100644 index e4fa8819..00000000 --- a/src/CommonLib/DeformedChunk.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) -// This file is part of the "This Space Of Mine" project -// For conditions of distribution and use, see copyright notice in LICENSE - -#include -#include -#include - -namespace tsom -{ - std::pair, Nz::Vector3f> DeformedChunk::BuildBlockCollider(const Nz::Vector3ui& blockIndices, float scale) const - { - auto corners = ComputeBlockCorners(blockIndices); - Nz::Vector3f blockCenter = std::accumulate(corners.begin(), corners.end(), Nz::Vector3f::Zero()) / corners.size(); - - for (Nz::Vector3f& corner : corners) - corner = (corner - blockCenter) * scale + blockCenter; - - return { std::make_shared(corners.data(), corners.size()), blockCenter }; - } - - std::shared_ptr DeformedChunk::BuildCollider(std::size_t layerIndex) const - { - std::vector indices; - std::vector positions; - std::vector triangleUserdata; - - auto AddVertices = [&](const Nz::Vector3ui& blockIndices, Direction direction) - { - VertexAttributes vertexAttributes; - - vertexAttributes.firstIndex = Nz::SafeCast(positions.size()); - positions.resize(positions.size() + 4); - vertexAttributes.position = Nz::SparsePtr(&positions[vertexAttributes.firstIndex]); - - Nz::UInt32 localBlockIndex = GetBlockLocalIndex(blockIndices) * 6 + static_cast(direction); - triangleUserdata.push_back(localBlockIndex); - triangleUserdata.push_back(localBlockIndex); - - return vertexAttributes; - }; - - BuildMesh(layerIndex, indices, m_deformationCenter, AddVertices); - if (indices.empty()) - return nullptr; - - Nz::MeshCollider3D::Settings meshSettings; - meshSettings.indexCount = indices.size(); - meshSettings.indices = indices.data(); - meshSettings.vertexCount = positions.size(); - meshSettings.vertices = &positions[0]; - meshSettings.triangleUserdata = &triangleUserdata[0]; - - return std::make_shared(meshSettings); - } - - std::optional DeformedChunk::ComputeHitCoordinates(const Nz::Vector3f& hitPos, const Nz::Vector3f& hitNormal, const Nz::Collider3D& collider, std::uint32_t hitSubshapeId) const - { - std::uint32_t remainder; - const Nz::Collider3D* subCollider = collider.GetSubCollider(hitSubshapeId, remainder); - if (!subCollider) - return std::nullopt; - - Nz::UInt32 userdata = SafeCast(subCollider)->GetTriangleUserData(remainder); - - return HitBlock { - .direction = static_cast(userdata % 6), - .blockIndices = GetBlockLocalIndices(userdata / 6) - }; - } - - Nz::EnumArray DeformedChunk::ComputeBlockCorners(const Nz::Vector3ui& indices) const - { - Nz::EnumArray corners = Chunk::ComputeBlockCorners(indices); - for (auto& position : corners) - position = DeformPosition(position, m_deformationCenter, m_deformationRadius); - - return corners; - } - - void DeformedChunk::DeformNormals(Nz::SparsePtr normals, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const - { - for (std::size_t i = 0; i < vertexCount; ++i) - { - Nz::Quaternionf rotation = GetNormalDeformation(positions[i], referenceNormal, m_deformationCenter, m_deformationRadius); - normals[i] = rotation * normals[i]; - } - } - - void DeformedChunk::DeformNormalsAndTangents(Nz::SparsePtr normals, Nz::SparsePtr tangents, const Nz::Vector3f& referenceNormal, Nz::SparsePtr positions, std::size_t vertexCount) const - { - for (std::size_t i = 0; i < vertexCount; ++i) - { - Nz::Quaternionf rotation = GetNormalDeformation(positions[i], referenceNormal, m_deformationCenter, m_deformationRadius); - normals[i] = rotation * normals[i]; - tangents[i] = rotation * tangents[i]; - } - } - - bool DeformedChunk::DeformPositions(Nz::SparsePtr positions, std::size_t positionCount) const - { - for (std::size_t i = 0; i < positionCount; ++i) - positions[i] = DeformPosition(positions[i], m_deformationCenter, m_deformationRadius); - - return true; - } -} diff --git a/src/CommonLib/NetworkReactor.cpp b/src/CommonLib/NetworkReactor.cpp index 8e84d7f3..716549d0 100644 --- a/src/CommonLib/NetworkReactor.cpp +++ b/src/CommonLib/NetworkReactor.cpp @@ -34,11 +34,11 @@ namespace tsom m_thread.join(); } - std::size_t NetworkReactor::ConnectTo(Nz::IpAddress address, Nz::UInt32 data) + std::size_t NetworkReactor::ConnectTo(const Nz::IpAddress& address, Nz::UInt32 data) { ConnectionRequest request; request.data = data; - request.remoteAddress = std::move(address); + request.remoteAddress = address; // We will need a few synchronization primitives to block the calling thread until the reactor has treated our request std::size_t newClientId = InvalidPeerId; diff --git a/src/CommonLib/Planet.cpp b/src/CommonLib/Planet.cpp index a90d1470..e4847071 100644 --- a/src/CommonLib/Planet.cpp +++ b/src/CommonLib/Planet.cpp @@ -141,9 +141,9 @@ namespace tsom { constexpr float PlanetGravityCenterStartDecrease = 16.f; constexpr float PlanetGravityCenterNoGravity = 4.f; - constexpr float PlanetGravitySpaceStart = 100.f; - constexpr float PlanetGravitySpaceFinish = 150.f; - constexpr float PlanetGravitySpaceNone = 350.f; + constexpr float PlanetGravitySpaceStart = 200.f; + constexpr float PlanetGravitySpaceFinish = 300.f; + constexpr float PlanetGravitySpaceNone = 500.f; // Decrease gravity near the center float distSq = position.SquaredDistance(GetCenter()); @@ -244,19 +244,26 @@ namespace tsom ChunkIndices chunkIndices = chunk.GetIndices(); bool created; - ScriptingContext& scriptingContext = m_scriptingContexts.GetOrCreate(created, m_app); + ChunkGenerator& chunkGenerator = m_chunkGenerators.GetOrCreate(created, m_app); if (created) { - scriptingContext.RegisterLibrary(); - scriptingContext.RegisterLibrary(); + chunkGenerator.scriptingContext.RegisterLibrary(); + chunkGenerator.scriptingContext.RegisterLibrary(); + + Nz::Result execResult = chunkGenerator.scriptingContext.LoadFile(fmt::format("scripts/planets/{}.lua", scriptName)); + if (!execResult) + return; + + chunkGenerator.generationFunction = execResult.GetValue(); } - Nz::Result execResult = scriptingContext.LoadFile(fmt::format("scripts/planets/{}.lua", scriptName)); - if (!execResult) - return; + Nz::Time t1 = Nz::GetElapsedNanoseconds(); + Nz::Time t2 = Nz::GetElapsedNanoseconds(); + + Nz::Time t3 = Nz::GetElapsedNanoseconds(); + auto result = chunkGenerator.generationFunction(chunk, seed, chunkCount); + Nz::Time t4 = Nz::GetElapsedNanoseconds(); - sol::protected_function generationFunction = execResult.GetValue(); - auto result = generationFunction(chunk, seed, chunkCount); if (!result.valid()) { sol::error err = result; @@ -273,6 +280,8 @@ namespace tsom auto& blockLibrary = chunk.GetBlockLibrary(); + Nz::Time t5 = Nz::GetElapsedNanoseconds(); + std::vector blocks(blockCount, EmptyBlockIndex); std::size_t maxEntries = std::min(blockCount, contentSize); for (std::size_t i = 0; i < maxEntries; ++i) @@ -287,11 +296,153 @@ namespace tsom blocks[i] = blockIndex; } + Nz::Time t6 = Nz::GetElapsedNanoseconds(); + ChunkWriteLock lock(&chunk); chunk.Reset([&](BlockIndex* blockIndices) { std::memcpy(blockIndices, blocks.data(), blockCount * sizeof(BlockIndex)); }); + + Nz::Time t7 = Nz::GetElapsedNanoseconds(); + + static std::atomic_int64_t counter = 0; + std::atomic_int64_t iterCount = ++counter; + + static std::atomic_int64_t accFile = 0; + std::atomic_int64_t a1 = accFile.fetch_add((t2 - t1).AsMicroseconds()); + + static std::atomic_int64_t accLua = 0; + std::atomic_int64_t a2 = accLua.fetch_add((t4 - t3).AsMicroseconds()); + + static std::atomic_int64_t accConvert = 0; + std::atomic_int64_t a3 = accConvert.fetch_add((t6 - t5).AsMicroseconds()); + + static std::atomic_int64_t accChunk = 0; + std::atomic_int64_t a4 = accChunk.fetch_add((t7 - t6).AsMicroseconds()); + + static std::atomic_int64_t accTotal = 0; + std::atomic_int64_t a5 = accTotal.fetch_add((t7 - t1).AsMicroseconds()); + + fmt::print("Total: {}us (load file: {}us, lua: {}us ({}), convert: {}us, chunk: {}us)\n", a5 / iterCount, a1 / iterCount, a2 / iterCount, (t4 - t3).AsMicroseconds(), a3 / iterCount, a4 / iterCount); + } + + void Planet::GenerateChunkNative(Chunk& chunk, Nz::UInt32 seed, const Nz::Vector3ui& chunkCount, std::string_view scriptName) + { +#if 0 + siv::PerlinNoise perlinNoise(seed); + auto& blockLibrary = chunk.GetBlockLibrary(); + + float minGrenerationFreeHeight = 0; + float baseFreeHeight = 30; + + float blockSize = chunk.GetBlockSize(); + float maxHeight = (chunk.GetSize() * chunkCount.x)/2 * blockSize; + float maxGenerationHeight = maxHeight - minGrenerationFreeHeight; + float baseHeight = maxHeight - baseFreeHeight; + + float terrainVariation1Scale = 0.06 * baseHeight; + float terrainVariation2Scale = 0.16 * baseHeight; + float moutainScale = 0.035 * baseHeight; + float spikeScale = 0.2 * baseHeight; + float caveScale = 0.06; + + std::size_t blockCount = chunk.GetBlockCount(); + std::vector blocks(blockCount, EmptyBlockIndex); + + std::size_t i = 0; + for (std::size_t z = 0; z < ChunkSize; ++z) + { + for (std::size_t y = 0; y < ChunkSize; ++y) + { + for (std::size_t x = 0; x < ChunkSize; ++x) + { + BlockIndices blockPos = GetBlockIndices(chunk.GetIndices(), { x, y, z }); + Nz::Vector3f blockPosScaled(blockPos); + blockPosScaled *= 0.5f; + + Nz::Vector3f blockPosNorm = blockPosScaled.GetNormal(); + float distToCenter = sdRoundBox(blockPosScaled, Nz::Vector3f(baseHeight), 16.0); + + //blocks[i]; + } + } + } + for z = 0, chunksize - 1 do + for y = 0, chunksize - 1 do + for x = 0, chunksize - 1 do + local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) + local blockPosScaled = Vec3f(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosNorm, distToCenter = blockPosScaled:GetNormal() + --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) + distToCenter = SignedDistance.RoundBox(blockPosScaled, Vec3f(baseHeight), 16.0) + + if distToCenter > baseFreeHeight then + table.insert(content, emptyBlock) + goto continue + end + + local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) + + if distToCenter <= -32.0 then + if blockPresence >= 0.3 and blockPresence <= 0.7 then + if distToCenter <= -5 then + table.insert(content, stoneBlock) + else + table.insert(content, dirtBlock) + end + else + table.insert(content, stoneBlock) + end + else + local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) + local mountainous + if baseMountainous < 0.6 then + mountainous = 0 + elseif baseMountainous < 0.8 then + mountainous = 5*baseMountainous-3 + else + mountainous = 1 + end + + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) + local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) + + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) + local spikeHeight + if baseSpikeHeight < 0.7 then + spikeHeight = 0 + elseif baseSpikeHeight < 0.9 then + spikeHeight = 5*baseSpikeHeight-3.5 + else + spikeHeight = 1 + end + spikeHeight = (1-mountainous) * spikeHeight * 20 + + local height = heightVariation1 + heightVariation2 + spikeHeight + + if distToCenter <= height then + if distToCenter >= height - spikeHeight then + table.insert(content, stoneMossyBlock) + elseif mountainous > 0.5 and heightVariation2 > 0.5 then + table.insert(content, snowBlock) + elseif mountainous > 0.1 then + table.insert(content, stoneBlock) + elseif baseMountainous < 0.4 then + table.insert(content, grassBlock) + else + table.insert(content, dirtBlock) + end + else + table.insert(content, emptyBlock) + end + end + + ::continue:: + end + end + end +#endif } void Planet::GenerateChunks(Nz::TaskScheduler& taskScheduler, Nz::UInt32 seed, const Nz::Vector3ui& chunkCount, std::string_view scriptName) diff --git a/src/CommonLib/Scripting/ChunkScriptingLibrary.cpp b/src/CommonLib/Scripting/ChunkScriptingLibrary.cpp index bbddf800..ff48dcc2 100644 --- a/src/CommonLib/Scripting/ChunkScriptingLibrary.cpp +++ b/src/CommonLib/Scripting/ChunkScriptingLibrary.cpp @@ -6,8 +6,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -16,9 +16,9 @@ #include #include -SOL_BASE_CLASSES(tsom::DeformedChunk, tsom::Chunk); +SOL_BASE_CLASSES(tsom::SurfaceNetsChunk, tsom::Chunk); SOL_BASE_CLASSES(tsom::FlatChunk, tsom::Chunk); -SOL_DERIVED_CLASSES(tsom::Chunk, tsom::DeformedChunk, tsom::FlatChunk); +SOL_DERIVED_CLASSES(tsom::Chunk, tsom::SurfaceNetsChunk, tsom::FlatChunk); namespace tsom { @@ -79,6 +79,7 @@ namespace tsom ), "GetBlockLocalIndex", LuaFunction(&Chunk::GetBlockLocalIndex), "GetBlockLocalIndices", LuaFunction(&Chunk::GetBlockLocalIndices), + "GetBlockSize", LuaFunction(&Chunk::GetBlockSize), "GetContainer", LuaFunction([](Chunk& chunk) { return &chunk.GetContainer(); diff --git a/src/CommonLib/Scripting/MathScriptingLibrary.cpp b/src/CommonLib/Scripting/MathScriptingLibrary.cpp index bf97e5b9..4b2005fa 100644 --- a/src/CommonLib/Scripting/MathScriptingLibrary.cpp +++ b/src/CommonLib/Scripting/MathScriptingLibrary.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include namespace tsom @@ -20,6 +22,22 @@ namespace tsom void MathScriptingLibrary::Register(sol::state& state) { state["DirectionFromNormal"] = &DirectionFromNormal; + state["CreateMetatable"] = [](sol::this_state L, const char* metaname) + { + if (luaL_newmetatable(L, metaname) == 0) + { + lua_pop(L, 1); + TriggerLuaArgError(L, 1, fmt::format("Metatable %s already exists", metaname)); + } + + return sol::stack_table(L); + }; + + state["GetMetatable"]= [](sol::this_state L, const char* metaname) + { + luaL_getmetatable(L, metaname); + return sol::stack_table(L); + }; RegisterBox(state, "Boxf"); RegisterBox(state, "Boxi"); @@ -28,13 +46,14 @@ namespace tsom RegisterEulerAngles(state, "EulerAnglesf"); RegisterPerlinNoise(state); RegisterQuaternion(state, "Quaternionf"); + RegisterSignedDistance(state); RegisterTime(state); - RegisterVector2(state, "Vec2f"); + /*RegisterVector2(state, "Vec2f"); RegisterVector2(state, "Vec2i"); RegisterVector2(state, "Vec2ui"); RegisterVector3(state, "Vec3f"); RegisterVector3(state, "Vec3i"); - RegisterVector3(state, "Vec3ui"); + RegisterVector3(state, "Vec3ui");*/ } template @@ -132,6 +151,14 @@ namespace tsom ); } + void MathScriptingLibrary::RegisterSignedDistance(sol::state& state) + { + state.create_named_table("SignedDistance", + "RoundBox", &sdRoundBox, + "Torus", &sdTorus + ); + } + void MathScriptingLibrary::RegisterTime(sol::state& state) { state.new_usertype("Time", diff --git a/src/CommonLib/Scripting/ScriptingContext.cpp b/src/CommonLib/Scripting/ScriptingContext.cpp index 2b9f1c4c..301666f9 100644 --- a/src/CommonLib/Scripting/ScriptingContext.cpp +++ b/src/CommonLib/Scripting/ScriptingContext.cpp @@ -9,6 +9,14 @@ #include #include +#ifndef LUAI_UACINT +#define LUAI_UACINT LUA_INTFRM_T +#endif + +#ifndef LUA_INTEGER_FMT +#define LUA_INTEGER_FMT LUA_INTFRMLEN +#endif + namespace tsom { namespace @@ -34,9 +42,9 @@ namespace tsom case LUA_TNUMBER: if (lua_isinteger(L, idx)) - lua_pushfstring(L, "%I", (LUAI_UACINT)lua_tointeger(L, idx)); + lua_pushfstring(L, LUA_INTEGER_FMT, (LUAI_UACINT)lua_tointeger(L, idx)); else - lua_pushfstring(L, "%f", (LUAI_UACNUMBER)lua_tonumber(L, idx)); + lua_pushfstring(L, LUA_NUMBER_FMT, (LUAI_UACNUMBER)lua_tonumber(L, idx)); break; case LUA_TBOOLEAN: diff --git a/src/CommonLib/Scripting/ScriptingUtils.cpp b/src/CommonLib/Scripting/ScriptingUtils.cpp index 9923e8c0..9b105195 100644 --- a/src/CommonLib/Scripting/ScriptingUtils.cpp +++ b/src/CommonLib/Scripting/ScriptingUtils.cpp @@ -30,18 +30,18 @@ namespace tsom [[noreturn]] void TriggerLuaError(lua_State* L, const std::string& errMessage) { luaL_error(L, errMessage.c_str()); - std::abort(); + std::abort(); //< shouldn't be called since luaL_error will trigger a Lua error } [[noreturn]] void TriggerLuaArgError(lua_State* L, int argIndex, const char* errMessage) { luaL_argerror(L, argIndex, errMessage); - std::abort(); + std::abort(); //< shouldn't be called since luaL_error will trigger a Lua error } [[noreturn]] void TriggerLuaArgError(lua_State* L, int argIndex, const std::string& errMessage) { luaL_argerror(L, argIndex, errMessage.c_str()); - std::abort(); + std::abort(); //< shouldn't be called since luaL_error will trigger a Lua error } } diff --git a/src/CommonLib/SurfaceNetsChunk.cpp b/src/CommonLib/SurfaceNetsChunk.cpp index 736b5c06..59a1e12f 100644 --- a/src/CommonLib/SurfaceNetsChunk.cpp +++ b/src/CommonLib/SurfaceNetsChunk.cpp @@ -302,7 +302,12 @@ namespace tsom Nz::Vector3i edgeNeighborPos = s_voxelQuads[axis][vertIndex] + s_edgeOffsets[z][1]; BlockIndex edge1 = GetNeighborBlock(neighborChunks, indices, edgePos); + if (edge1 == InvalidBlockIndex) + edge1 = EmptyBlockIndex; + BlockIndex edge2 = GetNeighborBlock(neighborChunks, indices, edgeNeighborPos); + if (edge2 == InvalidBlockIndex) + edge2 = EmptyBlockIndex; const auto& edge1BlockData = m_blockLibrary.GetBlockData(edge1); const auto& edge2BlockData = m_blockLibrary.GetBlockData(edge2); @@ -420,30 +425,23 @@ namespace tsom Nz::Vector3f n1 = Nz::Vector3f::CrossProduct(vertexAttributes.position[faceIndices[4]] - vertexAttributes.position[faceIndices[3]], vertexAttributes.position[faceIndices[5]] - vertexAttributes.position[faceIndices[3]]); Nz::Vector3f faceNormal = Nz::Vector3f::Normalize(n0 + n1); - for (unsigned int i = 0; i < 4; ++i) vertexAttributes.normal[i] = faceNormal; } - if (vertexAttributes.tangent) - { - Nz::Vector3f faceTangent = Nz::Vector3f::Normalize(vertexAttributes.position[1] - vertexAttributes.position[0]); - - for (std::size_t i = 0; i < 4; ++i) - vertexAttributes.tangent[i] = faceTangent; - } - - if (vertexAttributes.uv) + if (vertexAttributes.blockIndex) { - std::size_t textureIndex = blockData.texIndices[Direction::Up]; - float sliceIndex = textureIndex; for (std::size_t i = 0; i < 4; ++i) - vertexAttributes.uv[i] = { 0.f, 0.f, sliceIndex }; + vertexAttributes.blockIndex[i] = blockContent; } }; auto IsTransparent = [&](BlockIndex neighborBlockIndex) { + // don't render face for invalid chunks (chunks not loaded yet) + if (blockContent == InvalidBlockIndex || neighborBlockIndex == InvalidBlockIndex) + return false; + // don't render faces between blocks of the same type even if transparent if (blockContent == neighborBlockIndex) return false; @@ -554,7 +552,7 @@ namespace tsom { const Chunk* chunk = neighborChunks[ToNeighborChunk(chunkIndices - m_indices)]; if (!chunk) - return EmptyBlockIndex; + return InvalidBlockIndex; if (!chunk->HasContent()) return EmptyBlockIndex; diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index 81148add..02d0dbc4 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -3,6 +3,8 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include +#include +#include #include #include #include @@ -25,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +42,7 @@ #include #include #include +#include #include #ifdef TSOM_DEV_TOOLS @@ -47,6 +51,24 @@ namespace tsom { + namespace + { + enum class MandatoryFeature + { + Depth32F, + PersistentMapping, + StorageBuffers, + + Max = StorageBuffers + }; + + constexpr Nz::EnumArray s_featureNames = { + "Depth32F depth-buffers", + "persistent mapping", + "storage buffers" + }; + } + GameAppComponent::GameAppComponent(Nz::ApplicationBase& app) : ApplicationComponent(app) { @@ -56,14 +78,36 @@ namespace tsom { // Check if GPU has minimum required specs const Nz::RenderDevice& renderDevice = *Nz::Graphics::Instance()->GetRenderDevice(); - if (!renderDevice.IsTextureFormatSupported(Nz::PixelFormat::Depth32F, Nz::TextureUsage::DepthStencilAttachment)) + const Nz::RenderDeviceFeatures& renderDeviceFeatures = renderDevice.GetEnabledFeatures(); + + Nz::EnumArray featureTests = { + renderDevice.IsTextureFormatSupported(Nz::PixelFormat::Depth32F, Nz::TextureUsage::DepthStencilAttachment), + renderDeviceFeatures.persistentMapping, + renderDeviceFeatures.storageBuffers + }; + + // Test if all mandatory features are supported + if (std::find(featureTests.begin(), featureTests.end(), false) != featureTests.end()) { + std::string missingFeatures; + for (auto&& [feature, supported] : featureTests.iter_kv()) + { + if (!supported) + { + if (!missingFeatures.empty()) + missingFeatures += ", "; + + missingFeatures += s_featureNames[feature]; + } + } + const Nz::RenderDeviceInfo& deviceInfo = renderDevice.GetDeviceInfo(); - Nz::MessageBox requestBox(Nz::MessageBoxType::Error, "Missing GPU feature", + Nz::MessageBox requestBox(Nz::MessageBoxType::Error, "Missing GPU features", Nz::Format( - "Your GPU ({}) doesn't seem to support floating-point depth buffer (missing Depth32F support).\n" + "Your GPU ({}) doesn't seem to support mandatory features for the game (missing {} support).\n" "This is required for the game, try to update your drivers.{}", deviceInfo.name, + missingFeatures, (deviceInfo.type == Nz::RenderDeviceType::Integrated) ? "\nThe detected GPU seems to be integrated, try to use a dedicated GPU if possible.": "" ) ); @@ -245,13 +289,23 @@ namespace tsom auto& filesystem = app.GetComponent(); filesystem.Mount("assets", assetPath); + filesystem.Mount("CookedAssets", Nz::Utf8Path("cache/CookedAssets")); filesystem.Mount("scripts", scriptPath); Nz::Graphics* graphics = Nz::Graphics::Instance(); graphics->GetShaderModuleResolver()->RegisterDirectory(Nz::Utf8Path("assets/shaders"), true); m_blockLibrary.emplace(app); - m_blockLibrary->BuildTexture(); + + ClientAssetCooker assetCooker(app); + if (auto result = assetCooker.Cook(*m_blockLibrary); !result) + { + spdlog::critical("failed to cook assets: {}!", result.GetError()); + app.Quit(); + return false; + } + + m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); return true; } @@ -339,7 +393,7 @@ namespace tsom world.AddSystem(); world.AddSystem(); world.AddSystem(); - world.AddSystem(); + world.AddSystem([this](Nz::ElementRendererRegistry& elementRegistry) { return std::make_unique(elementRegistry, *m_blockLibrary); }); Nz::Physics3DSystem::Settings physSettings = Physics::BuildSettings(); physSettings.stepSize = Constants::TickDuration; diff --git a/src/Game/GameConfigAppComponent.cpp b/src/Game/GameConfigAppComponent.cpp index ce3f5b5c..0d68a547 100644 --- a/src/Game/GameConfigAppComponent.cpp +++ b/src/Game/GameConfigAppComponent.cpp @@ -43,7 +43,7 @@ namespace tsom RegisterIntegerOption(Config::Server_Port, 1, 0xFFFF, 29536); - RegisterFloatOption(Config::Visual_ChunkNormalSmoothAngle, 0.0, 180.0, 0.0); + RegisterFloatOption(Config::Visual_ChunkNormalSmoothAngle, 0.0, 180.0, 60.0); } std::filesystem::path GameConfigFile::GetPath() diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 6003b31b..dda0a7dd 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -15,9 +15,9 @@ #include #include #include -#include #include #include +#include #include #include #include diff --git a/src/Game/States/GameState.hpp b/src/Game/States/GameState.hpp index eae9de38..7d2198aa 100644 --- a/src/Game/States/GameState.hpp +++ b/src/Game/States/GameState.hpp @@ -7,7 +7,6 @@ #ifndef TSOM_GAME_STATES_GAMESTATE_HPP #define TSOM_GAME_STATES_GAMESTATE_HPP -#include #include #include #include diff --git a/src/Game/States/PlanetEditorState.hpp b/src/Game/States/PlanetEditorState.hpp index a206359c..272c2839 100644 --- a/src/Game/States/PlanetEditorState.hpp +++ b/src/Game/States/PlanetEditorState.hpp @@ -7,9 +7,9 @@ #ifndef TSOM_GAME_STATES_PLANETEDITORSTATE_HPP #define TSOM_GAME_STATES_PLANETEDITORSTATE_HPP -#include #include #include +#include #include #include #include @@ -64,7 +64,7 @@ namespace tsom struct PlanetSettings { float cornerRadius = 0.f; - Nz::Vector3ui chunkCount = Nz::Vector3ui(5); + Nz::Vector3ui chunkCount = Nz::Vector3ui(10); std::size_t seed = 42; std::string scriptName = "bob"; }; diff --git a/src/Server/main.cpp b/src/Server/main.cpp index 156a59ff..8d5a8c73 100644 --- a/src/Server/main.cpp +++ b/src/Server/main.cpp @@ -111,7 +111,7 @@ int ServerMain(int argc, char* argv[]) .id = 1, .generatorName = "bob", .seed = 42, - .chunkCount = Nz::Vector3ui(5), + .chunkCount = Nz::Vector3ui(15), .blockSize = 0.5f, .cornerRadius = 16.f, .gravity = 9.81f diff --git a/src/ServerLib/Systems/NetworkedEntitiesSystem.cpp b/src/ServerLib/Systems/NetworkedEntitiesSystem.cpp index 222e2af1..4d19c07e 100644 --- a/src/ServerLib/Systems/NetworkedEntitiesSystem.cpp +++ b/src/ServerLib/Systems/NetworkedEntitiesSystem.cpp @@ -62,9 +62,7 @@ namespace tsom if (it != m_pendingPlayers.end()) m_pendingPlayers.erase(it); else - { m_players.UnregisterPlayer(player); - } } void NetworkedEntitiesSystem::Update(Nz::Time /*elapsedTime*/) diff --git a/xmake.lua b/xmake.lua index 6df0fa08..b9f70ec2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -29,12 +29,13 @@ add_requires( "fast_float", "frozen", "libsodium 1.0.20", + "luajit", "lz4", "hopscotch-map", "nazarautils", "nlohmann_json", "perlinnoise", - "sol2", + "sol2[includes_lua=n]", "spdlog[fmt_external=y,header_only=n]", "sqlitecpp[sqlite3_external]" ) @@ -117,7 +118,7 @@ target("CommonLib", function () add_options("dev_tools", { public = true }) add_packages("nazaraengine", { components = { "physics3d", "network" }, public = true }) - add_packages("concurrentqueue", "cppcodec", "cpp-semver", "fast_float", "fmt", "hopscotch-map", "nlohmann_json", "sol2", "spdlog", { public = true }) + add_packages("concurrentqueue", "cppcodec", "cpp-semver", "fast_float", "fmt", "luajit", "hopscotch-map", "nlohmann_json", "sol2", "spdlog", { public = true }) add_packages("cpptrace", "frozen", "libsodium", "lz4", "perlinnoise") on_config(function (target, opt) From f0c3b953e07a0b66cb2a95630d47403ea0c57736 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Wed, 1 Apr 2026 22:15:04 +0200 Subject: [PATCH 02/23] Before --- assets/shaders/BlockPBR.nzsl | 82 +++++-------- include/ClientLib/ClientBlockLibrary.hpp | 6 +- include/ClientLib/ClientBlockLibrary.inl | 4 +- src/ClientLib/ClientBlockLibrary.cpp | 141 ++++++++++------------- src/ClientLib/ClientChunkEntities.cpp | 17 +-- 5 files changed, 97 insertions(+), 153 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 21557aca..89add867 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -23,13 +23,14 @@ option MaxLightCount: u32 = 3; [layout(std430)] struct BlockData { - baseColorFallback: vec4[f32], - baseColorMapIndices: vec2[i32], - normalMapIndices: vec2[i32], - ambientOcclusionHeightMapIndices: vec2[i32], - roughnessMetalnessMapIndices: vec2[i32], + ambientOcclusionMapIndex: f32, + baseColorMapIndex: f32, + heightMapIndex: f32, metalness: f32, - roughness: f32 + metalnessMapIndex: f32, + normalMapIndex: f32, + roughness: f32, + roughnessMapIndex: f32 } [layout(std430)] @@ -69,12 +70,7 @@ external MaterialData { [tag("Settings")] settings: uniform[MaterialSettings], [tag("GlobalBlockData")] globalBlockData: storage[GlobalBlockData], - - // TODO: Turn them into an array of texture - [tag("BlockTexture1")] blockTexture1: sampler2D_array[f32], - [tag("BlockTexture2")] blockTexture2: sampler2D_array[f32], - [tag("BlockTexture3")] blockTexture3: sampler2D_array[f32], - [tag("BlockTexture4")] blockTexture4: sampler2D_array[f32] + [tag("BlockTexture")] blockTexture: sampler2D_array[f32] } [tag("Engine")] @@ -111,33 +107,20 @@ struct FragOut [builtin(frag_depth), cond(DistanceDepth)] fragdepth: f32, } -fn SampleBlock(uv: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] -{ - // FIXME: Replace int to float conversion by bitcast for performance reasons - if (texIndices.x == 0) - return MaterialData.blockTexture1.Sample(vec3[f32](uv, f32(texIndices.y))); - else if (texIndices.x == 1) - return MaterialData.blockTexture2.Sample(vec3[f32](uv, f32(texIndices.y))); - else if (texIndices.x == 2) - return MaterialData.blockTexture3.Sample(vec3[f32](uv, f32(texIndices.y))); - else - return MaterialData.blockTexture4.Sample(vec3[f32](uv, f32(texIndices.y))); -} - -fn TriplanarSample(normal: vec3[f32], uvX: vec2[f32], uvY: vec2[f32], uvZ: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] +fn TriplanarSample(normal: vec3[f32], uvX: vec2[f32], uvY: vec2[f32], uvZ: vec2[f32], slice: f32) -> vec4[f32] { - let x = SampleBlock(uvX, texIndices); - let y = SampleBlock(uvY, texIndices); - let z = SampleBlock(uvZ, texIndices); + let x = MaterialData.blockTexture.Sample(vec3[f32](uvX, slice)); + let y = MaterialData.blockTexture.Sample(vec3[f32](uvY, slice)); + let z = MaterialData.blockTexture.Sample(vec3[f32](uvZ, slice)); let m = pow(abs(normal), (8.0).xxx); let textureColor = (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z); return textureColor; } -fn ParallaxMapping(texCoords: vec2[f32], heightMapIndices: vec2[i32], viewDir: vec3[f32]) -> vec2[f32] +fn ParallaxMapping(texCoords: vec2[f32], heightMapIndex: f32, viewDir: vec3[f32]) -> vec2[f32] { - let height = SampleBlock(texCoords, heightMapIndices).y; + let height = MaterialData.blockTexture.Sample(vec3[f32](texCoords, heightMapIndex)).x; return texCoords - viewDir.xy / viewDir.z * (height * 2.0); // number of depth layers @@ -189,13 +172,13 @@ fn ComputeColor(input: VertOut) -> vec4[f32] let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - if (blockData.baseColorMapIndices.x >= 0) + if (blockData.baseColorMapIndex >= 0.0) { let uvX = input.triplanarPos.zy; let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndex); } const if (AlphaTest) @@ -207,12 +190,9 @@ fn ComputeColor(input: VertOut) -> vec4[f32] return color; } -fn UnpackNormal(texData: vec2[f32]) -> vec3[f32] +fn UnpackNormal(tex: sampler2D_array[f32], uv: vec3[f32]) -> vec3[f32] { - let normal = vec3[f32](texData.x * 2.0 - 1.0, texData.y * 2.0 - 1.0, 0.0); - normal.z = sqrt(1.0 - normal.x - normal.y); - return normal; - //return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; + return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; } [export] @@ -228,9 +208,9 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - let tnormalX = UnpackNormal(SampleBlock(uvX, blockData.normalMapIndices).xy); - let tnormalY = UnpackNormal(SampleBlock(uvY, blockData.normalMapIndices).xy); - let tnormalZ = UnpackNormal(SampleBlock(uvZ, blockData.normalMapIndices).xy); + let tnormalX = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvX, blockData.normalMapIndex)); + let tnormalY = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvY, blockData.normalMapIndex)); + let tnormalZ = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvZ, blockData.normalMapIndex)); let axisSign = sign(input.normal); @@ -256,9 +236,9 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] uvZ = ParallaxMapping(uvZ, blockData.heightMapIndex, tbnZ * viewDirWS); }*/ - let color = blockData.baseColorFallback; - if (blockData.baseColorMapIndices.x >= 0) - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + let color = MaterialData.settings.BaseColor; + if (blockData.baseColorMapIndex >= 0.0) + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndex); const if (AlphaTest) { @@ -267,7 +247,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] } let normal: vec3[f32]; - if (blockData.normalMapIndices.x >= 0) + if (blockData.normalMapIndex >= 0.0) { // https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a let blend = pow(abs(input.normal), (16.0).rrr); @@ -287,21 +267,21 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let albedo = color.xyz; let metallic: f32; - /*if (blockData.metalnessMapIndex >= 0.0) + if (blockData.metalnessMapIndex >= 0.0) metallic = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.metalnessMapIndex).x; - else*/ + else metallic = blockData.metalness; let roughness: f32; - /*if (blockData.roughnessMapIndex >= 0.0) + if (blockData.roughnessMapIndex >= 0.0) roughness = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMapIndex).x; - else*/ + else roughness = blockData.roughness; let ao: f32; - /*if (blockData.ambientOcclusionMapIndex >= 0.0) + if (blockData.ambientOcclusionMapIndex >= 0.0) ao = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionMapIndex).x; - else*/ + else ao = 1.0; let F0 = vec3[f32](0.04, 0.04, 0.04); diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index 288741f9..e118e170 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -9,7 +9,6 @@ #include #include -#include namespace Nz { @@ -30,14 +29,15 @@ namespace tsom void BuildTexture(Nz::RenderDevice& renderDevice); - inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; + inline const std::shared_ptr& GetBaseColorTexture() const; inline const std::shared_ptr& GetGlobalBlockBuffer() const; inline const std::shared_ptr& GetPreviewTexture(BlockIndex blockIndex) const; private: std::shared_ptr m_globalBlockBuffer; + std::shared_ptr m_texture; + std::shared_ptr m_baseColorTexture; std::vector> m_previewTextures; - Nz::EnumArray> m_blockTextures; Nz::ApplicationBase& m_applicationBase; void* m_globalBlockBufferPtr; }; diff --git a/include/ClientLib/ClientBlockLibrary.inl b/include/ClientLib/ClientBlockLibrary.inl index 5af2d746..373b131e 100644 --- a/include/ClientLib/ClientBlockLibrary.inl +++ b/include/ClientLib/ClientBlockLibrary.inl @@ -14,9 +14,9 @@ namespace tsom m_blocks[idx].hasCollisions = true; } - inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const + inline const std::shared_ptr& ClientBlockLibrary::GetBaseColorTexture() const { - return m_blockTextures[textureType]; + return m_baseColorTexture; } inline const std::shared_ptr& ClientBlockLibrary::GetGlobalBlockBuffer() const diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index dd890b55..e41713bf 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -18,24 +18,27 @@ namespace tsom { enum class TextureType { - AmbientOcclusion_Height, + AmbientOcclusion, BaseColor, + Height, + Metalness, Normal, - Roughness_Metalness, + Roughness, - Max = Roughness_Metalness + Max = Roughness }; struct GlobalBlockBufferEntryOffsets { nzsl::FieldOffsets fieldOffsets; - std::size_t baseColorFallback; - std::size_t ambientOcclusionHeightMapIndex; + std::size_t ambientOcclusionMapIndex; std::size_t baseColorMapIndex; + std::size_t heightMapIndex; std::size_t metalness; + std::size_t metalnessMapIndex; std::size_t normalMapIndex; std::size_t roughness; - std::size_t roughnessMetalnessMapIndex; + std::size_t roughnessMapIndex; }; struct GlobalBlockBufferOffsets @@ -50,13 +53,14 @@ namespace tsom nzsl::FieldOffsets(nzsl::StructLayout::Std430) }; - bufferOffsets.baseColorFallback = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float4); - bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.ambientOcclusionHeightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.roughnessMetalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.ambientOcclusionMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.heightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); bufferOffsets.metalness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.metalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); bufferOffsets.roughness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.roughnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); return bufferOffsets; } @@ -86,105 +90,72 @@ namespace tsom auto& fs = m_applicationBase.GetComponent(); - std::optional cookRegistry; - fs.GetFileContent("CookedAssets/registry.json", [&](const void* ptr, Nz::UInt64 size) - { - cookRegistry = ClientAssetCookRegistry::LoadFromContent(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); - return true; - }); - - if (!cookRegistry) - throw std::runtime_error("failed to load cook registry"); - struct BlockTexture { - Nz::EnumArray textureData; + Nz::EnumArray> streams; }; Nz::UInt8* blockBufferPtr = static_cast(m_globalBlockBufferPtr) + s_blockBufferOffsets.entries; - Nz::EnumArray textureCount; - textureCount.fill(0); - Nz::UInt32 sliceCount = 0; std::vector blockTextures; blockTextures.reserve(m_blocks.size()); for (const BlockData& blockData : m_blocks) { - const auto& cookedBlockData = cookRegistry->GetBlock(blockData.name); - std::size_t blockIndex = blockTextures.size(); auto& blockTexture = blockTextures.emplace_back(); - blockTexture.textureData[TextureType::AmbientOcclusion_Height] = &cookedBlockData.ambientOcclusionHeightTexture; - blockTexture.textureData[TextureType::BaseColor] = &cookedBlockData.baseColorTexture; - blockTexture.textureData[TextureType::Normal] = &cookedBlockData.normalMapTexture; - blockTexture.textureData[TextureType::Roughness_Metalness] = &cookedBlockData.roughnessMetalnessTexture; - - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorFallback) = cookedBlockData.baseColorFallback; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.normalMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex) = { -1, -1 }; + blockTexture.streams[TextureType::BaseColor] = fs.GetFile(fmt::format("assets/{}.png", blockData.basePath)); + blockTexture.streams[TextureType::AmbientOcclusion] = fs.GetFile(fmt::format("assets/{}_ao.png", blockData.basePath)); + blockTexture.streams[TextureType::Height] = fs.GetFile(fmt::format("assets/{}_height.png", blockData.basePath)); + blockTexture.streams[TextureType::Metalness] = fs.GetFile(fmt::format("assets/{}_metallic.png", blockData.basePath)); + blockTexture.streams[TextureType::Normal] = fs.GetFile(fmt::format("assets/{}_normal.png", blockData.basePath)); + blockTexture.streams[TextureType::Roughness] = fs.GetFile(fmt::format("assets/{}_roughness.png", blockData.basePath)); + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.metalness) = blockData.metalness; Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughness) = blockData.roughness; - for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) + for (const auto& stream : blockTexture.streams) { - if (textureData->type != ClientAssetCookRegistry::TextureType::None) - textureCount[textureData->type]++; + if (stream) + sliceCount++; } } constexpr std::size_t texSize = 2048; // TODO: use texture size? - constexpr Nz::EnumArray textureFormat = { - Nz::PixelFormat::BC1_RGBA_Unorm, - Nz::PixelFormat::BC3_Unorm, - Nz::PixelFormat::BC4_Unorm, - Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm - }; + m_texture = renderDevice.InstantiateTexture({ + .pixelFormat = Nz::PixelFormat::RGBA8, + .type = Nz::ImageType::E2D_Array, + .layerCount = sliceCount, + .height = texSize, + .width = texSize + }); - for (auto&& [type, sliceCount] : textureCount.iter_kv()) - { - if (sliceCount > 0) - { - m_blockTextures[type] = renderDevice.InstantiateTexture({ - .pixelFormat = textureFormat[type], - .type = Nz::ImageType::E2D_Array, - .layerCount = sliceCount, - .height = texSize, - .width = texSize - }); - } - } + Nz::ImageParams loadParams; + loadParams.loadFormat = Nz::PixelFormat::RGBA8; constexpr Nz::EnumArray textureSliceOffsets = { - s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex, + s_blockBufferEntryOffsets.ambientOcclusionMapIndex, s_blockBufferEntryOffsets.baseColorMapIndex, + s_blockBufferEntryOffsets.heightMapIndex, + s_blockBufferEntryOffsets.metalnessMapIndex, s_blockBufferEntryOffsets.normalMapIndex, - s_blockBufferEntryOffsets.roughnessMetalnessMapIndex + s_blockBufferEntryOffsets.roughnessMapIndex }; - Nz::EnumArray textureSlice; - textureSlice.fill(0); - - auto UploadImage = [&](ClientAssetCookRegistry::TextureType textureType, Nz::UInt32 textureSlice, std::string_view filePath) + Nz::UInt32 textureSlice = 0; + auto UploadImage = [&](Nz::UInt32 blockIndex, TextureType textureType, Nz::Stream& stream) { - std::string assetPath = fmt::format("CookedAssets/{}", filePath); - - std::shared_ptr image = Nz::Image::LoadFromStream(*fs.GetFile(assetPath)); + std::shared_ptr image = Nz::Image::LoadFromStream(stream, loadParams); if (!image) return false; - for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) + m_texture->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) { - m_blockTextures[textureType]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) - { - std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); - return true; - }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(level), image->GetHeight(level), 1), level); - } + Nz::ImageUtils::Copy(pixelBuffer, image->GetConstPixels(), image->GetFormat(), image->GetWidth(), image->GetHeight(), 1, rowPitch, depthPitch, 0, 0); + return true; + }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(), image->GetHeight(), 1), 0); return true; }; @@ -196,23 +167,27 @@ namespace tsom { const BlockTexture& blockTexture = blockTextures.front(); - for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) + for (const auto& [textureType, stream] : blockTexture.streams.iter_kv()) { - if (textureData->type != ClientAssetCookRegistry::TextureType::None && - UploadImage(textureData->type, textureSlice[textureData->type], textureData->path)) + float& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); + if (stream && UploadImage(blockIndex, textureType, *stream)) { - Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); - blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; + if (textureType == TextureType::BaseColor) + previewTextures[blockIndex] = textureSlice; - textureSlice[textureData->type]++; + blockTextureSlice = textureSlice++; } + else + blockTextureSlice = -1.0f; } blockTextures.erase(blockTextures.begin()); //< Destroy entry to free streams blockIndex++; } - /*m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); + m_texture->BuildMipmaps(); + + m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); m_previewTextures.resize(m_blocks.size()); for (std::size_t blockIndex = 0; blockIndex < m_blocks.size(); ++blockIndex) @@ -225,6 +200,6 @@ namespace tsom }; m_previewTextures[blockIndex] = Nz::TextureAsset::CreateView(m_baseColorTexture, slotTexView); - }*/ + } } } diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index c378ad64..fed2d370 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -56,17 +55,11 @@ namespace tsom settings.AddValueProperty("ShadowPosScale", 1.f - 0.0025f); settings.AddValueProperty("TriplanarOffset", Nz::Vector3f::Zero()); settings.AddBufferProperty("GlobalBlockData"); - settings.AddTextureProperty("BlockTexture1", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("BlockTexture2", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("BlockTexture3", Nz::ImageType::E2D_Array); - settings.AddTextureProperty("BlockTexture4", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture", Nz::ImageType::E2D_Array); settings.AddPropertyHandler("GlobalBlockData"); settings.AddPropertyHandler("AlphaTest"); - settings.AddPropertyHandler("BlockTexture1"); - settings.AddPropertyHandler("BlockTexture2"); - settings.AddPropertyHandler("BlockTexture3"); - settings.AddPropertyHandler("BlockTexture4"); + settings.AddPropertyHandler("BlockTexture"); settings.AddPropertyHandler("BaseColor"); settings.AddPropertyHandler("AlphaTestThreshold"); settings.AddPropertyHandler("ShadowMapNormalOffset"); @@ -106,10 +99,7 @@ namespace tsom m_chunkMaterial = blockMaterial->Instantiate(); m_chunkMaterial->SetBufferProperty("GlobalBlockData", blockLibrary.GetGlobalBlockBuffer()); - m_chunkMaterial->SetTextureProperty("BlockTexture1", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1)), blockSampler); - //m_chunkMaterial->SetTextureProperty("BlockTexture2", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3)), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture3", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4)), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture4", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture", blockLibrary.GetBaseColorTexture(), blockSampler); m_chunkMaterial->SetValueProperty("ShadowPosScale", 1.f); m_chunkMaterial->SetValueProperty("AlphaTest", true); m_chunkMaterial->UpdatePassesStates({ "ShadowPass", "DistanceShadowPass" }, [](Nz::RenderStates& states) @@ -320,7 +310,6 @@ namespace tsom if (updateJob->cancelled) return; - // FIXME: If ClientChunkEntities is deleted before job finished, it can result in a crash ChunkReadLock lock(chunkPtr.get()); updateJob->mesh = BuildMesh(*chunkPtr); updateJob->jobDone++; From eaa3dce217d3a7c5387f75d670f36a66b61feb31 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Wed, 1 Apr 2026 22:15:19 +0200 Subject: [PATCH 03/23] Revert "Before" This reverts commit 28fe465f9fac8a0cd8fac0df3d4e05635266f20b. --- assets/shaders/BlockPBR.nzsl | 82 ++++++++----- include/ClientLib/ClientBlockLibrary.hpp | 6 +- include/ClientLib/ClientBlockLibrary.inl | 4 +- src/ClientLib/ClientBlockLibrary.cpp | 141 +++++++++++++---------- src/ClientLib/ClientChunkEntities.cpp | 17 ++- 5 files changed, 153 insertions(+), 97 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 89add867..21557aca 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -23,14 +23,13 @@ option MaxLightCount: u32 = 3; [layout(std430)] struct BlockData { - ambientOcclusionMapIndex: f32, - baseColorMapIndex: f32, - heightMapIndex: f32, + baseColorFallback: vec4[f32], + baseColorMapIndices: vec2[i32], + normalMapIndices: vec2[i32], + ambientOcclusionHeightMapIndices: vec2[i32], + roughnessMetalnessMapIndices: vec2[i32], metalness: f32, - metalnessMapIndex: f32, - normalMapIndex: f32, - roughness: f32, - roughnessMapIndex: f32 + roughness: f32 } [layout(std430)] @@ -70,7 +69,12 @@ external MaterialData { [tag("Settings")] settings: uniform[MaterialSettings], [tag("GlobalBlockData")] globalBlockData: storage[GlobalBlockData], - [tag("BlockTexture")] blockTexture: sampler2D_array[f32] + + // TODO: Turn them into an array of texture + [tag("BlockTexture1")] blockTexture1: sampler2D_array[f32], + [tag("BlockTexture2")] blockTexture2: sampler2D_array[f32], + [tag("BlockTexture3")] blockTexture3: sampler2D_array[f32], + [tag("BlockTexture4")] blockTexture4: sampler2D_array[f32] } [tag("Engine")] @@ -107,20 +111,33 @@ struct FragOut [builtin(frag_depth), cond(DistanceDepth)] fragdepth: f32, } -fn TriplanarSample(normal: vec3[f32], uvX: vec2[f32], uvY: vec2[f32], uvZ: vec2[f32], slice: f32) -> vec4[f32] +fn SampleBlock(uv: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] +{ + // FIXME: Replace int to float conversion by bitcast for performance reasons + if (texIndices.x == 0) + return MaterialData.blockTexture1.Sample(vec3[f32](uv, f32(texIndices.y))); + else if (texIndices.x == 1) + return MaterialData.blockTexture2.Sample(vec3[f32](uv, f32(texIndices.y))); + else if (texIndices.x == 2) + return MaterialData.blockTexture3.Sample(vec3[f32](uv, f32(texIndices.y))); + else + return MaterialData.blockTexture4.Sample(vec3[f32](uv, f32(texIndices.y))); +} + +fn TriplanarSample(normal: vec3[f32], uvX: vec2[f32], uvY: vec2[f32], uvZ: vec2[f32], texIndices: vec2[i32]) -> vec4[f32] { - let x = MaterialData.blockTexture.Sample(vec3[f32](uvX, slice)); - let y = MaterialData.blockTexture.Sample(vec3[f32](uvY, slice)); - let z = MaterialData.blockTexture.Sample(vec3[f32](uvZ, slice)); + let x = SampleBlock(uvX, texIndices); + let y = SampleBlock(uvY, texIndices); + let z = SampleBlock(uvZ, texIndices); let m = pow(abs(normal), (8.0).xxx); let textureColor = (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z); return textureColor; } -fn ParallaxMapping(texCoords: vec2[f32], heightMapIndex: f32, viewDir: vec3[f32]) -> vec2[f32] +fn ParallaxMapping(texCoords: vec2[f32], heightMapIndices: vec2[i32], viewDir: vec3[f32]) -> vec2[f32] { - let height = MaterialData.blockTexture.Sample(vec3[f32](texCoords, heightMapIndex)).x; + let height = SampleBlock(texCoords, heightMapIndices).y; return texCoords - viewDir.xy / viewDir.z * (height * 2.0); // number of depth layers @@ -172,13 +189,13 @@ fn ComputeColor(input: VertOut) -> vec4[f32] let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - if (blockData.baseColorMapIndex >= 0.0) + if (blockData.baseColorMapIndices.x >= 0) { let uvX = input.triplanarPos.zy; let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndex); + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); } const if (AlphaTest) @@ -190,9 +207,12 @@ fn ComputeColor(input: VertOut) -> vec4[f32] return color; } -fn UnpackNormal(tex: sampler2D_array[f32], uv: vec3[f32]) -> vec3[f32] +fn UnpackNormal(texData: vec2[f32]) -> vec3[f32] { - return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; + let normal = vec3[f32](texData.x * 2.0 - 1.0, texData.y * 2.0 - 1.0, 0.0); + normal.z = sqrt(1.0 - normal.x - normal.y); + return normal; + //return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; } [export] @@ -208,9 +228,9 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - let tnormalX = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvX, blockData.normalMapIndex)); - let tnormalY = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvY, blockData.normalMapIndex)); - let tnormalZ = UnpackNormal(MaterialData.blockTexture, vec3[f32](uvZ, blockData.normalMapIndex)); + let tnormalX = UnpackNormal(SampleBlock(uvX, blockData.normalMapIndices).xy); + let tnormalY = UnpackNormal(SampleBlock(uvY, blockData.normalMapIndices).xy); + let tnormalZ = UnpackNormal(SampleBlock(uvZ, blockData.normalMapIndices).xy); let axisSign = sign(input.normal); @@ -236,9 +256,9 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] uvZ = ParallaxMapping(uvZ, blockData.heightMapIndex, tbnZ * viewDirWS); }*/ - let color = MaterialData.settings.BaseColor; - if (blockData.baseColorMapIndex >= 0.0) - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndex); + let color = blockData.baseColorFallback; + if (blockData.baseColorMapIndices.x >= 0) + color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); const if (AlphaTest) { @@ -247,7 +267,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] } let normal: vec3[f32]; - if (blockData.normalMapIndex >= 0.0) + if (blockData.normalMapIndices.x >= 0) { // https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a let blend = pow(abs(input.normal), (16.0).rrr); @@ -267,21 +287,21 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let albedo = color.xyz; let metallic: f32; - if (blockData.metalnessMapIndex >= 0.0) + /*if (blockData.metalnessMapIndex >= 0.0) metallic = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.metalnessMapIndex).x; - else + else*/ metallic = blockData.metalness; let roughness: f32; - if (blockData.roughnessMapIndex >= 0.0) + /*if (blockData.roughnessMapIndex >= 0.0) roughness = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMapIndex).x; - else + else*/ roughness = blockData.roughness; let ao: f32; - if (blockData.ambientOcclusionMapIndex >= 0.0) + /*if (blockData.ambientOcclusionMapIndex >= 0.0) ao = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionMapIndex).x; - else + else*/ ao = 1.0; let F0 = vec3[f32](0.04, 0.04, 0.04); diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index e118e170..288741f9 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -9,6 +9,7 @@ #include #include +#include namespace Nz { @@ -29,15 +30,14 @@ namespace tsom void BuildTexture(Nz::RenderDevice& renderDevice); - inline const std::shared_ptr& GetBaseColorTexture() const; + inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; inline const std::shared_ptr& GetGlobalBlockBuffer() const; inline const std::shared_ptr& GetPreviewTexture(BlockIndex blockIndex) const; private: std::shared_ptr m_globalBlockBuffer; - std::shared_ptr m_texture; - std::shared_ptr m_baseColorTexture; std::vector> m_previewTextures; + Nz::EnumArray> m_blockTextures; Nz::ApplicationBase& m_applicationBase; void* m_globalBlockBufferPtr; }; diff --git a/include/ClientLib/ClientBlockLibrary.inl b/include/ClientLib/ClientBlockLibrary.inl index 373b131e..5af2d746 100644 --- a/include/ClientLib/ClientBlockLibrary.inl +++ b/include/ClientLib/ClientBlockLibrary.inl @@ -14,9 +14,9 @@ namespace tsom m_blocks[idx].hasCollisions = true; } - inline const std::shared_ptr& ClientBlockLibrary::GetBaseColorTexture() const + inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const { - return m_baseColorTexture; + return m_blockTextures[textureType]; } inline const std::shared_ptr& ClientBlockLibrary::GetGlobalBlockBuffer() const diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index e41713bf..dd890b55 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -18,27 +18,24 @@ namespace tsom { enum class TextureType { - AmbientOcclusion, + AmbientOcclusion_Height, BaseColor, - Height, - Metalness, Normal, - Roughness, + Roughness_Metalness, - Max = Roughness + Max = Roughness_Metalness }; struct GlobalBlockBufferEntryOffsets { nzsl::FieldOffsets fieldOffsets; - std::size_t ambientOcclusionMapIndex; + std::size_t baseColorFallback; + std::size_t ambientOcclusionHeightMapIndex; std::size_t baseColorMapIndex; - std::size_t heightMapIndex; std::size_t metalness; - std::size_t metalnessMapIndex; std::size_t normalMapIndex; std::size_t roughness; - std::size_t roughnessMapIndex; + std::size_t roughnessMetalnessMapIndex; }; struct GlobalBlockBufferOffsets @@ -53,14 +50,13 @@ namespace tsom nzsl::FieldOffsets(nzsl::StructLayout::Std430) }; - bufferOffsets.ambientOcclusionMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); - bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); - bufferOffsets.heightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); + bufferOffsets.baseColorFallback = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float4); + bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.ambientOcclusionHeightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.roughnessMetalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); bufferOffsets.metalness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); - bufferOffsets.metalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); - bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); bufferOffsets.roughness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); - bufferOffsets.roughnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); return bufferOffsets; } @@ -90,72 +86,105 @@ namespace tsom auto& fs = m_applicationBase.GetComponent(); + std::optional cookRegistry; + fs.GetFileContent("CookedAssets/registry.json", [&](const void* ptr, Nz::UInt64 size) + { + cookRegistry = ClientAssetCookRegistry::LoadFromContent(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + return true; + }); + + if (!cookRegistry) + throw std::runtime_error("failed to load cook registry"); + struct BlockTexture { - Nz::EnumArray> streams; + Nz::EnumArray textureData; }; Nz::UInt8* blockBufferPtr = static_cast(m_globalBlockBufferPtr) + s_blockBufferOffsets.entries; + Nz::EnumArray textureCount; + textureCount.fill(0); + Nz::UInt32 sliceCount = 0; std::vector blockTextures; blockTextures.reserve(m_blocks.size()); for (const BlockData& blockData : m_blocks) { + const auto& cookedBlockData = cookRegistry->GetBlock(blockData.name); + std::size_t blockIndex = blockTextures.size(); auto& blockTexture = blockTextures.emplace_back(); - blockTexture.streams[TextureType::BaseColor] = fs.GetFile(fmt::format("assets/{}.png", blockData.basePath)); - blockTexture.streams[TextureType::AmbientOcclusion] = fs.GetFile(fmt::format("assets/{}_ao.png", blockData.basePath)); - blockTexture.streams[TextureType::Height] = fs.GetFile(fmt::format("assets/{}_height.png", blockData.basePath)); - blockTexture.streams[TextureType::Metalness] = fs.GetFile(fmt::format("assets/{}_metallic.png", blockData.basePath)); - blockTexture.streams[TextureType::Normal] = fs.GetFile(fmt::format("assets/{}_normal.png", blockData.basePath)); - blockTexture.streams[TextureType::Roughness] = fs.GetFile(fmt::format("assets/{}_roughness.png", blockData.basePath)); - + blockTexture.textureData[TextureType::AmbientOcclusion_Height] = &cookedBlockData.ambientOcclusionHeightTexture; + blockTexture.textureData[TextureType::BaseColor] = &cookedBlockData.baseColorTexture; + blockTexture.textureData[TextureType::Normal] = &cookedBlockData.normalMapTexture; + blockTexture.textureData[TextureType::Roughness_Metalness] = &cookedBlockData.roughnessMetalnessTexture; + + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorFallback) = cookedBlockData.baseColorFallback; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.normalMapIndex) = { -1, -1 }; + Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex) = { -1, -1 }; Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.metalness) = blockData.metalness; Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughness) = blockData.roughness; - for (const auto& stream : blockTexture.streams) + for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) { - if (stream) - sliceCount++; + if (textureData->type != ClientAssetCookRegistry::TextureType::None) + textureCount[textureData->type]++; } } constexpr std::size_t texSize = 2048; // TODO: use texture size? - m_texture = renderDevice.InstantiateTexture({ - .pixelFormat = Nz::PixelFormat::RGBA8, - .type = Nz::ImageType::E2D_Array, - .layerCount = sliceCount, - .height = texSize, - .width = texSize - }); + constexpr Nz::EnumArray textureFormat = { + Nz::PixelFormat::BC1_RGBA_Unorm, + Nz::PixelFormat::BC3_Unorm, + Nz::PixelFormat::BC4_Unorm, + Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm + }; - Nz::ImageParams loadParams; - loadParams.loadFormat = Nz::PixelFormat::RGBA8; + for (auto&& [type, sliceCount] : textureCount.iter_kv()) + { + if (sliceCount > 0) + { + m_blockTextures[type] = renderDevice.InstantiateTexture({ + .pixelFormat = textureFormat[type], + .type = Nz::ImageType::E2D_Array, + .layerCount = sliceCount, + .height = texSize, + .width = texSize + }); + } + } constexpr Nz::EnumArray textureSliceOffsets = { - s_blockBufferEntryOffsets.ambientOcclusionMapIndex, + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex, s_blockBufferEntryOffsets.baseColorMapIndex, - s_blockBufferEntryOffsets.heightMapIndex, - s_blockBufferEntryOffsets.metalnessMapIndex, s_blockBufferEntryOffsets.normalMapIndex, - s_blockBufferEntryOffsets.roughnessMapIndex + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex }; - Nz::UInt32 textureSlice = 0; - auto UploadImage = [&](Nz::UInt32 blockIndex, TextureType textureType, Nz::Stream& stream) + Nz::EnumArray textureSlice; + textureSlice.fill(0); + + auto UploadImage = [&](ClientAssetCookRegistry::TextureType textureType, Nz::UInt32 textureSlice, std::string_view filePath) { - std::shared_ptr image = Nz::Image::LoadFromStream(stream, loadParams); + std::string assetPath = fmt::format("CookedAssets/{}", filePath); + + std::shared_ptr image = Nz::Image::LoadFromStream(*fs.GetFile(assetPath)); if (!image) return false; - m_texture->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) + for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) { - Nz::ImageUtils::Copy(pixelBuffer, image->GetConstPixels(), image->GetFormat(), image->GetWidth(), image->GetHeight(), 1, rowPitch, depthPitch, 0, 0); - return true; - }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(), image->GetHeight(), 1), 0); + m_blockTextures[textureType]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) + { + std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); + return true; + }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(level), image->GetHeight(level), 1), level); + } return true; }; @@ -167,27 +196,23 @@ namespace tsom { const BlockTexture& blockTexture = blockTextures.front(); - for (const auto& [textureType, stream] : blockTexture.streams.iter_kv()) + for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) { - float& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); - if (stream && UploadImage(blockIndex, textureType, *stream)) + if (textureData->type != ClientAssetCookRegistry::TextureType::None && + UploadImage(textureData->type, textureSlice[textureData->type], textureData->path)) { - if (textureType == TextureType::BaseColor) - previewTextures[blockIndex] = textureSlice; + Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); + blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; - blockTextureSlice = textureSlice++; + textureSlice[textureData->type]++; } - else - blockTextureSlice = -1.0f; } blockTextures.erase(blockTextures.begin()); //< Destroy entry to free streams blockIndex++; } - m_texture->BuildMipmaps(); - - m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); + /*m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); m_previewTextures.resize(m_blocks.size()); for (std::size_t blockIndex = 0; blockIndex < m_blocks.size(); ++blockIndex) @@ -200,6 +225,6 @@ namespace tsom }; m_previewTextures[blockIndex] = Nz::TextureAsset::CreateView(m_baseColorTexture, slotTexView); - } + }*/ } } diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index fed2d370..c378ad64 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -55,11 +56,17 @@ namespace tsom settings.AddValueProperty("ShadowPosScale", 1.f - 0.0025f); settings.AddValueProperty("TriplanarOffset", Nz::Vector3f::Zero()); settings.AddBufferProperty("GlobalBlockData"); - settings.AddTextureProperty("BlockTexture", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture1", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture2", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture3", Nz::ImageType::E2D_Array); + settings.AddTextureProperty("BlockTexture4", Nz::ImageType::E2D_Array); settings.AddPropertyHandler("GlobalBlockData"); settings.AddPropertyHandler("AlphaTest"); - settings.AddPropertyHandler("BlockTexture"); + settings.AddPropertyHandler("BlockTexture1"); + settings.AddPropertyHandler("BlockTexture2"); + settings.AddPropertyHandler("BlockTexture3"); + settings.AddPropertyHandler("BlockTexture4"); settings.AddPropertyHandler("BaseColor"); settings.AddPropertyHandler("AlphaTestThreshold"); settings.AddPropertyHandler("ShadowMapNormalOffset"); @@ -99,7 +106,10 @@ namespace tsom m_chunkMaterial = blockMaterial->Instantiate(); m_chunkMaterial->SetBufferProperty("GlobalBlockData", blockLibrary.GetGlobalBlockBuffer()); - m_chunkMaterial->SetTextureProperty("BlockTexture", blockLibrary.GetBaseColorTexture(), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture1", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1)), blockSampler); + //m_chunkMaterial->SetTextureProperty("BlockTexture2", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture3", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture4", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5)), blockSampler); m_chunkMaterial->SetValueProperty("ShadowPosScale", 1.f); m_chunkMaterial->SetValueProperty("AlphaTest", true); m_chunkMaterial->UpdatePassesStates({ "ShadowPass", "DistanceShadowPass" }, [](Nz::RenderStates& states) @@ -310,6 +320,7 @@ namespace tsom if (updateJob->cancelled) return; + // FIXME: If ClientChunkEntities is deleted before job finished, it can result in a crash ChunkReadLock lock(chunkPtr.get()); updateJob->mesh = BuildMesh(*chunkPtr); updateJob->jobDone++; From a042b063069471477508045801bfca1908ff67c1 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 2 Apr 2026 00:39:22 +0200 Subject: [PATCH 04/23] wip --- assets/shaders/BlockPBR.nzsl | 42 ++++++++++++---------- src/ClientLib/ClientAssetCooker.cpp | 22 ++++++------ src/ClientLib/ClientBlockLibrary.cpp | 52 +++++++++++++++++----------- src/Game/GameAppComponent.cpp | 4 +-- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 21557aca..5531b748 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -9,6 +9,8 @@ import ViewerData from Engine.ViewerData; import Lighting.Shadow; import SkinLinearPosition, SkinLinearPositionNormal from Engine.SkinningLinear; +import Math.Color; + // Pass-specific options option DepthPass: bool = false; option DistanceDepth: bool = false; @@ -258,7 +260,11 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let color = blockData.baseColorFallback; if (blockData.baseColorMapIndices.x >= 0) - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + { + let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + //texData.rgb = Color.sRGBToLinear(texData.rgb); + color *= texData; + } const if (AlphaTest) { @@ -286,23 +292,23 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let albedo = color.xyz; - let metallic: f32; - /*if (blockData.metalnessMapIndex >= 0.0) - metallic = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.metalnessMapIndex).x; - else*/ - metallic = blockData.metalness; - - let roughness: f32; - /*if (blockData.roughnessMapIndex >= 0.0) - roughness = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMapIndex).x; - else*/ - roughness = blockData.roughness; - - let ao: f32; - /*if (blockData.ambientOcclusionMapIndex >= 0.0) - ao = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionMapIndex).x; - else*/ - ao = 1.0; + let metallic = blockData.metalness; + let roughness = blockData.roughness; + let ao = 1.0; + + if (blockData.roughnessMetalnessMapIndices.x >= 0) + { + let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; + roughness = texData.x; + if (blockData.roughnessMetalnessMapIndices.y > 2) + metallic = texData.y; + } + + if (blockData.ambientOcclusionHeightMapIndices.x >= 0) + { + let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionHeightMapIndices).xy; + ao = texData.x; + } let F0 = vec3[f32](0.04, 0.04, 0.04); F0 = albedo * metallic + F0 * (1.0 - metallic); diff --git a/src/ClientLib/ClientAssetCooker.cpp b/src/ClientLib/ClientAssetCooker.cpp index f4c50124..379bf8d4 100644 --- a/src/ClientLib/ClientAssetCooker.cpp +++ b/src/ClientLib/ClientAssetCooker.cpp @@ -143,7 +143,6 @@ namespace tsom cookedPixels += 2; } } - cookedNormalMap.GenerateMipmaps(); cookedNormalMap = Nz::ImageCompressor::RG8ToBC5(cookedNormalMap); @@ -166,10 +165,6 @@ namespace tsom if (roughnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) return Nz::Err(fmt::format("{} roughness map has an unexpected size", blockData.name)); - std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness_metalness.dds", blockData.name)); - - std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; - if (streams[TextureType::Metalness]) { // BC5 roughness/metalness @@ -195,10 +190,13 @@ namespace tsom cookedPixels += 2; } } - cookedRoughnessMetalnessMap.GenerateMipmaps(); cookedRoughnessMetalnessMap = Nz::ImageCompressor::RG8ToBC5(cookedRoughnessMetalnessMap); + + std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness_metalness.dds", blockData.name)); + std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; + blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; if (!cookedRoughnessMetalnessMap.SaveToFile(targetPath)) @@ -216,10 +214,13 @@ namespace tsom for (std::size_t x = 0; x < imageSize; ++x) *cookedPixels++ = *sourcePixels++; } - cookedRoughnessMap.GenerateMipmaps(); cookedRoughnessMap = Nz::ImageCompressor::R8ToBC4(cookedRoughnessMap); + + std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness.dds", blockData.name)); + std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; + blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC4, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; if (!cookedRoughnessMap.SaveToFile(targetPath)) @@ -252,11 +253,12 @@ namespace tsom *cookedPixels++ = *aoPixels++; } - cookedAOMap = Nz::ImageCompressor::R8ToBC4(cookedAOMap); + cookedAOMap.GenerateMipmaps(); + cookedAOMap = Nz::ImageCompressor::R8ToBC4(cookedAOMap); - std::filesystem::path aoHeightFilename = Nz::Utf8Path(fmt::format("{}_ao_height.dds", blockData.name)); - blockEntry.ambientOcclusionHeightTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / aoHeightFilename) }; + std::filesystem::path aoHeightFilename = Nz::Utf8Path(fmt::format("{}_ao.dds", blockData.name)); + blockEntry.ambientOcclusionHeightTexture = { ClientAssetCookRegistry::TextureType::BC4, Nz::PathToString(blockDir / aoHeightFilename) }; std::filesystem::path targetPath = cookedBlockPath / aoHeightFilename; if (!cookedAOMap.SaveToFile(targetPath)) diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index dd890b55..83837a33 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include namespace tsom { @@ -79,7 +79,7 @@ namespace tsom void ClientBlockLibrary::BuildTexture(Nz::RenderDevice& renderDevice) { - std::size_t bufferSize = s_blockBufferOffsets.fieldOffsets.GetSize() * m_blocks.size(); + std::size_t bufferSize = s_blockBufferOffsets.fieldOffsets.GetAlignedSize() * m_blocks.size(); m_globalBlockBuffer = renderDevice.InstantiateBuffer(bufferSize, Nz::BufferUsage::DeviceLocal | Nz::BufferUsage::StorageBuffer | Nz::BufferUsage::PersistentMapping); m_globalBlockBufferPtr = m_globalBlockBuffer->Map(0, bufferSize); @@ -171,20 +171,6 @@ namespace tsom auto UploadImage = [&](ClientAssetCookRegistry::TextureType textureType, Nz::UInt32 textureSlice, std::string_view filePath) { - std::string assetPath = fmt::format("CookedAssets/{}", filePath); - - std::shared_ptr image = Nz::Image::LoadFromStream(*fs.GetFile(assetPath)); - if (!image) - return false; - - for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) - { - m_blockTextures[textureType]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) - { - std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); - return true; - }, Nz::Boxui(0, 0, textureSlice, image->GetWidth(level), image->GetHeight(level), 1), level); - } return true; }; @@ -198,14 +184,40 @@ namespace tsom for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) { - if (textureData->type != ClientAssetCookRegistry::TextureType::None && - UploadImage(textureData->type, textureSlice[textureData->type], textureData->path)) + if (textureData->type == ClientAssetCookRegistry::TextureType::None) + continue; + + std::string texturePath = fmt::format("CookedAssets/{}", textureData->path); + std::shared_ptr stream = fs.GetFile(texturePath); + if (!stream) { + spdlog::error("asset {} not found", texturePath); + continue; + } + + std::shared_ptr image = Nz::Image::LoadFromStream(*stream); + if (!image) + { + spdlog::error("failed to load {}", texturePath); + continue; + } + + //if (textureSlice[textureData->type] == 0) + { + for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) + { + m_blockTextures[textureData->type]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) + { + std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); + return true; + }, Nz::Boxui(0, 0, textureSlice[textureData->type], image->GetWidth(level), image->GetHeight(level), 1), level); + } + Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; - - textureSlice[textureData->type]++; } + + textureSlice[textureData->type]++; } blockTextures.erase(blockTextures.begin()); //< Destroy entry to free streams diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index 02d0dbc4..fc869f86 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -297,13 +297,13 @@ namespace tsom m_blockLibrary.emplace(app); - ClientAssetCooker assetCooker(app); + /*ClientAssetCooker assetCooker(app); if (auto result = assetCooker.Cook(*m_blockLibrary); !result) { spdlog::critical("failed to cook assets: {}!", result.GetError()); app.Quit(); return false; - } + }*/ m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); From 8f7dce5caf6111332f69f399e96a757a9dee3823 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Fri, 3 Apr 2026 13:51:18 +0200 Subject: [PATCH 05/23] wip --- assets/shaders/BlockPBR.nzsl | 34 +++--- include/ClientLib/BlockSelectionBar.hpp | 2 +- include/ClientLib/ClientAssetCookRegistry.hpp | 3 + include/ClientLib/ClientBlockLibrary.hpp | 4 +- include/ClientLib/ClientBlockLibrary.inl | 2 +- src/ClientLib/BlockSelectionBar.cpp | 2 +- src/ClientLib/ClientAssetCookRegistry.cpp | 46 ++------ src/ClientLib/ClientAssetCooker.cpp | 37 +++++- src/ClientLib/ClientBlockLibrary.cpp | 108 +++++++++--------- src/ClientLib/ClientChunkEntities.cpp | 12 +- src/Game/GameAppComponent.cpp | 16 ++- 11 files changed, 140 insertions(+), 126 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 5531b748..2c6da8b6 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -30,6 +30,7 @@ struct BlockData normalMapIndices: vec2[i32], ambientOcclusionHeightMapIndices: vec2[i32], roughnessMetalnessMapIndices: vec2[i32], + ambientOcclusion: f32, metalness: f32, roughness: f32 } @@ -70,7 +71,7 @@ const SpotLight = 2; external MaterialData { [tag("Settings")] settings: uniform[MaterialSettings], - [tag("GlobalBlockData")] globalBlockData: storage[GlobalBlockData], + [tag("GlobalBlockData")] globalBlockData: storage[GlobalBlockData, readonly], // TODO: Turn them into an array of texture [tag("BlockTexture1")] blockTexture1: sampler2D_array[f32], @@ -191,14 +192,14 @@ fn ComputeColor(input: VertOut) -> vec4[f32] let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - if (blockData.baseColorMapIndices.x >= 0) + /*if (blockData.baseColorMapIndices.x >= 0) { let uvX = input.triplanarPos.zy; let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); - } + }*/ const if (AlphaTest) { @@ -213,8 +214,8 @@ fn UnpackNormal(texData: vec2[f32]) -> vec3[f32] { let normal = vec3[f32](texData.x * 2.0 - 1.0, texData.y * 2.0 - 1.0, 0.0); normal.z = sqrt(1.0 - normal.x - normal.y); + return normal; - //return tex.Sample(uv).xyz * 2.0 - (1.0).rrr; } [export] @@ -259,12 +260,11 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] }*/ let color = blockData.baseColorFallback; - if (blockData.baseColorMapIndices.x >= 0) + /*if (blockData.baseColorMapIndices.x >= 0) { - let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); - //texData.rgb = Color.sRGBToLinear(texData.rgb); - color *= texData; - } + color = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + color.rgb = Color.sRGBToLinear(color.rgb); + }*/ const if (AlphaTest) { @@ -273,7 +273,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] } let normal: vec3[f32]; - if (blockData.normalMapIndices.x >= 0) + /*if (blockData.normalMapIndices.x >= 0) { // https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a let blend = pow(abs(input.normal), (16.0).rrr); @@ -287,16 +287,16 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] clamp(tbnZ * tnormalZ, (-1.0).rrr, (1.0).rrr) * blend.z ); } - else + else*/ normal = normalize(input.normal); let albedo = color.xyz; let metallic = blockData.metalness; let roughness = blockData.roughness; - let ao = 1.0; + let ao = blockData.ambientOcclusion; - if (blockData.roughnessMetalnessMapIndices.x >= 0) + /*if (blockData.roughnessMetalnessMapIndices.x >= 0) { let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; roughness = texData.x; @@ -308,7 +308,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] { let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionHeightMapIndices).xy; ao = texData.x; - } + }*/ let F0 = vec3[f32](0.04, 0.04, 0.04); F0 = albedo * metallic + F0 * (1.0 - metallic); @@ -366,11 +366,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] } let ambient = (0.05).rrr * albedo * ao; - - let finalColor = ambient + lightRadiance; - finalColor = finalColor / (finalColor + vec3[f32](1.0, 1.0, 1.0)); - - return vec4[f32](finalColor, color.a); + return vec4[f32](ambient + lightRadiance, color.a); } [export, entry(frag), cond(!DepthPass)] diff --git a/include/ClientLib/BlockSelectionBar.hpp b/include/ClientLib/BlockSelectionBar.hpp index d4279a8c..508985a4 100644 --- a/include/ClientLib/BlockSelectionBar.hpp +++ b/include/ClientLib/BlockSelectionBar.hpp @@ -50,7 +50,7 @@ namespace tsom BlockSelectionBar& operator=(const BlockSelectionBar&) = delete; BlockSelectionBar& operator=(BlockSelectionBar&&) = delete; - static constexpr float InventoryTileSize = 96.f; + static constexpr float InventoryTileSize = 64.f; static constexpr float Padding = 5.f; private: diff --git a/include/ClientLib/ClientAssetCookRegistry.hpp b/include/ClientLib/ClientAssetCookRegistry.hpp index 31ec4711..1aeed4aa 100644 --- a/include/ClientLib/ClientAssetCookRegistry.hpp +++ b/include/ClientLib/ClientAssetCookRegistry.hpp @@ -63,6 +63,9 @@ namespace tsom Texture baseColorTexture; Texture normalMapTexture; Texture roughnessMetalnessTexture; + float ambientOcclusionFallback; + float roughnessFallback; + float metalnessFallback; }; private: diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index 288741f9..f7d82c00 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -30,14 +30,14 @@ namespace tsom void BuildTexture(Nz::RenderDevice& renderDevice); - inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; + inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; inline const std::shared_ptr& GetGlobalBlockBuffer() const; inline const std::shared_ptr& GetPreviewTexture(BlockIndex blockIndex) const; private: std::shared_ptr m_globalBlockBuffer; std::vector> m_previewTextures; - Nz::EnumArray> m_blockTextures; + Nz::EnumArray> m_blockTextures; Nz::ApplicationBase& m_applicationBase; void* m_globalBlockBufferPtr; }; diff --git a/include/ClientLib/ClientBlockLibrary.inl b/include/ClientLib/ClientBlockLibrary.inl index 5af2d746..d37dcd3b 100644 --- a/include/ClientLib/ClientBlockLibrary.inl +++ b/include/ClientLib/ClientBlockLibrary.inl @@ -14,7 +14,7 @@ namespace tsom m_blocks[idx].hasCollisions = true; } - inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const + inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const { return m_blockTextures[textureType]; } diff --git a/src/ClientLib/BlockSelectionBar.cpp b/src/ClientLib/BlockSelectionBar.cpp index 411a022a..96697c57 100644 --- a/src/ClientLib/BlockSelectionBar.cpp +++ b/src/ClientLib/BlockSelectionBar.cpp @@ -26,7 +26,7 @@ namespace tsom NazaraAssertMsg(blockIndex != InvalidBlockIndex, "%s is not a valid block name", blockName.data()); std::shared_ptr slotMat = Nz::MaterialInstance::Instantiate(Nz::MaterialType::Basic); - //slotMat->SetTextureProperty("BaseColorMap", m_blockLibrary.GetPreviewTexture(blockIndex)); + slotMat->SetTextureProperty("BaseColorMap", m_blockLibrary.GetPreviewTexture(blockIndex)); Nz::ImageWidget* imageWidget = Add(slotMat); imageWidget->SetColor((active) ? Nz::Color::White() : Nz::Color::sRGBToLinear(Nz::Color::Gray())); diff --git a/src/ClientLib/ClientAssetCookRegistry.cpp b/src/ClientLib/ClientAssetCookRegistry.cpp index 0adbb7f0..21f3a8e7 100644 --- a/src/ClientLib/ClientAssetCookRegistry.cpp +++ b/src/ClientLib/ClientAssetCookRegistry.cpp @@ -25,13 +25,6 @@ namespace nlohmann {tsom::ClientAssetCookRegistry::TextureType::BC5, "bc5"}, }) - template - void from_json(const BasicJsonType& j, tsom::ClientAssetCookRegistry::Texture& texture) - { - j.at("path").get_to(texture.path); - j.at("type").get_to(texture.type); - } - template void to_json(BasicJsonType& j, const Nz::Color& color) { @@ -41,18 +34,18 @@ namespace nlohmann {"b", color.b} }; - if (Nz::NumberEquals(color.a, 1.0f)) + if (!Nz::NumberEquals(color.a, 1.0f)) j["a"] = color.a; } - template - void to_json(BasicJsonType& j, const tsom::ClientAssetCookRegistry::Texture& texture) - { - j = BasicJsonType{ - {"path", texture.path}, - {"type", texture.type} - }; - } + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::ClientAssetCookRegistry::Texture, path, type) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::ClientAssetCookRegistry::BlockEntry, + ambientOcclusionFallback, ambientOcclusionHeightTexture, + baseColorFallback, baseColorTexture, + metalnessFallback, normalMapTexture, + roughnessFallback, roughnessMetalnessTexture + ) } namespace tsom @@ -72,16 +65,7 @@ namespace tsom { nlohmann::ordered_json blockEntries; for (const auto& [blockName, blockEntry] : m_blockEntries) - { - nlohmann::ordered_json blockEntryDoc; - blockEntryDoc["ambientOcclusionHeightTexture"] = blockEntry.ambientOcclusionHeightTexture; - blockEntryDoc["baseColorFallback"] = blockEntry.baseColorFallback; - blockEntryDoc["baseColorTexture"] = blockEntry.baseColorTexture; - blockEntryDoc["normalMapTexture"] = blockEntry.normalMapTexture; - blockEntryDoc["roughnessMetalnessTexture"] = blockEntry.roughnessMetalnessTexture; - - blockEntries[blockName] = std::move(blockEntryDoc); - } + blockEntries[blockName] = blockEntry; nlohmann::ordered_json doc; doc["blocks"] = std::move(blockEntries); @@ -97,15 +81,7 @@ namespace tsom ClientAssetCookRegistry cookRegistry; for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) - { - cookRegistry.AddBlock(blockName, BlockEntry { - .baseColorFallback = blockEntryDoc["baseColorFallback"], - .ambientOcclusionHeightTexture = blockEntryDoc["ambientOcclusionHeightTexture"], - .baseColorTexture = blockEntryDoc["baseColorTexture"], - .normalMapTexture = blockEntryDoc["normalMapTexture"], - .roughnessMetalnessTexture = blockEntryDoc["roughnessMetalnessTexture"], - }); - } + cookRegistry.AddBlock(blockName, blockEntryDoc); return std::move(cookRegistry); } diff --git a/src/ClientLib/ClientAssetCooker.cpp b/src/ClientLib/ClientAssetCooker.cpp index 379bf8d4..b5bbae6f 100644 --- a/src/ClientLib/ClientAssetCooker.cpp +++ b/src/ClientLib/ClientAssetCooker.cpp @@ -74,13 +74,18 @@ namespace tsom if (!streams[TextureType::BaseColor]) return Nz::Err(fmt::format("failed to open {} base color", blockData.name)); - std::shared_ptr baseColor = Nz::Image::LoadFromStream(*streams[TextureType::BaseColor], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::RGBA8 }); + std::shared_ptr baseColor = Nz::Image::LoadFromStream(*streams[TextureType::BaseColor]); if (!baseColor) return Nz::Err(fmt::format("failed to load {} base color", blockData.name)); - blockEntry.baseColorFallback = baseColor->ComputeAverageColor(); + if (baseColor->GetFormat() != Nz::PixelFormat::RGB8 && baseColor->GetFormat() != Nz::PixelFormat::RGBA8) + return Nz::Err(fmt::format("{} color map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(baseColor->GetFormat()))); + + blockEntry.baseColorFallback = Nz::Color::sRGBToLinear(baseColor->ComputeAverageColor()); spdlog::debug("{} base color map average color: {};{};{};{}", blockData.name, blockEntry.baseColorFallback.r, blockEntry.baseColorFallback.g, blockEntry.baseColorFallback.b, blockEntry.baseColorFallback.a); + bool hasAlpha = baseColor->HasAlpha(); // test before potential resize (faster) + if (baseColor->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) { spdlog::warn("{} base color has an unexpected size", blockData.name); @@ -92,7 +97,7 @@ namespace tsom std::filesystem::path colorFilename = Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name)); Nz::Image compressedBaseColor; - if (baseColor->HasAlpha()) + if (hasAlpha) { // Compress using BC3 // TODO: Detect 1bit alpha @@ -106,7 +111,11 @@ namespace tsom // Compress using BC1 spdlog::debug("{} base color map has no alpha", blockData.name); - compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC1(*baseColor); + if (baseColor->GetFormat() == Nz::PixelFormat::RGBA8) + compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC1(*baseColor); + else + compressedBaseColor = Nz::ImageCompressor::RGB8ToBC1(*baseColor); + blockEntry.baseColorTexture = { ClientAssetCookRegistry::TextureType::BC1, Nz::PathToString(blockDir / colorFilename) }; } @@ -118,10 +127,13 @@ namespace tsom // Handle normal maps if (streams[TextureType::Normal]) { - std::shared_ptr normalMap = Nz::Image::LoadFromStream(*streams[TextureType::Normal], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::RGBA8 }); + std::shared_ptr normalMap = Nz::Image::LoadFromStream(*streams[TextureType::Normal]); if (!normalMap) return Nz::Err(fmt::format("failed to load {} normal map", blockData.name)); + if (normalMap->GetFormat() != Nz::PixelFormat::RGB8 && normalMap->GetFormat() != Nz::PixelFormat::RGBA8) + return Nz::Err(fmt::format("{} normal map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(normalMap->GetFormat()))); + if (normalMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) return Nz::Err(fmt::format("{} normal map has an unexpected size", blockData.name)); @@ -129,6 +141,8 @@ namespace tsom const Nz::UInt8* sourcePixels = normalMap->GetConstPixels(); Nz::UInt8* cookedPixels = cookedNormalMap.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(normalMap->GetFormat()); + for (std::size_t y = 0; y < imageSize; ++y) { for (std::size_t x = 0; x < imageSize; ++x) @@ -139,7 +153,7 @@ namespace tsom cookedPixels[0] = sourcePixels[0]; cookedPixels[1] = sourcePixels[1]; - sourcePixels += 4; + sourcePixels += bpp; cookedPixels += 2; } } @@ -156,6 +170,9 @@ namespace tsom } // Roughness/metalness + blockEntry.metalnessFallback = blockData.metalness; + blockEntry.roughnessFallback = blockData.roughness; + if (streams[TextureType::Roughness]) { std::shared_ptr roughnessMap = Nz::Image::LoadFromStream(*streams[TextureType::Roughness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); @@ -165,6 +182,8 @@ namespace tsom if (roughnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) return Nz::Err(fmt::format("{} roughness map has an unexpected size", blockData.name)); + blockEntry.roughnessFallback = roughnessMap->ComputeAverageColor().r; + if (streams[TextureType::Metalness]) { // BC5 roughness/metalness @@ -175,6 +194,8 @@ namespace tsom if (metalnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) return Nz::Err(fmt::format("{} metalness map has an unexpected size", blockData.name)); + blockEntry.metalnessFallback = metalnessMap->ComputeAverageColor().r; + Nz::Image cookedRoughnessMetalnessMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, imageSize, imageSize); const Nz::UInt8* roughnessPixels = roughnessMap->GetConstPixels(); @@ -233,6 +254,8 @@ namespace tsom } // Ambient Occlusion (+ Heightmap once used) + blockEntry.ambientOcclusionFallback = 1.0f; + if (streams[TextureType::AmbientOcclusion]) { // BC4 @@ -243,6 +266,8 @@ namespace tsom if (aoMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) return Nz::Err(fmt::format("{} ambient occlusion map has an unexpected size", blockData.name)); + blockEntry.ambientOcclusionFallback = aoMap->ComputeAverageColor().r; + Nz::Image cookedAOMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, imageSize, imageSize); const Nz::UInt8* aoPixels = aoMap->GetConstPixels(); diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index 83837a33..7dca385b 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -30,12 +30,13 @@ namespace tsom { nzsl::FieldOffsets fieldOffsets; std::size_t baseColorFallback; - std::size_t ambientOcclusionHeightMapIndex; - std::size_t baseColorMapIndex; + std::size_t ambientOcclusionHeightMapIndices; + std::size_t baseColorMapIndices; std::size_t metalness; - std::size_t normalMapIndex; + std::size_t normalMapIndices; + std::size_t ambientOcclusion; std::size_t roughness; - std::size_t roughnessMetalnessMapIndex; + std::size_t roughnessMetalnessMapIndices; }; struct GlobalBlockBufferOffsets @@ -51,10 +52,11 @@ namespace tsom }; bufferOffsets.baseColorFallback = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float4); - bufferOffsets.baseColorMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.normalMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.ambientOcclusionHeightMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); - bufferOffsets.roughnessMetalnessMapIndex = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.baseColorMapIndices = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.normalMapIndices = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.ambientOcclusionHeightMapIndices = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.roughnessMetalnessMapIndices = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Int2); + bufferOffsets.ambientOcclusion = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); bufferOffsets.metalness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); bufferOffsets.roughness = bufferOffsets.fieldOffsets.AddField(nzsl::StructFieldType::Float1); @@ -107,27 +109,29 @@ namespace tsom textureCount.fill(0); Nz::UInt32 sliceCount = 0; - std::vector blockTextures; - blockTextures.reserve(m_blocks.size()); + std::vector remainingBlockTextures; + remainingBlockTextures.reserve(m_blocks.size()); for (const BlockData& blockData : m_blocks) { const auto& cookedBlockData = cookRegistry->GetBlock(blockData.name); - std::size_t blockIndex = blockTextures.size(); - auto& blockTexture = blockTextures.emplace_back(); + std::size_t blockIndex = remainingBlockTextures.size(); + auto& blockTexture = remainingBlockTextures.emplace_back(); blockTexture.textureData[TextureType::AmbientOcclusion_Height] = &cookedBlockData.ambientOcclusionHeightTexture; blockTexture.textureData[TextureType::BaseColor] = &cookedBlockData.baseColorTexture; blockTexture.textureData[TextureType::Normal] = &cookedBlockData.normalMapTexture; blockTexture.textureData[TextureType::Roughness_Metalness] = &cookedBlockData.roughnessMetalnessTexture; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorFallback) = cookedBlockData.baseColorFallback; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.baseColorMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.normalMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughnessMetalnessMapIndex) = { -1, -1 }; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.metalness) = blockData.metalness; - Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + s_blockBufferEntryOffsets.roughness) = blockData.roughness; + Nz::UInt8* blockDataPtr = blockBufferPtr + blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize(); + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.baseColorFallback) = cookedBlockData.baseColorFallback; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndices) = { -1, -1 }; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.baseColorMapIndices) = { -1, -1 }; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.normalMapIndices) = { -1, -1 }; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.roughnessMetalnessMapIndices) = { -1, -1 }; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.ambientOcclusion) = cookedBlockData.ambientOcclusionFallback; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.metalness) = cookedBlockData.metalnessFallback; + Nz::AccessByOffset(blockDataPtr, s_blockBufferEntryOffsets.roughness) = cookedBlockData.roughnessFallback; for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) { @@ -138,18 +142,21 @@ namespace tsom constexpr std::size_t texSize = 2048; // TODO: use texture size? + // BC1/BC3 sRGB formats are not well supported with OpenGL, sRGB to linear conversion is done in shader + // TODO: Add a shader option to use sRGB formats if supported to avoid conversion cost constexpr Nz::EnumArray textureFormat = { - Nz::PixelFormat::BC1_RGBA_Unorm, + Nz::PixelFormat::BC1_RGBA_Unorm, Nz::PixelFormat::BC3_Unorm, Nz::PixelFormat::BC4_Unorm, Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm }; + Nz::EnumArray> blockTextures; for (auto&& [type, sliceCount] : textureCount.iter_kv()) { if (sliceCount > 0) { - m_blockTextures[type] = renderDevice.InstantiateTexture({ + blockTextures[type] = renderDevice.InstantiateTexture({ .pixelFormat = textureFormat[type], .type = Nz::ImageType::E2D_Array, .layerCount = sliceCount, @@ -160,27 +167,20 @@ namespace tsom } constexpr Nz::EnumArray textureSliceOffsets = { - s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndex, - s_blockBufferEntryOffsets.baseColorMapIndex, - s_blockBufferEntryOffsets.normalMapIndex, - s_blockBufferEntryOffsets.roughnessMetalnessMapIndex + s_blockBufferEntryOffsets.ambientOcclusionHeightMapIndices, + s_blockBufferEntryOffsets.baseColorMapIndices, + s_blockBufferEntryOffsets.normalMapIndices, + s_blockBufferEntryOffsets.roughnessMetalnessMapIndices }; Nz::EnumArray textureSlice; textureSlice.fill(0); - auto UploadImage = [&](ClientAssetCookRegistry::TextureType textureType, Nz::UInt32 textureSlice, std::string_view filePath) - { - - return true; - }; - - std::vector previewTextures(m_blocks.size()); + std::vector> previewTextures(m_blocks.size()); - Nz::UInt32 blockIndex = 0; - while (!blockTextures.empty()) + for (std::size_t blockIndex = 0; blockIndex < remainingBlockTextures.size(); ++blockIndex) { - const BlockTexture& blockTexture = blockTextures.front(); + const BlockTexture& blockTexture = remainingBlockTextures[blockIndex]; for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) { @@ -202,29 +202,31 @@ namespace tsom continue; } - //if (textureSlice[textureData->type] == 0) + for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) { - for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) + blockTextures[textureData->type]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) { - m_blockTextures[textureData->type]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) - { - std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); - return true; - }, Nz::Boxui(0, 0, textureSlice[textureData->type], image->GetWidth(level), image->GetHeight(level), 1), level); - } - - Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); - blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; + NazaraAssert(rowPitch == 0 && depthPitch == 0); + std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); + return true; + }, Nz::Boxui(0, 0, textureSlice[textureData->type], image->GetWidth(level), image->GetHeight(level), 1), level); } + Nz::Vector2i32& blockTextureSlice = Nz::AccessByOffset(blockBufferPtr, blockIndex * s_blockBufferEntryOffsets.fieldOffsets.GetAlignedSize() + textureSliceOffsets[textureType]); + blockTextureSlice = { static_cast(textureData->type), static_cast(textureSlice[textureData->type]) }; + + if (textureType == TextureType::BaseColor) + previewTextures[blockIndex] = std::make_pair(textureData->type, textureSlice[textureData->type]); + textureSlice[textureData->type]++; } - - blockTextures.erase(blockTextures.begin()); //< Destroy entry to free streams - blockIndex++; } - /*m_baseColorTexture = Nz::TextureAsset::CreateFromTexture(m_texture); + for (auto&& [type, texture] : blockTextures.iter_kv()) + { + if (texture) + m_blockTextures[type] = Nz::TextureAsset::CreateFromTexture(std::move(texture)); + } m_previewTextures.resize(m_blocks.size()); for (std::size_t blockIndex = 0; blockIndex < m_blocks.size(); ++blockIndex) @@ -233,10 +235,10 @@ namespace tsom Nz::TextureViewInfo slotTexView = { .viewType = Nz::ImageType::E2D, - .baseArrayLayer = previewTextures[blockIndex] + .baseArrayLayer = previewTextures[blockIndex].second }; - m_previewTextures[blockIndex] = Nz::TextureAsset::CreateView(m_baseColorTexture, slotTexView); - }*/ + m_previewTextures[blockIndex] = Nz::TextureAsset::CreateView(m_blockTextures[previewTextures[blockIndex].first], slotTexView); + } } } diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index c378ad64..13b132ee 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -51,7 +51,7 @@ namespace tsom Nz::MaterialSettings settings; settings.AddValueProperty("BaseColor", Nz::Color::White()); settings.AddValueProperty("AlphaTest", false); - settings.AddValueProperty("AlphaTestThreshold", 0.5f); + settings.AddValueProperty("AlphaTestThreshold", 0.6f); settings.AddValueProperty("ShadowMapNormalOffset", 0.f); settings.AddValueProperty("ShadowPosScale", 1.f - 0.0025f); settings.AddValueProperty("TriplanarOffset", Nz::Vector3f::Zero()); @@ -106,10 +106,10 @@ namespace tsom m_chunkMaterial = blockMaterial->Instantiate(); m_chunkMaterial->SetBufferProperty("GlobalBlockData", blockLibrary.GetGlobalBlockBuffer()); - m_chunkMaterial->SetTextureProperty("BlockTexture1", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1)), blockSampler); - //m_chunkMaterial->SetTextureProperty("BlockTexture2", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3)), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture3", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4)), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture4", Nz::TextureAsset::CreateFromTexture(blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5)), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture1", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture2", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture3", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture4", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5), blockSampler); m_chunkMaterial->SetValueProperty("ShadowPosScale", 1.f); m_chunkMaterial->SetValueProperty("AlphaTest", true); m_chunkMaterial->UpdatePassesStates({ "ShadowPass", "DistanceShadowPass" }, [](Nz::RenderStates& states) @@ -349,7 +349,7 @@ namespace tsom void ClientChunkEntities::UpdateChunkDebugCollider(const ChunkIndices& chunkIndices) { -#if 0 +#if 1 std::shared_ptr colliderModel; { entt::handle chunkEntity = Nz::Retrieve(m_chunkEntities, chunkIndices); diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index fc869f86..a9302aa6 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -55,6 +55,10 @@ namespace tsom { enum class MandatoryFeature { + BC1_sRGB, + BC3_sRGB, + BC4, + BC5, Depth32F, PersistentMapping, StorageBuffers, @@ -63,6 +67,10 @@ namespace tsom }; constexpr Nz::EnumArray s_featureNames = { + "BC1_sRGB pixel format", + "BC3_sRGB pixel format", + "BC4 pixel format", + "BC5 pixel format", "Depth32F depth-buffers", "persistent mapping", "storage buffers" @@ -81,6 +89,10 @@ namespace tsom const Nz::RenderDeviceFeatures& renderDeviceFeatures = renderDevice.GetEnabledFeatures(); Nz::EnumArray featureTests = { + renderDevice.IsTextureFormatSupported(Nz::PixelFormat::BC1_RGBA_Unorm, Nz::TextureUsage::ShaderSampling), + renderDevice.IsTextureFormatSupported(Nz::PixelFormat::BC3_Unorm, Nz::TextureUsage::ShaderSampling), + renderDevice.IsTextureFormatSupported(Nz::PixelFormat::BC4_Unorm, Nz::TextureUsage::ShaderSampling), + renderDevice.IsTextureFormatSupported(Nz::PixelFormat::BC5_Unorm, Nz::TextureUsage::ShaderSampling), renderDevice.IsTextureFormatSupported(Nz::PixelFormat::Depth32F, Nz::TextureUsage::DepthStencilAttachment), renderDeviceFeatures.persistentMapping, renderDeviceFeatures.storageBuffers @@ -297,13 +309,13 @@ namespace tsom m_blockLibrary.emplace(app); - /*ClientAssetCooker assetCooker(app); + ClientAssetCooker assetCooker(app); if (auto result = assetCooker.Cook(*m_blockLibrary); !result) { spdlog::critical("failed to cook assets: {}!", result.GetError()); app.Quit(); return false; - }*/ + } m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); From 04550e075b6fc69e2db619db43bd28fb879adcfd Mon Sep 17 00:00:00 2001 From: SirLynix Date: Fri, 3 Apr 2026 23:43:03 +0200 Subject: [PATCH 06/23] wip --- assets/shaders/BlockPBR.nzsl | 21 ++++++++++----------- src/ClientLib/ClientChunkEntities.cpp | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 2c6da8b6..c136d176 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -188,18 +188,17 @@ fn ParallaxMapping(texCoords: vec2[f32], heightMapIndices: vec2[i32], viewDir: v [export] fn ComputeColor(input: VertOut) -> vec4[f32] { - let color = MaterialData.settings.BaseColor; - let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - /*if (blockData.baseColorMapIndices.x >= 0) + let color = blockData.baseColorFallback; + if (blockData.baseColorMapIndices.x >= 0) { let uvX = input.triplanarPos.zy; let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - color *= TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); - }*/ + color = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + } const if (AlphaTest) { @@ -260,11 +259,11 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] }*/ let color = blockData.baseColorFallback; - /*if (blockData.baseColorMapIndices.x >= 0) + if (blockData.baseColorMapIndices.x >= 0) { color = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); color.rgb = Color.sRGBToLinear(color.rgb); - }*/ + } const if (AlphaTest) { @@ -273,7 +272,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] } let normal: vec3[f32]; - /*if (blockData.normalMapIndices.x >= 0) + if (blockData.normalMapIndices.x >= 0) { // https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a let blend = pow(abs(input.normal), (16.0).rrr); @@ -287,7 +286,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] clamp(tbnZ * tnormalZ, (-1.0).rrr, (1.0).rrr) * blend.z ); } - else*/ + else normal = normalize(input.normal); let albedo = color.xyz; @@ -296,7 +295,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let roughness = blockData.roughness; let ao = blockData.ambientOcclusion; - /*if (blockData.roughnessMetalnessMapIndices.x >= 0) + if (blockData.roughnessMetalnessMapIndices.x >= 0) { let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; roughness = texData.x; @@ -308,7 +307,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] { let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionHeightMapIndices).xy; ao = texData.x; - }*/ + } let F0 = vec3[f32](0.04, 0.04, 0.04); F0 = albedo * metallic + F0 * (1.0 - metallic); diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index 13b132ee..6a18b4a4 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -349,7 +349,7 @@ namespace tsom void ClientChunkEntities::UpdateChunkDebugCollider(const ChunkIndices& chunkIndices) { -#if 1 +#if 0 std::shared_ptr colliderModel; { entt::handle chunkEntity = Nz::Retrieve(m_chunkEntities, chunkIndices); From f0c7729f57f39565d0b0c9e9f6398013cef0a956 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Sat, 4 Apr 2026 21:36:37 +0200 Subject: [PATCH 07/23] lua wip --- include/ClientLib/ClientAssetCookRegistry.hpp | 8 +- include/ClientLib/ClientAssetCooker.hpp | 6 +- .../ClientAssetLibraryAppComponent.inl | 2 +- include/ClientLib/ClientBlockLibrary.hpp | 2 +- include/ClientLib/ClientFramePipeline.hpp | 2 +- include/CommonLib/Planet.hpp | 2 +- .../Scripting/BaseScriptingLibrary.hpp | 35 +++ .../Scripting/BaseScriptingLibrary.inl | 7 + .../Scripting/MathScriptingLibrary.hpp | 8 - .../CommonLib/Scripting/ScriptingUtils.inl | 14 +- scripts/assets/computer.lua | 6 +- scripts/assets/tree.lua | 2 +- scripts/commands/flood_fill.lua | 4 +- scripts/entities/atmosphere_sensor.lua | 8 +- scripts/entities/computer.lua | 2 +- scripts/entities/tree.lua | 2 +- scripts/libraries/box.lua | 26 +++ scripts/libraries/color.lua | 24 +++ scripts/libraries/eulerangles.lua | 11 + scripts/libraries/quaternion.lua | 33 +++ scripts/libraries/vec2.lua | 66 ++++++ scripts/libraries/vec3.lua | 71 +++++++ scripts/planets/alice.lua | 4 +- scripts/planets/bob.lua | 52 ----- scripts/planets/test.lua | 4 +- scripts/planets/torus.lua | 10 +- src/ClientLib/BlockSelectionBar.cpp | 2 +- src/ClientLib/ClientAssetCookRegistry.cpp | 2 +- src/ClientLib/ClientBlockLibrary.cpp | 4 +- src/ClientLib/ClientSessionHandler.cpp | 3 + src/CommonLib/Planet.cpp | 18 +- .../Scripting/BaseScriptingLibrary.cpp | 57 +++++ .../Scripting/MathScriptingLibrary.cpp | 200 ------------------ src/Game/GameAppComponent.cpp | 2 +- src/Game/States/GameState.cpp | 8 +- src/ServerLib/ServerInstance.cpp | 3 + src/ServerLib/ServerPlayer.cpp | 4 + 37 files changed, 399 insertions(+), 315 deletions(-) create mode 100644 include/CommonLib/Scripting/BaseScriptingLibrary.hpp create mode 100644 include/CommonLib/Scripting/BaseScriptingLibrary.inl create mode 100644 scripts/libraries/box.lua create mode 100644 scripts/libraries/color.lua create mode 100644 scripts/libraries/eulerangles.lua create mode 100644 scripts/libraries/quaternion.lua create mode 100644 scripts/libraries/vec2.lua create mode 100644 scripts/libraries/vec3.lua create mode 100644 src/CommonLib/Scripting/BaseScriptingLibrary.cpp diff --git a/include/ClientLib/ClientAssetCookRegistry.hpp b/include/ClientLib/ClientAssetCookRegistry.hpp index 1aeed4aa..810d1007 100644 --- a/include/ClientLib/ClientAssetCookRegistry.hpp +++ b/include/ClientLib/ClientAssetCookRegistry.hpp @@ -4,13 +4,13 @@ #pragma once -#ifndef TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP -#define TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP +#ifndef TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP +#define TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP #include +#include #include #include -#include #include #include #include @@ -75,4 +75,4 @@ namespace tsom #include -#endif // TSOM_CLIENTLIB_CLIENTCHUNKCOOKREGISTRY_HPP +#endif // TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP diff --git a/include/ClientLib/ClientAssetCooker.hpp b/include/ClientLib/ClientAssetCooker.hpp index 56b034cc..ac700918 100644 --- a/include/ClientLib/ClientAssetCooker.hpp +++ b/include/ClientLib/ClientAssetCooker.hpp @@ -4,8 +4,8 @@ #pragma once -#ifndef TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP -#define TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP +#ifndef TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP +#define TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP #include #include @@ -39,4 +39,4 @@ namespace tsom #include -#endif // TSOM_CLIENTLIB_CLIENTCHUNKCOOKER_HPP +#endif // TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP diff --git a/include/ClientLib/ClientAssetLibraryAppComponent.inl b/include/ClientLib/ClientAssetLibraryAppComponent.inl index 8a7cd39a..12d443f6 100644 --- a/include/ClientLib/ClientAssetLibraryAppComponent.inl +++ b/include/ClientLib/ClientAssetLibraryAppComponent.inl @@ -23,7 +23,7 @@ namespace tsom { return m_textureLibrary.Get(name); } - + inline std::shared_ptr ClientAssetLibraryAppComponent::QueryFont(std::string_view name) const { return m_fontLibrary.Query(name); diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index f7d82c00..13197847 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -8,8 +8,8 @@ #define TSOM_CLIENTLIB_CLIENTBLOCKLIBRARY_HPP #include -#include #include +#include namespace Nz { diff --git a/include/ClientLib/ClientFramePipeline.hpp b/include/ClientLib/ClientFramePipeline.hpp index dd81297c..241166f3 100644 --- a/include/ClientLib/ClientFramePipeline.hpp +++ b/include/ClientLib/ClientFramePipeline.hpp @@ -7,8 +7,8 @@ #ifndef TSOM_CLIENTLIB_CLIENTFRAMEPIPELINE_HPP #define TSOM_CLIENTLIB_CLIENTFRAMEPIPELINE_HPP -#include #include +#include namespace tsom { diff --git a/include/CommonLib/Planet.hpp b/include/CommonLib/Planet.hpp index 71afe62c..1aef4a58 100644 --- a/include/CommonLib/Planet.hpp +++ b/include/CommonLib/Planet.hpp @@ -12,8 +12,8 @@ #include #include #include -#include #include +#include #include #include #include diff --git a/include/CommonLib/Scripting/BaseScriptingLibrary.hpp b/include/CommonLib/Scripting/BaseScriptingLibrary.hpp new file mode 100644 index 00000000..aab0bfde --- /dev/null +++ b/include/CommonLib/Scripting/BaseScriptingLibrary.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_COMMONLIB_SCRIPTING_BASESCRIPTINGLIBRARY_HPP +#define TSOM_COMMONLIB_SCRIPTING_BASESCRIPTINGLIBRARY_HPP + +#include +#include + +namespace tsom +{ + class TSOM_COMMONLIB_API BaseScriptingLibrary : public ScriptingLibrary + { + public: + BaseScriptingLibrary() = default; + BaseScriptingLibrary(const BaseScriptingLibrary&) = delete; + BaseScriptingLibrary(BaseScriptingLibrary&&) = delete; + ~BaseScriptingLibrary() = default; + + void Register(sol::state& state) override; + + BaseScriptingLibrary& operator=(const BaseScriptingLibrary&) = delete; + BaseScriptingLibrary& operator=(BaseScriptingLibrary&&) = delete; + + private: + void RegisterTime(sol::state& state); + }; +} + +#include + +#endif // TSOM_COMMONLIB_SCRIPTING_BASESCRIPTINGLIBRARY_HPP diff --git a/include/CommonLib/Scripting/BaseScriptingLibrary.inl b/include/CommonLib/Scripting/BaseScriptingLibrary.inl new file mode 100644 index 00000000..aa43beb0 --- /dev/null +++ b/include/CommonLib/Scripting/BaseScriptingLibrary.inl @@ -0,0 +1,7 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +namespace tsom +{ +} diff --git a/include/CommonLib/Scripting/MathScriptingLibrary.hpp b/include/CommonLib/Scripting/MathScriptingLibrary.hpp index 9d3ab633..ef0b6037 100644 --- a/include/CommonLib/Scripting/MathScriptingLibrary.hpp +++ b/include/CommonLib/Scripting/MathScriptingLibrary.hpp @@ -26,15 +26,7 @@ namespace tsom MathScriptingLibrary& operator=(MathScriptingLibrary&&) = delete; private: - template void RegisterBox(sol::state& state, const char* name); - void RegisterColor(sol::state& state); - template void RegisterEulerAngles(sol::state& state, const char* name); void RegisterPerlinNoise(sol::state& state); - template void RegisterQuaternion(sol::state& state, const char* name); - void RegisterSignedDistance(sol::state& state); - void RegisterTime(sol::state& state); - template void RegisterVector2(sol::state& state, const char* name); - template void RegisterVector3(sol::state& state, const char* name); }; } diff --git a/include/CommonLib/Scripting/ScriptingUtils.inl b/include/CommonLib/Scripting/ScriptingUtils.inl index 5e3f252a..c329c9d1 100644 --- a/include/CommonLib/Scripting/ScriptingUtils.inl +++ b/include/CommonLib/Scripting/ScriptingUtils.inl @@ -227,9 +227,9 @@ namespace sol { int absoluteIndex = lua_absindex(L, index); - sol::table rect = sol::stack::get(L, absoluteIndex); - T x = rect["x"]; - T y = rect["y"]; + sol::table vec2 = sol::stack::get(L, absoluteIndex); + T x = vec2["x"]; + T y = vec2["y"]; tracking.use(1); @@ -241,10 +241,10 @@ namespace sol { int absoluteIndex = lua_absindex(L, index); - sol::table rect = sol::stack::get(L, absoluteIndex); - T x = rect["x"]; - T y = rect["y"]; - T z = rect["z"]; + sol::table vec3 = sol::stack::get(L, absoluteIndex); + T x = vec3["x"]; + T y = vec3["y"]; + T z = vec3["z"]; tracking.use(1); diff --git a/scripts/assets/computer.lua b/scripts/assets/computer.lua index ef81de30..aa24720f 100644 --- a/scripts/assets/computer.lua +++ b/scripts/assets/computer.lua @@ -1,9 +1,9 @@ local params = { mesh = { center = true, - --texCoordScale = Vec2f(-1.0, 1.0), - vertexRotation = EulerAnglesf(180, 0, 0), - vertexScale = Vec3f(1.0 / 500.0) * Vec3f(1, -1, -1) + --texCoordScale = Vec2(-1.0, 1.0), + vertexRotation = EulerAngles(180, 0, 0), + vertexScale = Vec3(1.0 / 500.0) * Vec3(1, -1, -1) }, loadMaterials = false } diff --git a/scripts/assets/tree.lua b/scripts/assets/tree.lua index 296e6b57..df4ffff0 100644 --- a/scripts/assets/tree.lua +++ b/scripts/assets/tree.lua @@ -27,7 +27,7 @@ for treeName, treeData in pairs(trees) do end local aabb = treeMesh:GetAABB() - treeMesh:Translate(-aabb:GetCenter() + Vec3f(0, aabb.width * 0.5, 0)) + treeMesh:Translate(-aabb:GetCenter() + Vec3(0, aabb.width * 0.5, 0)) local treeModel = Model.BuildFromMesh(treeMesh) AssetLibrary.RegisterModel(treeName, treeModel) diff --git a/scripts/commands/flood_fill.lua b/scripts/commands/flood_fill.lua index 80b24cfe..d8d16ee9 100644 --- a/scripts/commands/flood_fill.lua +++ b/scripts/commands/flood_fill.lua @@ -24,7 +24,7 @@ return function () local eyePos = controller:GetEyePosition() local cameraRot = controller:GetCameraRotation() - local result = physWorld:RaycastQueryFirst(eyePos, eyePos + cameraRot * Vec3f(0, 0, -10), { IgnorePlayers = true }) + local result = physWorld:RaycastQueryFirst(eyePos, eyePos + cameraRot * Vec3(0, 0, -10), { IgnorePlayers = true }) if not result.hitEntity or not result.hitChunk then print("no chunk hit") @@ -102,7 +102,7 @@ return function () local updatedChunks = {} - local toProcess = math.max(remaining // 50, 1) + local toProcess = math.max(math.floor(remaining / 50), 1) for i = 1, toProcess do local firstBlock = table.remove(pendingList, 1) diff --git a/scripts/entities/atmosphere_sensor.lua b/scripts/entities/atmosphere_sensor.lua index 99f10d10..63619e1b 100644 --- a/scripts/entities/atmosphere_sensor.lua +++ b/scripts/entities/atmosphere_sensor.lua @@ -9,7 +9,7 @@ classData:On("init", function (self) local physSettings = { kind = "dynamic", mass = 10.0, - collider = BoxCollider3D.new(Vec3f(0.75)), + collider = BoxCollider3D.new(Vec3(0.75)), objectLayer = Constants.ObjectLayerDynamic } @@ -48,9 +48,9 @@ else local n2 = self:GetProperty("sensor_n2") local sum = math.max(o2 + co2 + n2, 1) -- avoid division by zero - local o2_pct = o2 * 100 // sum - local co2_pct = co2 * 100 // sum - local n2_pct = n2 * 100 // sum + local o2_pct = math.floor(o2 * 100 / sum) + local co2_pct = math.floor(co2 * 100 / sum) + local n2_pct = math.floor(n2 * 100 / sum) self:SetInteractibleText(string.format("Oxygen: %.2fL (%d%%)\nCarbon dioxyde: %.2fL (%d%%)\nNitrogen: %.2fL (%d%%)\n", o2 / 1000, o2_pct, co2 / 1000, co2_pct, n2 / 1000, n2_pct)) end diff --git a/scripts/entities/computer.lua b/scripts/entities/computer.lua index a5f22cd2..0771b408 100644 --- a/scripts/entities/computer.lua +++ b/scripts/entities/computer.lua @@ -4,7 +4,7 @@ classData:On("init", function (self) local physSettings = { kind = "static", mass = 0.0, - collider = BoxCollider3D.new(Vec3f(0.5)), + collider = BoxCollider3D.new(Vec3(0.5)), objectLayer = Constants.ObjectLayerStatic } diff --git a/scripts/entities/tree.lua b/scripts/entities/tree.lua index cc53ce47..370c596d 100644 --- a/scripts/entities/tree.lua +++ b/scripts/entities/tree.lua @@ -12,7 +12,7 @@ classData:On("init", function (self) gfx:AttachRenderable(model, Constants.RenderMask3D) local node = self:GetComponent("node") - node:Scale(Vec3f(self:GetProperty("scale"))) + node:Scale(Vec3(self:GetProperty("scale"))) end if SERVER then diff --git a/scripts/libraries/box.lua b/scripts/libraries/box.lua new file mode 100644 index 00000000..d4feaefc --- /dev/null +++ b/scripts/libraries/box.lua @@ -0,0 +1,26 @@ + +local BoxMt = CreateMetatable("box") +BoxMt.__index = BoxMt + +function BoxMt:GetCenter() + return self:GetPosition() + self:GetLengths() * 0.5 +end + +function BoxMt:GetPosition() + return Vec3(self.x, self.y, self.z) +end + +function BoxMt:GetLengths() + return Vec3(self.width, self.height, self.depth) +end + +function Box(x, y, z, width, height, depth) + return setmetatable({ + x = x, + y = y, + z = z, + width = width, + height = height, + depth = depth + }, BoxMt) +end diff --git a/scripts/libraries/color.lua b/scripts/libraries/color.lua new file mode 100644 index 00000000..a8eed385 --- /dev/null +++ b/scripts/libraries/color.lua @@ -0,0 +1,24 @@ + +local ColorMt = CreateMetatable("color") +ColorMt.__index = ColorMt + +function ColorMt:GetCenter() + return self:GetPosition() + self:GetLengths() * 0.5 +end + +function ColorMt:GetPosition() + return Vec3(self.x, self.y, self.z) +end + +function ColorMt:GetLengths() + return Vec3(self.width, self.height, self.depth) +end + +function Color(r, g, b, a) + return setmetatable({ + r = r, + g = g, + b = b, + a = a or 1.0 + }, ColorMt) +end diff --git a/scripts/libraries/eulerangles.lua b/scripts/libraries/eulerangles.lua new file mode 100644 index 00000000..356c1484 --- /dev/null +++ b/scripts/libraries/eulerangles.lua @@ -0,0 +1,11 @@ + +local EulerAnglesMt = CreateMetatable("eulerangles") +EulerAnglesMt.__index = EulerAnglesMt + +function Color(pitch, yaw, roll) + return setmetatable({ + pitch = pitch, + yaw = yaw, + roll = roll + }, EulerAnglesMt) +end diff --git a/scripts/libraries/quaternion.lua b/scripts/libraries/quaternion.lua new file mode 100644 index 00000000..0c4fb7b7 --- /dev/null +++ b/scripts/libraries/quaternion.lua @@ -0,0 +1,33 @@ + +local QuaternionMt = CreateMetatable("quaternion") +QuaternionMt.__index = QuaternionMt + +function QuaternionMt:GetConjugate() + return Quaternion(-self.x, -self.y, -self.z, self.w) +end + +function QuaternionMt:__mul(quat) + assert(type(quat) == "userdata") + local mt = getmetatable(quat) + if mt == QuaternionMt then + return Quaternion( + self.w * quat.w - self.x * quat.x - self.y * quat.y - self.z * quat.z, + self.w * quat.x + self.x * quat.w + self.y * quat.z - self.z * quat.y, + self.w * quat.y + self.y * quat.w + self.z * quat.x - self.x * quat.z, + self.w * quat.z + self.z * quat.w + self.x * quat.y - self.y * quat.x + ) + else + local quatVec = Vec3(self.x, self.y, self.z) + local uv = quatVec.CrossProduct(quat) + local uuv = quatVec.CrossProduct(uv) + + uv = uv * 2.0 * self.w + uuv = uuv * 2.0 + + return quat + uv + uuv + end +end + +function Quaternion(x, y, z, w) + return setmetatable({x = x, y = y, z = z, w = w}, QuaternionMt) +end diff --git a/scripts/libraries/vec2.lua b/scripts/libraries/vec2.lua new file mode 100644 index 00000000..9d58e484 --- /dev/null +++ b/scripts/libraries/vec2.lua @@ -0,0 +1,66 @@ + +local Vec2Mt = CreateMetatable("vec2") +Vec2Mt.__index = Vec2Mt + +function Vec2Mt:DotProduct(vec) + return self.x * vec.x + self.y * vec.y +end + +function Vec2Mt:GetAbs() + return Vec2(math.abs(self.x), math.abs(self.y)) +end + +function Vec2Mt:GetLength() + return math.sqrt(self.x * self.x + self.y * self.y) +end + +function Vec2Mt:GetNormal() + local length = self:GetLength() + return Vec2(self.x / length, self.y / length), length +end + +function Vec2Mt:Maximize(vec) + return Vec2(math.max(self.x, vec.x), math.max(self.y, vec.y)) +end + +function Vec2Mt:Minimize(vec) + return Vec2(math.min(self.x, vec.x), math.min(self.y, vec.y)) +end + +function Vec2Mt:__add(vec) + return Vec2(self.x + vec.x, self.y + vec.y) +end + +function Vec2Mt:__sub(vec) + return Vec2(self.x - vec.x, self.y - vec.y) +end + +function Vec2Mt:__mul(vec) + if type(vec) == "number" then + return Vec2(self.x * vec, self.y * vec) + else + return Vec2(self.x * vec.x, self.y * vec.y) + end +end + +function Vec2Mt:__div(vec) + if type(vec) == "number" then + return Vec2(self.x / vec, self.y / vec) + else + return Vec2(self.x / vec.x, self.y / vec.y) + end +end + +local Vec2ClassMt = {} + +function Vec2ClassMt.__call(t, x, y) + return setmetatable({x = x, y = y or x}, Vec2Mt) +end + +Vec2 = {} + +function Vec2.Distance(vec1, vec2) + return (vec2 - vec1):GetLength() +end + +setmetatable(Vec2, Vec2ClassMt) diff --git a/scripts/libraries/vec3.lua b/scripts/libraries/vec3.lua new file mode 100644 index 00000000..96d3b63e --- /dev/null +++ b/scripts/libraries/vec3.lua @@ -0,0 +1,71 @@ + +local Vec3Mt = CreateMetatable("vec3") +Vec3Mt.__index = Vec3Mt + +function Vec3Mt:CrossProduct(vec) + return Vec3(self.y * vec.z - self.z * vec.y, self.z * vec.x - self.x * vec.z, self.x * vec.y - self.y * vec.x) +end + +function Vec3Mt:DotProduct(vec) + return self.x * vec.x + self.y * vec.y + self.z * vec.z +end + +function Vec3Mt:GetAbs() + return Vec3(math.abs(self.x), math.abs(self.y), math.abs(self.z)) +end + +function Vec3Mt:GetLength() + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) +end + +function Vec3Mt:GetNormal() + local length = self:GetLength() + return Vec3(self.x / length, self.y / length, self.z / length), length +end + +function Vec3Mt:Maximize(vec) + return Vec3(math.max(self.x, vec.x), math.max(self.y, vec.y), math.max(self.z, vec.z)) +end + +function Vec3Mt:Minimize(vec) + return Vec3(math.min(self.x, vec.x), math.min(self.y, vec.y), math.min(self.z, vec.z)) +end + +function Vec3Mt:__add(vec) + return Vec3(self.x + vec.x, self.y + vec.y, self.z + vec.z) +end + +function Vec3Mt:__sub(vec) + return Vec3(self.x - vec.x, self.y - vec.y, self.z - vec.z) +end + +function Vec3Mt:__mul(vec) + if type(vec) == "number" then + return Vec3(self.x * vec, self.y * vec, self.z * vec) + else + return Vec3(self.x * vec.x, self.y * vec.y, self.z * vec.z) + end +end + +function Vec3Mt:__div(vec) + if type(vec) == "number" then + return Vec3(self.x / vec, self.y / vec, self.z / vec) + else + return Vec3(self.x / vec.x, self.y / vec.y, self.z / vec.z) + end +end + +local Vec3ClassMt = {} + +function Vec3ClassMt.__call(t, x, y, z) + return setmetatable({x = x, y = y or x, z = z or x}, Vec3Mt) +end + +Vec3 = {} +Vec3.Metatable = Vec3Mt + +function Vec3.Distance(vec1, vec2) + return (vec2 - vec1):GetLength() +end + +setmetatable(Vec3, Vec3ClassMt) diff --git a/scripts/planets/alice.lua b/scripts/planets/alice.lua index 7061ab7d..7bcd12e8 100644 --- a/scripts/planets/alice.lua +++ b/scripts/planets/alice.lua @@ -46,10 +46,10 @@ return function (chunk, seed, chunkDims) for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) - local blockPosScaled = Vec3f(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosScaled = Vec3(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) local blockPosNorm, distToCenter = blockPosScaled:GetNormal() --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) - distToCenter = SignedDistance.RoundBox(blockPosScaled, Vec3f(baseHeight), 16.0) + distToCenter = SignedDistance.RoundBox(blockPosScaled, Vec3(baseHeight), 16.0) if distToCenter > baseFreeHeight then table.insert(content, emptyBlock) diff --git a/scripts/planets/bob.lua b/scripts/planets/bob.lua index 039fbb7d..09db4e74 100644 --- a/scripts/planets/bob.lua +++ b/scripts/planets/bob.lua @@ -5,58 +5,6 @@ local chunksize = 32 local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight -local Vec3mt = CreateMetatable("vec3") -Vec3mt.__index = Vec3mt - -function Vec3mt:__add(vec) - return Vec3(self.x + vec.x, self.y + vec.y, self.z + vec.z) -end - -function Vec3mt:__sub(vec) - return Vec3(self.x - vec.x, self.y - vec.y, self.z - vec.z) -end - -function Vec3mt:__mul(vec) - if type(vec) == "number" then - return Vec3(self.x * vec, self.y * vec, self.z * vec) - else - return Vec3(self.x * vec.x, self.y * vec.y, self.z * vec.z) - end -end - -function Vec3mt:__div(vec) - if type(vec) == "number" then - return Vec3(self.x / vec, self.y / vec, self.z / vec) - else - return Vec3(self.x / vec.x, self.y / vec.y, self.z / vec.z) - end -end - -function Vec3mt:GetAbs() - return Vec3(math.abs(self.x), math.abs(self.y), math.abs(self.z)) -end - -function Vec3mt:GetLength() - return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) -end - -function Vec3mt:GetNormal() - local length = self:GetLength() - return Vec3(self.x / length, self.y / length, self.z / length), length -end - -function Vec3mt:Maximize(vec) - return Vec3(math.max(self.x, vec.x), math.max(self.y, vec.y), math.max(self.z, vec.z)) -end - -function Vec3mt:Minimize(vec) - return Vec3(math.min(self.x, vec.x), math.min(self.y, vec.y), math.min(self.z, vec.z)) -end - -function Vec3(x, y, z) - return setmetatable({x = x, y = y or x, z = z or x}, Vec3mt) -end - local function GetBlockIndices(chunkIndices, blockIndices) local x = chunkIndices.x * chunksize + blockIndices.x - chunksize * 0.5 local y = chunkIndices.y * chunksize + blockIndices.z - chunksize * 0.5 diff --git a/scripts/planets/test.lua b/scripts/planets/test.lua index 275cc1c6..9cfd6273 100644 --- a/scripts/planets/test.lua +++ b/scripts/planets/test.lua @@ -49,9 +49,9 @@ return function (chunk, seed, chunkDims) for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do local blockPos = planet:GetBlockIndices(chunkIndices, Vec3ui(x, y, z)) - local blockPosNorm, distToCenter = Vec3f(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5):GetNormal() + local blockPosNorm, distToCenter = Vec3(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5):GetNormal() --distToCenter = math.max(math.abs(blockPos.x * 0.25 + 0.5), math.abs(blockPos.y * 0.25 + 0.5), math.abs(blockPos.z * 0.25 + 0.5)) - --distToCenter = roundBox(Vec3f(blockPos.x * 0.25, blockPos.z * 0.25, blockPos.y * 0.25), Vec3f(baseHeight, baseHeight, baseHeight), 16.0) + --distToCenter = roundBox(Vec3(blockPos.x * 0.25, blockPos.z * 0.25, blockPos.y * 0.25), Vec3(baseHeight, baseHeight, baseHeight), 16.0) if distToCenter > 60 then table.insert(content, emptyBlock) diff --git a/scripts/planets/torus.lua b/scripts/planets/torus.lua index 1c347cf6..6ea66320 100644 --- a/scripts/planets/torus.lua +++ b/scripts/planets/torus.lua @@ -5,12 +5,12 @@ local freespace = 30 -- https://iquilezles.org/articles/distfunctions/ local function sdTorus(p, t) - local q = Vec2f(Vec2f(p.x, p.z):GetLength() - t.x, p.y) + local q = Vec2(Vec2(p.x, p.z):GetLength() - t.x, p.y) return q:GetLength() - t.y end local function sdOctahedron(p, s) - p = Vec3f(math.abs(p.x), math.abs(p.y), math.abs(p.z)) + p = Vec3(math.abs(p.x), math.abs(p.y), math.abs(p.z)) return (p.x+p.y+p.z-s)*0.57735027; end @@ -23,7 +23,7 @@ float sdCappedCylinder( vec3 p, float r, float h ) ]] local function sdCappedCylinder(p, r, h) - local d = Vec2f(Vec2f(p.x, p.z):GetLength(), math.abs(p.y)) - Vec2f(r, h) + local d = Vec2(Vec2(p.x, p.z):GetLength(), math.abs(p.y)) - Vec2(r, h) d.x = math.max(d.x, 0.0) d.y = math.max(d.y, 0.0) return math.min(math.max(d.x, d.y),0.0) + d:GetLength() @@ -61,7 +61,7 @@ return function (chunk, seed, chunkcount) maxHeight.y - math.abs(blockPos.y), maxHeight.z - math.abs(blockPos.z) )]] - --[[local depth = maxHeight.x - math.max(0, sdTorus(blockPos, Vec2f(20, 50))) + --[[local depth = maxHeight.x - math.max(0, sdTorus(blockPos, Vec2(20, 50))) if (depth < freespace) then table.insert(content, empty) @@ -93,7 +93,7 @@ return function (chunk, seed, chunkcount) table.insert(content, blockIndex)]] - local dist = sdTorus(blockPos, Vec2f(50, 20)) + local dist = sdTorus(blockPos, Vec2(50, 20)) if dist < 0 then table.insert(content, dirt) else diff --git a/src/ClientLib/BlockSelectionBar.cpp b/src/ClientLib/BlockSelectionBar.cpp index 96697c57..c93e538c 100644 --- a/src/ClientLib/BlockSelectionBar.cpp +++ b/src/ClientLib/BlockSelectionBar.cpp @@ -4,10 +4,10 @@ #include #include -#include #include #include #include +#include namespace tsom { diff --git a/src/ClientLib/ClientAssetCookRegistry.cpp b/src/ClientLib/ClientAssetCookRegistry.cpp index 21f3a8e7..3d36ae37 100644 --- a/src/ClientLib/ClientAssetCookRegistry.cpp +++ b/src/ClientLib/ClientAssetCookRegistry.cpp @@ -16,7 +16,7 @@ namespace nlohmann j.at("b").get_to(color.b); color.a = j.value("a", 1.0f); } - + NLOHMANN_JSON_SERIALIZE_ENUM(tsom::ClientAssetCookRegistry::TextureType, { {tsom::ClientAssetCookRegistry::TextureType::None, "none"}, {tsom::ClientAssetCookRegistry::TextureType::BC1, "bc1"}, diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index 7dca385b..910dab0f 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -3,12 +3,12 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include -#include #include #include #include #include #include +#include #include #include @@ -145,7 +145,7 @@ namespace tsom // BC1/BC3 sRGB formats are not well supported with OpenGL, sRGB to linear conversion is done in shader // TODO: Add a shader option to use sRGB formats if supported to avoid conversion cost constexpr Nz::EnumArray textureFormat = { - Nz::PixelFormat::BC1_RGBA_Unorm, + Nz::PixelFormat::BC1_RGBA_Unorm, Nz::PixelFormat::BC3_Unorm, Nz::PixelFormat::BC4_Unorm, Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm diff --git a/src/ClientLib/ClientSessionHandler.cpp b/src/ClientLib/ClientSessionHandler.cpp index d64dd450..9beb6541 100644 --- a/src/ClientLib/ClientSessionHandler.cpp +++ b/src/ClientLib/ClientSessionHandler.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -78,8 +79,10 @@ namespace tsom SetupHandlerTable(this); SetupAttributeTable(s_packetAttributes); + m_scriptingContext.RegisterLibrary(); m_scriptingContext.RegisterLibrary(); m_scriptingContext.RegisterLibrary(m_app); + m_scriptingContext.LoadDirectory("scripts/libraries"); m_scriptingContext.LoadDirectory("scripts/assets"); m_entityRegistry.RegisterClassLibrary(m_app, config, m_blockLibrary); diff --git a/src/CommonLib/Planet.cpp b/src/CommonLib/Planet.cpp index e4847071..16dfd02a 100644 --- a/src/CommonLib/Planet.cpp +++ b/src/CommonLib/Planet.cpp @@ -6,15 +6,16 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include -#include #include #include @@ -247,9 +248,12 @@ namespace tsom ChunkGenerator& chunkGenerator = m_chunkGenerators.GetOrCreate(created, m_app); if (created) { + chunkGenerator.scriptingContext.RegisterLibrary(); chunkGenerator.scriptingContext.RegisterLibrary(); chunkGenerator.scriptingContext.RegisterLibrary(); + chunkGenerator.scriptingContext.LoadDirectory("scripts/libraries"); + Nz::Result execResult = chunkGenerator.scriptingContext.LoadFile(fmt::format("scripts/planets/{}.lua", scriptName)); if (!execResult) return; @@ -383,7 +387,7 @@ namespace tsom end local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) - + if distToCenter <= -32.0 then if blockPresence >= 0.3 and blockPresence <= 0.7 then if distToCenter <= -5 then @@ -404,10 +408,10 @@ namespace tsom else mountainous = 1 end - + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) - + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) local spikeHeight if baseSpikeHeight < 0.7 then @@ -418,9 +422,9 @@ namespace tsom spikeHeight = 1 end spikeHeight = (1-mountainous) * spikeHeight * 20 - + local height = heightVariation1 + heightVariation2 + spikeHeight - + if distToCenter <= height then if distToCenter >= height - spikeHeight then table.insert(content, stoneMossyBlock) @@ -437,7 +441,7 @@ namespace tsom table.insert(content, emptyBlock) end end - + ::continue:: end end diff --git a/src/CommonLib/Scripting/BaseScriptingLibrary.cpp b/src/CommonLib/Scripting/BaseScriptingLibrary.cpp new file mode 100644 index 00000000..ca0a9c0f --- /dev/null +++ b/src/CommonLib/Scripting/BaseScriptingLibrary.cpp @@ -0,0 +1,57 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include + +namespace tsom +{ + void BaseScriptingLibrary::Register(sol::state& state) + { + state["CreateMetatable"] = [](sol::this_state L, const char* metaname) + { + if (luaL_newmetatable(L, metaname) == 0) + { + lua_pop(L, 1); + TriggerLuaArgError(L, 1, fmt::format("Metatable %s already exists", metaname)); + } + + return sol::stack_table(L); + }; + + state["GetMetatable"]= [](sol::this_state L, const char* metaname) + { + luaL_getmetatable(L, metaname); + return sol::stack_table(L); + }; + + RegisterTime(state); + } + + void BaseScriptingLibrary::RegisterTime(sol::state& state) + { + state.new_usertype("Time", + sol::no_constructor, + "AsMicroseconds", &Nz::Time::AsMicroseconds, + "AsMilliseconds", &Nz::Time::AsMilliseconds, + "AsNanoseconds", &Nz::Time::AsNanoseconds, + "AsSeconds", &Nz::Time::AsSeconds, + "Milliseconds", &Nz::Time::Milliseconds, + "Microseconds", &Nz::Time::Microseconds, + "Nanosecond", &Nz::Time::Nanosecond, + "Second", &Nz::Time::Second, + "Seconds", &Nz::Time::Seconds, + "Zero", &Nz::Time::Zero, + sol::meta_function::equal_to, [](Nz::Time t1, Nz::Time t2) { return t1 == t2; }, + sol::meta_function::less_than, [](Nz::Time t1, Nz::Time t2) { return t1 < t2; }, + sol::meta_function::less_than_or_equal_to, [](Nz::Time t1, Nz::Time t2) { return t1 <= t2; }, + sol::meta_function::addition, [](Nz::Time t1, Nz::Time t2) { return t1 + t2; }, + sol::meta_function::subtraction, [](Nz::Time t1, Nz::Time t2) { return t1 - t2; }, + sol::meta_function::unary_minus, [](Nz::Time t) { return -t; } + ); + } +} diff --git a/src/CommonLib/Scripting/MathScriptingLibrary.cpp b/src/CommonLib/Scripting/MathScriptingLibrary.cpp index 4b2005fa..e44019af 100644 --- a/src/CommonLib/Scripting/MathScriptingLibrary.cpp +++ b/src/CommonLib/Scripting/MathScriptingLibrary.cpp @@ -5,14 +5,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -22,79 +14,7 @@ namespace tsom void MathScriptingLibrary::Register(sol::state& state) { state["DirectionFromNormal"] = &DirectionFromNormal; - state["CreateMetatable"] = [](sol::this_state L, const char* metaname) - { - if (luaL_newmetatable(L, metaname) == 0) - { - lua_pop(L, 1); - TriggerLuaArgError(L, 1, fmt::format("Metatable %s already exists", metaname)); - } - - return sol::stack_table(L); - }; - - state["GetMetatable"]= [](sol::this_state L, const char* metaname) - { - luaL_getmetatable(L, metaname); - return sol::stack_table(L); - }; - - RegisterBox(state, "Boxf"); - RegisterBox(state, "Boxi"); - RegisterBox(state, "Boxui"); - RegisterColor(state); - RegisterEulerAngles(state, "EulerAnglesf"); RegisterPerlinNoise(state); - RegisterQuaternion(state, "Quaternionf"); - RegisterSignedDistance(state); - RegisterTime(state); - /*RegisterVector2(state, "Vec2f"); - RegisterVector2(state, "Vec2i"); - RegisterVector2(state, "Vec2ui"); - RegisterVector3(state, "Vec3f"); - RegisterVector3(state, "Vec3i"); - RegisterVector3(state, "Vec3ui");*/ - } - - template - void MathScriptingLibrary::RegisterBox(sol::state& state, const char* name) - { - state.new_usertype>(name, - sol::call_constructor, sol::constructors(), Nz::Box(T, T, T), Nz::Box(const Nz::Vector3& pos, const Nz::Vector3& lengths), Nz::Box(const Nz::Box&)>(), - "GetCenter", &Nz::Box::GetCenter, - "GetLengths", &Nz::Box::GetLengths, - "x", &Nz::Box::x, - "y", &Nz::Box::y, - "z", &Nz::Box::z, - "width", &Nz::Box::width, - "height", &Nz::Box::height, - "depth", &Nz::Box::depth, - sol::meta_function::to_string, &Nz::Box::ToString - ); - } - - void MathScriptingLibrary::RegisterColor(sol::state& state) - { - state.new_usertype("Color", - sol::call_constructor, sol::constructors(), - "r", &Nz::Color::r, - "g", &Nz::Color::g, - "b", &Nz::Color::b, - "a", &Nz::Color::a, - sol::meta_function::to_string, &Nz::Color::ToString - ); - } - - template - void MathScriptingLibrary::RegisterEulerAngles(sol::state& state, const char* name) - { - state.new_usertype>(name, - sol::call_constructor, sol::constructors(), Nz::EulerAngles(T, T, T), Nz::EulerAngles(const Nz::EulerAngles&)>(), - "pitch", &Nz::EulerAngles::pitch, - "yaw", &Nz::EulerAngles::yaw, - "roll", &Nz::EulerAngles::roll, - sol::meta_function::to_string, &Nz::EulerAngles::ToString - ); } void MathScriptingLibrary::RegisterPerlinNoise(sol::state& state) @@ -133,125 +53,5 @@ namespace tsom "normalizedOctave2D_01", LuaFunction(&siv::PerlinNoise::normalizedOctave2D_01), "normalizedOctave3D_01", LuaFunction(&siv::PerlinNoise::normalizedOctave3D_01) ); - - } - - template - void MathScriptingLibrary::RegisterQuaternion(sol::state& state, const char* name) - { - state.new_usertype>(name, - sol::call_constructor, sol::constructors(), Nz::Quaternion(T, T, T, T), Nz::Quaternion(const Nz::Quaternion&)>(), - "GetConjugate", &Nz::Quaternion::GetConjugate, - "x", &Nz::Quaternion::x, - "y", &Nz::Quaternion::y, - "z", &Nz::Quaternion::z, - "w", &Nz::Quaternion::w, - sol::meta_function::multiplication, sol::overload(Nz::Overload&>(&Nz::Quaternion::operator*), Nz::Overload&>(&Nz::Quaternion::operator*)), - sol::meta_function::to_string, &Nz::Quaternion::ToString - ); - } - - void MathScriptingLibrary::RegisterSignedDistance(sol::state& state) - { - state.create_named_table("SignedDistance", - "RoundBox", &sdRoundBox, - "Torus", &sdTorus - ); - } - - void MathScriptingLibrary::RegisterTime(sol::state& state) - { - state.new_usertype("Time", - sol::no_constructor, - "AsMicroseconds", &Nz::Time::AsMicroseconds, - "AsMilliseconds", &Nz::Time::AsMilliseconds, - "AsNanoseconds", &Nz::Time::AsNanoseconds, - "AsSeconds", &Nz::Time::AsSeconds, - "Milliseconds", &Nz::Time::Milliseconds, - "Microseconds", &Nz::Time::Microseconds, - "Nanosecond", &Nz::Time::Nanosecond, - "Second", &Nz::Time::Second, - "Seconds", &Nz::Time::Seconds, - "Zero", &Nz::Time::Zero, - sol::meta_function::equal_to, [](Nz::Time t1, Nz::Time t2) { return t1 == t2; }, - sol::meta_function::less_than, [](Nz::Time t1, Nz::Time t2) { return t1 < t2; }, - sol::meta_function::less_than_or_equal_to, [](Nz::Time t1, Nz::Time t2) { return t1 <= t2; }, - sol::meta_function::addition, [](Nz::Time t1, Nz::Time t2) { return t1 + t2; }, - sol::meta_function::subtraction, [](Nz::Time t1, Nz::Time t2) { return t1 - t2; }, - sol::meta_function::unary_minus, [](Nz::Time t) { return -t; } - ); - } - - template - void MathScriptingLibrary::RegisterVector2(sol::state& state, const char* name) - { - state.new_usertype>(name, - sol::call_constructor, sol::constructors(), Nz::Vector2(T), Nz::Vector2(T, T), Nz::Vector2(const Nz::Vector2&)>(), - "GetLength", [](const Nz::Vector2& vec) - { - return vec.template GetLength(); - }, - "GetNormal", [](const Nz::Vector2& vec) - { - T length; - Nz::Vector2 normalizedVec = vec.GetNormal(&length); - - return std::make_pair(normalizedVec, length); - }, - "GetSquaredLength", &Nz::Vector2::GetSquaredLength, - "Distance", [](const Nz::Vector2& vec1, const Nz::Vector2& vec2) - { - return vec1.Distance(vec2); - }, - "SquaredDistance", [](const Nz::Vector2& vec1, const Nz::Vector2& vec2) - { - return vec1.SquaredDistance(vec2); - }, - "x", &Nz::Vector2::x, - "y", &Nz::Vector2::y, - sol::meta_function::addition, Nz::Overload&>(&Nz::Vector2::operator+), - sol::meta_function::division, sol::overload(Nz::Overload(&Nz::Vector2::operator/), Nz::Overload&>(&Nz::Vector2::operator/)), - sol::meta_function::multiplication, sol::overload(Nz::Overload(&Nz::Vector2::operator*), Nz::Overload&>(&Nz::Vector2::operator*)), - sol::meta_function::subtraction, Nz::Overload&>(&Nz::Vector2::operator-), - sol::meta_function::to_string, &Nz::Vector2::ToString, - sol::meta_function::unary_minus, Nz::Overload<>(&Nz::Vector2::operator-) - ); - } - - template - void MathScriptingLibrary::RegisterVector3(sol::state& state, const char* name) - { - state.new_usertype>(name, - sol::call_constructor, sol::constructors(), Nz::Vector3(T), Nz::Vector3(T, T, T), Nz::Vector3(const Nz::Vector3&)>(), - "GetLength", [](const Nz::Vector3& vec) - { - return vec.template GetLength(); - }, - "GetNormal", [](const Nz::Vector3& vec) - { - T length; - Nz::Vector3 normalizedVec = vec.GetNormal(&length); - - return std::make_pair(normalizedVec, length); - }, - "GetSquaredLength", &Nz::Vector3::GetSquaredLength, - "Distance", [](const Nz::Vector3& vec1, const Nz::Vector3& vec2) - { - return vec1.Distance(vec2); - }, - "SquaredDistance", [](const Nz::Vector3& vec1, const Nz::Vector3& vec2) - { - return vec1.SquaredDistance(vec2); - }, - "x", &Nz::Vector3::x, - "y", &Nz::Vector3::y, - "z", &Nz::Vector3::z, - sol::meta_function::addition, Nz::Overload&>(&Nz::Vector3::operator+), - sol::meta_function::division, sol::overload(Nz::Overload(&Nz::Vector3::operator/), Nz::Overload&>(&Nz::Vector3::operator/)), - sol::meta_function::multiplication, sol::overload(Nz::Overload(&Nz::Vector3::operator*), Nz::Overload&>(&Nz::Vector3::operator*)), - sol::meta_function::subtraction, Nz::Overload&>(&Nz::Vector3::operator-), - sol::meta_function::to_string, &Nz::Vector3::ToString, - sol::meta_function::unary_minus, Nz::Overload<>(&Nz::Vector3::operator-) - ); } } diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index a9302aa6..8f34adb0 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -41,6 +40,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index dda0a7dd..ecea16d2 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -17,8 +17,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -52,12 +52,12 @@ #include #include #include +#include #include #include #include -#include -#include #include +#include #include #define DEBUG_ROTATION 0 @@ -1119,7 +1119,7 @@ namespace tsom } GetStateData().world->GetSystem().SetCameraPosition(m_cameraEntity.get().GetGlobalPosition()); - + #if defined(TSOM_DEV_TOOLS) && 0 if (stateData.imgui) { diff --git a/src/ServerLib/ServerInstance.cpp b/src/ServerLib/ServerInstance.cpp index 3cf328ec..41fd1202 100644 --- a/src/ServerLib/ServerInstance.cpp +++ b/src/ServerLib/ServerInstance.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace tsom m_entityRegistry.RegisterClassLibrary(m_application, m_blockLibrary); m_entityRegistry.RegisterClassLibrary(m_application); + m_scriptingContext.RegisterLibrary(); m_scriptingContext.RegisterLibrary(); auto& entityScriptingLibrary = m_scriptingContext.RegisterLibrary(m_entityRegistry); m_scriptingContext.RegisterLibrary(entityScriptingLibrary); @@ -270,6 +272,7 @@ namespace tsom { if (!isReloading) { + m_scriptingContext.LoadDirectory("scripts/libraries"); m_scriptingContext.LoadDirectory("scripts/entities"); return; } diff --git a/src/ServerLib/ServerPlayer.cpp b/src/ServerLib/ServerPlayer.cpp index cfdc60fb..e350218f 100644 --- a/src/ServerLib/ServerPlayer.cpp +++ b/src/ServerLib/ServerPlayer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,7 @@ namespace tsom Nz::ApplicationBase& applicationBase = m_serverInstance.GetApplication(); m_console.Emplace(applicationBase); + m_console->scriptingContext.RegisterLibrary(); m_console->scriptingContext.RegisterLibrary(applicationBase); m_console->scriptingContext.RegisterLibrary(); m_console->scriptingContext.RegisterLibrary(); @@ -100,6 +102,8 @@ namespace tsom m_console->scriptingContext.RegisterLibrary(entityScriptingLibrary); m_console->scriptingContext.RegisterLibrary(m_serverInstance, entityScriptingLibrary); + m_console->scriptingContext.LoadDirectory("scripts/libraries"); + sol::state& state = m_console->scriptingContext.GetState(); state["CurrentPlayer"] = CreateHandle(); From 7132286c8762dcf24dc6ddd6a50379d88f5906c7 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 9 Apr 2026 15:46:30 +0200 Subject: [PATCH 08/23] wip --- assets/2d.passlist | 6 +- assets/3d.passlist | 6 +- assets/3d_dev.passlist | 6 +- assets/skybox.passlist | 2 +- include/ClientLib/ClientSessionHandler.hpp | 3 + include/CommonLib/ChunkLock.hpp | 1 + include/CommonLib/ChunkLock.inl | 14 ++++ src/ClientLib/ClientBlockLibrary.cpp | 3 +- src/ClientLib/ClientChunkEntities.cpp | 26 ++++--- src/ClientLib/ClientSessionHandler.cpp | 83 +++++++++++++++++++--- src/Game/GameAppComponent.cpp | 4 +- src/Game/States/GameState.cpp | 2 + 12 files changed, 123 insertions(+), 33 deletions(-) diff --git a/assets/2d.passlist b/assets/2d.passlist index 2c3f6ecc..311db671 100644 --- a/assets/2d.passlist +++ b/assets/2d.passlist @@ -14,7 +14,7 @@ passlist "2D" { impl "Forward" - output "Output" "ForwardOutput" + output "Output0" "ForwardOutput" { clearcolor "Viewer" } @@ -36,8 +36,8 @@ passlist "2D" Shader "PostProcess.GammaCorrection" } - input "Input" "ForwardOutput" - output "Output" "Gamma corrected" + input "Input0" "ForwardOutput" + output "Output0" "Gamma corrected" } output "Gamma corrected" diff --git a/assets/3d.passlist b/assets/3d.passlist index c82d335d..6df8f649 100644 --- a/assets/3d.passlist +++ b/assets/3d.passlist @@ -24,7 +24,7 @@ passlist "Forward Passlist" pass "ForwardPass" { impl "Forward" - output "Output" "ForwardOutput" + output "Output0" "ForwardOutput" { clearcolor "Viewer" } @@ -60,8 +60,8 @@ passlist "Forward Passlist" Shader "PostProcess.GammaCorrection" } - input "Input" "AtmosphereOutput" - output "Output" "Gamma corrected" + input "Input0" "AtmosphereOutput" + output "Output0" "Gamma corrected" } attachmentproxy "Debug draw output" "Gamma corrected" diff --git a/assets/3d_dev.passlist b/assets/3d_dev.passlist index eb3d543a..df52f7d1 100644 --- a/assets/3d_dev.passlist +++ b/assets/3d_dev.passlist @@ -24,7 +24,7 @@ passlist "Forward Passlist" pass "ForwardPass" { impl "Forward" - output "Output" "ForwardOutput" + output "Output0" "ForwardOutput" { clearcolor "Viewer" } @@ -60,8 +60,8 @@ passlist "Forward Passlist" Shader "PostProcess.GammaCorrection" } - input "Input" "AtmosphereOutput" - output "Output" "Gamma corrected" + input "Input0" "AtmosphereOutput" + output "Output0" "Gamma corrected" } attachmentproxy "Debug draw output" "Gamma corrected" diff --git a/assets/skybox.passlist b/assets/skybox.passlist index ae6ecd02..788b760f 100644 --- a/assets/skybox.passlist +++ b/assets/skybox.passlist @@ -13,7 +13,7 @@ passlist "Skybox" pass "Skybox rendering" { impl "Forward" - output "Output" "ForwardOutput" + output "Output0" "ForwardOutput" cleardepth "Viewer" depthstenciloutput "DepthBuffer" } diff --git a/include/ClientLib/ClientSessionHandler.hpp b/include/ClientLib/ClientSessionHandler.hpp index fa8f9e14..7ebd57c9 100644 --- a/include/ClientLib/ClientSessionHandler.hpp +++ b/include/ClientLib/ClientSessionHandler.hpp @@ -75,6 +75,8 @@ namespace tsom void LoadScripts(bool isReloading = false); + void Update(); + NazaraSignal(OnAuthResponse, const Packets::S_AuthResponse& /*authResponse*/); NazaraSignal(OnChatMessage, const std::string& /*message*/); NazaraSignal(OnConsoleOutput, const Nz::Color& /*color*/, std::string_view /*message*/); @@ -140,6 +142,7 @@ namespace tsom std::vector> m_entities; //< FIXME: Nz::SparseVector std::vector> m_environments; //< FIXME: Nz::SparseVector std::vector> m_players; //< FIXME: Nz::SparseVector + tsl::hopscotch_map m_pendingChunkReset; Nz::ApplicationBase& m_app; Nz::EnttWorld& m_world; ClientBlockLibrary& m_blockLibrary; diff --git a/include/CommonLib/ChunkLock.hpp b/include/CommonLib/ChunkLock.hpp index a9f3e9be..86f8bdd9 100644 --- a/include/CommonLib/ChunkLock.hpp +++ b/include/CommonLib/ChunkLock.hpp @@ -28,6 +28,7 @@ namespace tsom ~ChunkLock(); void Lock(); + bool TryLock(); void Unlock(); ChunkLock& operator=(const ChunkLock&) = delete; diff --git a/include/CommonLib/ChunkLock.inl b/include/CommonLib/ChunkLock.inl index 07c59725..d42d98da 100644 --- a/include/CommonLib/ChunkLock.inl +++ b/include/CommonLib/ChunkLock.inl @@ -81,6 +81,20 @@ namespace tsom m_isLocked = true; } + template + bool ChunkLock::TryLock() + { + NazaraAssertMsg(!m_isLocked, "ChunkLock is already locked"); + + if (Detail::TryLockChunk(m_chunk)) + { + m_isLocked = true; + return true; + } + + return false; + } + template void ChunkLock::Unlock() { diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index 910dab0f..4ffdd5de 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -204,9 +204,8 @@ namespace tsom for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) { - blockTextures[textureData->type]->Update([&](void* pixelBuffer, Nz::UInt32 rowPitch, Nz::UInt32 depthPitch) + blockTextures[textureData->type]->Update([&](void* pixelBuffer) { - NazaraAssert(rowPitch == 0 && depthPitch == 0); std::memcpy(pixelBuffer, image->GetConstPixels(level), Nz::PixelFormatInfo::ComputeSize(image->GetFormat(), image->GetWidth(level), image->GetHeight(level), 1)); return true; }, Nz::Boxui(0, 0, textureSlice[textureData->type], image->GetWidth(level), image->GetHeight(level), 1), level); diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index 6a18b4a4..fc0713cc 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include namespace tsom { @@ -283,19 +285,25 @@ namespace tsom entityOwnerComp.Register(visualEntity); } - auto& gfxComponent = visualEntity.get_or_emplace(); - gfxComponent.Clear(); - if (colliderUpdateJob.mesh) { - // TODO: Move GPU upload to async task (should almost already work on Vulkan, problem is OpenGL) - std::shared_ptr gfxMesh = Nz::GraphicalMesh::BuildFromMesh(*colliderUpdateJob.mesh); + Nz::RenderDevice& renderDevice = *Nz::Graphics::Instance()->GetRenderDevice(); + std::unique_ptr asyncTransfer = renderDevice.InstantiateAsyncCommands(Nz::QueueType::Transfer); + std::shared_ptr gfxMesh = Nz::GraphicalMesh::BuildFromMesh(*asyncTransfer, *colliderUpdateJob.mesh); + + asyncTransfer->AddCompletionCallback([this, gfxMesh, visualEntity, c = Nz::HighPrecisionClock()] + { + auto& gfxComponent = visualEntity.get_or_emplace(); + gfxComponent.Clear(); + + std::shared_ptr model = std::make_shared(std::move(gfxMesh)); + model->SetMaterial(0, m_chunkMaterial); + model->UpdateRenderLayer(m_blockLibrary.GetLayerData(m_layerIndex).renderLayer); - std::shared_ptr model = std::make_shared(std::move(gfxMesh)); - model->SetMaterial(0, m_chunkMaterial); - model->UpdateRenderLayer(m_blockLibrary.GetLayerData(m_layerIndex).renderLayer); + gfxComponent.AttachRenderable(std::move(model), tsom::Constants::RenderMask3D); + }); - gfxComponent.AttachRenderable(std::move(model), tsom::Constants::RenderMask3D); + renderDevice.SubmitAsyncCommands(std::move(asyncTransfer)); } UpdateChunkDebugCollider(chunkIndices); diff --git a/src/ClientLib/ClientSessionHandler.cpp b/src/ClientLib/ClientSessionHandler.cpp index 9beb6541..d9c2c344 100644 --- a/src/ClientLib/ClientSessionHandler.cpp +++ b/src/ClientLib/ClientSessionHandler.cpp @@ -166,6 +166,8 @@ namespace tsom chunkNetworkMap.chunkNetworkIndices.erase(chunk); chunkNetworkMap.chunkByNetworkIndex.erase(it); + + m_pendingChunkReset.erase(chunkDestroy.chunkId); } void ClientSessionHandler::HandlePacket(Packets::S_ChunkReset&& chunkReset) @@ -181,18 +183,23 @@ namespace tsom return; } - ChunkWriteLock lock(chunk); + ChunkWriteLock lock(chunk, std::defer_lock); - if (!chunkReset.content.empty()) + if (lock.TryLock()) { - chunk->Reset([&](BlockIndex* blocks) + if (!chunkReset.content.empty()) { - for (BlockIndex blockContent : chunkReset.content) - *blocks++ = blockContent; - }); + chunk->Reset([&](BlockIndex* blocks) + { + for (BlockIndex blockContent : chunkReset.content) + *blocks++ = blockContent; + }); + } + else + chunk->Reset(); } else - chunk->Reset(); + m_pendingChunkReset[chunkReset.chunkId] = std::move(chunkReset); } void ClientSessionHandler::HandlePacket(Packets::S_ChunkUpdate&& chunkUpdate) @@ -202,10 +209,24 @@ namespace tsom auto& chunkNetworkMap = entity.get(); Chunk* chunk = Nz::Retrieve(chunkNetworkMap.chunkByNetworkIndex, chunkUpdate.chunkId); - ChunkWriteLock lock(chunk); - for (auto&& [blockPos, blockIndex] : chunkUpdate.updates) - chunk->UpdateBlock({ blockPos.x, blockPos.y, blockPos.z }, Nz::SafeCast(blockIndex)); + if (auto it = m_pendingChunkReset.find(chunkUpdate.chunkId); it != m_pendingChunkReset.end()) + { + // Apply update to pending chunk reset + Packets::S_ChunkReset& pendingChunkReset = it.value(); + if (pendingChunkReset.content.empty()) + pendingChunkReset.content.resize(chunk->GetBlockCount(), EmptyBlockIndex); + + for (auto&& [blockPos, blockIndex] : chunkUpdate.updates) + pendingChunkReset.content[chunk->GetBlockLocalIndex({ blockPos.x, blockPos.y, blockPos.z })] = Nz::SafeCast(blockIndex); + } + else + { + ChunkWriteLock lock(chunk); + + for (auto&& [blockPos, blockIndex] : chunkUpdate.updates) + chunk->UpdateBlock({ blockPos.x, blockPos.y, blockPos.z }, Nz::SafeCast(blockIndex)); + } } void ClientSessionHandler::HandlePacket(Packets::S_ConsoleOutput&& consoleOutput) @@ -480,6 +501,48 @@ namespace tsom }); } + void ClientSessionHandler::Update() + { + for (auto it = m_pendingChunkReset.begin(); it != m_pendingChunkReset.end();) + { + Packets::Helper::ChunkId chunkId = it.key(); + const Packets::S_ChunkReset& chunkReset = it.value(); + + assert(m_entities[chunkReset.entityId]); + entt::handle& entity = m_entities[chunkReset.entityId]->entity; + auto& chunkNetworkMap = entity.get(); + + Chunk* chunk = Nz::Retrieve(chunkNetworkMap.chunkByNetworkIndex, chunkReset.chunkId); + if (!chunk) + { + spdlog::error("ChunkReset handler (pending): unknown chunk {}", chunkReset.chunkId); + it = m_pendingChunkReset.erase(it); + continue; + } + + ChunkWriteLock lock(chunk, std::defer_lock); + + if (!lock.TryLock()) + { + ++it; + continue; + } + + if (!chunkReset.content.empty()) + { + chunk->Reset([&](BlockIndex* blocks) + { + for (BlockIndex blockContent : chunkReset.content) + *blocks++ = blockContent; + }); + } + else + chunk->Reset(); + + it = m_pendingChunkReset.erase(it); + } + } + void ClientSessionHandler::HandleEntityCreation(Packets::Helper::EntityData&& entityData) { entt::handle entity = m_world.CreateEntity(); diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index 8f34adb0..dbda3446 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -309,13 +309,13 @@ namespace tsom m_blockLibrary.emplace(app); - ClientAssetCooker assetCooker(app); + /*ClientAssetCooker assetCooker(app); if (auto result = assetCooker.Cook(*m_blockLibrary); !result) { spdlog::critical("failed to cook assets: {}!", result.GetError()); app.Quit(); return false; - } + }*/ m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index ecea16d2..635644ff 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -788,6 +788,8 @@ namespace tsom if (!stateData.networkSession) return true; + stateData.sessionHandler->Update(); + m_timerManager.Update(elapsedTime); m_chatBox->Update(); From f8578e43f3ef89a901098e15cf029c2094ab89eb Mon Sep 17 00:00:00 2001 From: SirLynix Date: Fri, 10 Apr 2026 23:18:31 +0200 Subject: [PATCH 09/23] Client: Improve shadow mapping cascades --- src/Game/States/GameState.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 635644ff..9b13aca8 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -95,7 +95,7 @@ namespace tsom cameraComponent.UpdateClearDepth(0.f); cameraComponent.UpdateRenderMask(tsom::Constants::RenderMask3D & ~tsom::Constants::RenderMaskLocalPlayer); cameraComponent.UpdateZNear(0.1f); - cameraComponent.UpdateZFar(10000.f); //< when infinite zfar is enabled, zfar is used as a limit for directional lights + cameraComponent.UpdateZFar(5000.f); //< when infinite zfar is enabled, zfar is used as a limit for directional lights m_targetCameraFOV = cameraComponent.GetFOV(); } @@ -131,7 +131,7 @@ namespace tsom dirLight.UpdateEnergy(5.f); dirLight.EnableFixedShadowCascadeSplit(true); - float splitFactors[] = { 0.0001f, 0.003f, 0.02f }; + float splitFactors[] = { 0.00075f, 0.003f, 0.03f }; dirLight.UpdateShadowCascadeFixedSplitFactors(splitFactors); } From 0aa0f55d7bf6970babf6d83f9794301537bc7954 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Fri, 10 Apr 2026 23:18:59 +0200 Subject: [PATCH 10/23] Planet: Fix chunk cache --- src/CommonLib/Planet.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CommonLib/Planet.cpp b/src/CommonLib/Planet.cpp index 16dfd02a..45cab520 100644 --- a/src/CommonLib/Planet.cpp +++ b/src/CommonLib/Planet.cpp @@ -451,6 +451,7 @@ namespace tsom void Planet::GenerateChunks(Nz::TaskScheduler& taskScheduler, Nz::UInt32 seed, const Nz::Vector3ui& chunkCount, std::string_view scriptName) { + m_chunkGenerators.Clear(); ForEachChunk([=, this, &taskScheduler](const ChunkIndices& chunkIndices, Chunk& chunk) { if (chunk.HasContent()) From 916514fc03b702a00c519dc7cde1fe83e48d80e5 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Sat, 11 Apr 2026 00:49:11 +0200 Subject: [PATCH 11/23] Planet: Update bob --- scripts/planets/bob.lua | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/scripts/planets/bob.lua b/scripts/planets/bob.lua index 09db4e74..a995ebad 100644 --- a/scripts/planets/bob.lua +++ b/scripts/planets/bob.lua @@ -3,7 +3,7 @@ local perlin = PerlinNoise() local chunksize = 32 local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things -local baseFreeHeight = 30 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight +local baseFreeHeight = 40 -- Should be greater than minFreeHeight, difference between both will define max generation height from baseFreeHeight local function GetBlockIndices(chunkIndices, blockIndices) local x = chunkIndices.x * chunksize + blockIndices.x - chunksize * 0.5 @@ -38,18 +38,22 @@ return function (chunk, seed, chunkDims) local stoneBricksBlock = blockLibrary:GetBlockIndex("stone_bricks") local copperBlock = blockLibrary:GetBlockIndex("copper_block") local glassBlock = blockLibrary:GetBlockIndex("glass") + local waterBlock = blockLibrary:GetBlockIndex("water") + local rockBlock = blockLibrary:GetBlockIndex("rock") + local barkBlock = blockLibrary:GetBlockIndex("bark") + local cliffRock = blockLibrary:GetBlockIndex("cliff_rocks") local planet = chunk:GetContainer() local chunkIndices = chunk:GetIndices() - local maxHeight = (chunksize * chunkDims.x)/2 * blockSize; + local maxHeight = (chunksize * chunkDims.x)/2 * blockSize local maxGenerationHeight = maxHeight - minGrenerationFreeHeight local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions local terrainVariation1Scale = 0.06 * baseHeight local terrainVariation2Scale = 0.16 * baseHeight local moutainScale = 0.035 * baseHeight - local spikeScale = 0.2 * baseHeight + local spikeScale = 0.1 * baseHeight local caveScale = 0.06 -- Other scale unit local content = {} @@ -58,27 +62,18 @@ return function (chunk, seed, chunkDims) for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do local blockPos = GetBlockIndices(chunkIndices, Vec3(x, y, z)) - local blockPosScaled = Vec3(blockPos.x * 0.5, blockPos.y * 0.5, blockPos.z * 0.5) + local blockPosScaled = blockPos * blockSize local blockPosNorm, distToCenter = blockPosScaled:GetNormal() --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) - distToCenter = sdRoundBox(blockPosScaled, Vec3(baseHeight), 16.0) - - if distToCenter > baseFreeHeight then - table.insert(content, emptyBlock) - goto continue - end + distToCenter = sdRoundBox(blockPosScaled, Vec3(baseHeight), 32.0) local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) - if distToCenter <= -32.0 then + if distToCenter <= 0.0 then if blockPresence >= 0.3 and blockPresence <= 0.7 then - if distToCenter <= -5 then - table.insert(content, stoneBlock) - else - table.insert(content, dirtBlock) - end - else table.insert(content, stoneBlock) + else + table.insert(content, emptyBlock) end else local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) @@ -92,7 +87,7 @@ return function (chunk, seed, chunkDims) end local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) - local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) + local heightVariation2 = 50 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) local spikeHeight @@ -103,27 +98,27 @@ return function (chunk, seed, chunkDims) else spikeHeight = 1 end - spikeHeight = (1-mountainous) * spikeHeight * 20 + spikeHeight = (1-mountainous) * spikeHeight * 15 local height = heightVariation1 + heightVariation2 + spikeHeight if distToCenter <= height then if distToCenter >= height - spikeHeight then - table.insert(content, stoneMossyBlock) + table.insert(content, rockBlock) elseif mountainous > 0.5 and heightVariation2 > 0.5 then table.insert(content, snowBlock) elseif mountainous > 0.1 then - table.insert(content, stoneBlock) - elseif baseMountainous < 0.4 then + table.insert(content, cliffRock) + elseif baseMountainous < 0.6 then table.insert(content, grassBlock) else - table.insert(content, dirtBlock) + table.insert(content, rockBlock) end else table.insert(content, emptyBlock) end end - + ::continue:: end end From 91e51b98b9b9c4c7a7f531de87604f91860cb09e Mon Sep 17 00:00:00 2001 From: SirLynix Date: Mon, 13 Apr 2026 23:02:23 +0200 Subject: [PATCH 12/23] Move BlockLibrary to external file loaded at startup --- .gitignore | 1 + assets/blocks.json | 114 +++++++++++ include/ClientLib/ClientAssetCookRegistry.hpp | 2 +- include/CommonLib/BlockLibrary.hpp | 14 +- include/CommonLib/BlockLibrary.inl | 8 + src/ClientLib/ClientAssetCookRegistry.cpp | 4 +- src/ClientLib/ClientBlockLibrary.cpp | 2 +- src/CommonLib/BlockLibrary.cpp | 181 +++++------------- src/Game/GameAppComponent.cpp | 6 + src/Server/main.cpp | 2 +- src/ServerLib/ServerInstance.cpp | 9 + 11 files changed, 207 insertions(+), 136 deletions(-) create mode 100644 assets/blocks.json diff --git a/.gitignore b/.gitignore index c9e34884..6254db8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ assets/* !assets/*.passlist +!assets/*.json !assets/shaders cache/ saves/ diff --git a/assets/blocks.json b/assets/blocks.json new file mode 100644 index 00000000..1861ae72 --- /dev/null +++ b/assets/blocks.json @@ -0,0 +1,114 @@ +{ + "layers": { + "default": { + "isBlended": false + }, + "water": { + "physicsLayer": "StaticWater", + "isBlended": true, + "isFluid": true, + "isPhysicsTrigger": true, + "renderLayer": 100 + } + }, + "blocks": { + "empty": { + "hasCollisions": false, + "isSmooth": true, + "isTransparent": true, + "permeability": 1.0 + }, + "debug": { + "basePath": "blocks/debug_up" + }, + "dirt": { + "basePath": "blocks/dirt", + "isSmooth": true, + "density": 2.0, + "permeability": 0.1 + }, + "hull": { + "basePath": "blocks/smooth_stone" + }, + "snow": { + "basePath": "blocks/snow", + "isSmooth": true, + "permeability": 0.5 + }, + "stone": { + "basePath": "blocks/cobblestone", + "isSmooth": true, + "density": 4.0 + }, + "stone_mossy": { + "basePath": "blocks/mossy_cobblestone", + "isSmooth": true + }, + "forcefield": { + "basePath": "blocks/forcefield", + "hasCollisions": false, + "isTransparent": true + }, + "planks": { + "basePath": "blocks/planks" + }, + "stone_bricks": { + "basePath": "blocks/stone_bricks" + }, + "copper_block": { + "basePath": "blocks/copper_block" + }, + "grass": { + "basePath": "blocks/grass", + "isSmooth": true, + "density": 2.0, + "permeability": 0.1 + }, + "glass": { + "basePath": "blocks/glass", + "isDoubleSided": true, + "isSmooth": true, + "isTransparent": true + }, + "water": { + "layerName": "water", + "basePath": "blocks/water", + "isDoubleSided": true, + "isSmooth": true, + "isTransparent": true + }, + "bark": { + "basePath": "blocks/bark", + "isSmooth": true + }, + "cliff_rocks": { + "basePath": "blocks/cliff_rocks", + "isSmooth": true + }, + "rock": { + "basePath": "blocks/rock", + "isSmooth": true + }, + "wood_floor": { + "basePath": "blocks/wood_floor" + }, + "white_bricks": { + "basePath": "blocks/white_bricks" + }, + "gold": { + "basePath": "blocks/gold" + }, + "metal": { + "basePath": "blocks/metal" + }, + "metal_plates": { + "basePath": "blocks/metal_plates" + }, + "brickswall": { + "basePath": "blocks/brickswall" + }, + "floor_tiles": { + "basePath": "blocks/floor_tiles" + } + } +} \ No newline at end of file diff --git a/include/ClientLib/ClientAssetCookRegistry.hpp b/include/ClientLib/ClientAssetCookRegistry.hpp index 810d1007..11984137 100644 --- a/include/ClientLib/ClientAssetCookRegistry.hpp +++ b/include/ClientLib/ClientAssetCookRegistry.hpp @@ -36,7 +36,7 @@ namespace tsom ClientAssetCookRegistry& operator=(const ClientAssetCookRegistry&) = delete; ClientAssetCookRegistry& operator=(ClientAssetCookRegistry&&) = default; - static std::optional LoadFromContent(std::string_view content); + static std::optional LoadFromString(std::string_view content); static std::optional LoadFromFile(const std::filesystem::path& path); enum class TextureType diff --git a/include/CommonLib/BlockLibrary.hpp b/include/CommonLib/BlockLibrary.hpp index 911152e6..643331a7 100644 --- a/include/CommonLib/BlockLibrary.hpp +++ b/include/CommonLib/BlockLibrary.hpp @@ -26,11 +26,13 @@ namespace tsom struct LayerData; struct LayerInfo; - BlockLibrary(); + BlockLibrary() = default; BlockLibrary(const BlockLibrary&) = delete; BlockLibrary(BlockLibrary&&) = delete; ~BlockLibrary() = default; + void Clear(); + inline const BlockData& GetBlockData(BlockIndex blockIndex) const; inline const std::vector& GetBlocks() const; inline const LayerData& GetLayerData(std::size_t layerIndex) const; @@ -38,6 +40,8 @@ namespace tsom inline bool IsValidBlock(BlockIndex blockIndex) const; inline bool IsValidLayer(std::size_t layerIndex) const; + bool LoadFromString(std::string_view content, bool merge = false); + BlockIndex RegisterBlock(std::string name, BlockInfo blockInfo); std::size_t RegisterLayer(std::string name, LayerInfo layerInfo); @@ -70,10 +74,14 @@ namespace tsom float roughness = 1.0f; //< Used if texture is not available }; + struct PhysicsLayer + { + Nz::PhysObjectLayer3D layer; + }; + struct LayerInfo { - std::string name; - Nz::PhysObjectLayer3D physicsLayer = Constants::ObjectLayerStatic; + PhysicsLayer physicsLayer = PhysicsLayer { Constants::ObjectLayerStatic }; bool isBlended; bool isFluid = false; bool isPhysicsTrigger = false; diff --git a/include/CommonLib/BlockLibrary.inl b/include/CommonLib/BlockLibrary.inl index 44cf6a74..94ad84ef 100644 --- a/include/CommonLib/BlockLibrary.inl +++ b/include/CommonLib/BlockLibrary.inl @@ -4,6 +4,14 @@ namespace tsom { + inline void BlockLibrary::Clear() + { + m_blockIndices.clear(); + m_layerIndices.clear(); + m_blocks.clear(); + m_layers.clear(); + } + inline auto BlockLibrary::GetBlockData(BlockIndex blockIndex) const -> const BlockData& { return m_blocks[blockIndex]; diff --git a/src/ClientLib/ClientAssetCookRegistry.cpp b/src/ClientLib/ClientAssetCookRegistry.cpp index 3d36ae37..217ae686 100644 --- a/src/ClientLib/ClientAssetCookRegistry.cpp +++ b/src/ClientLib/ClientAssetCookRegistry.cpp @@ -75,7 +75,7 @@ namespace tsom return Nz::File::WriteWhole(path, content.data(), content.size()); } - std::optional ClientAssetCookRegistry::LoadFromContent(std::string_view content) + std::optional ClientAssetCookRegistry::LoadFromString(std::string_view content) { nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); @@ -92,6 +92,6 @@ namespace tsom if (!contentOpt) return std::nullopt; - return LoadFromContent(std::string_view(reinterpret_cast(contentOpt->data()), contentOpt->size())); + return LoadFromString(std::string_view(reinterpret_cast(contentOpt->data()), contentOpt->size())); } } diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index 4ffdd5de..af027858 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -91,7 +91,7 @@ namespace tsom std::optional cookRegistry; fs.GetFileContent("CookedAssets/registry.json", [&](const void* ptr, Nz::UInt64 size) { - cookRegistry = ClientAssetCookRegistry::LoadFromContent(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + cookRegistry = ClientAssetCookRegistry::LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); return true; }); diff --git a/src/CommonLib/BlockLibrary.cpp b/src/CommonLib/BlockLibrary.cpp index 57a915a9..44681a47 100644 --- a/src/CommonLib/BlockLibrary.cpp +++ b/src/CommonLib/BlockLibrary.cpp @@ -3,147 +3,72 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include +#include #include +#include +#include +#include +#include namespace tsom { - BlockLibrary::BlockLibrary() + namespace { - /************************************************************************/ - RegisterLayer("default", { - .isBlended = false - }); - - RegisterLayer("water", { - .physicsLayer = Constants::ObjectLayerStaticWater, - .isBlended = true, - .isFluid = true, - .isPhysicsTrigger = true, - .renderLayer = 100 - }); - - /************************************************************************/ - RegisterBlock("empty", { - .hasCollisions = false, - .isSmooth = true, - .isTransparent = true, - .permeability = 1.f - }); - - RegisterBlock("debug", { - .basePath = "blocks/debug_up", - }); - - RegisterBlock("dirt", { - .basePath = "blocks/dirt", - .isSmooth = true, - .density = 1.0f, - .permeability = 0.1f - }); - - RegisterBlock("grass", { - .basePath = "blocks/grass", - .isSmooth = true, - .density = 2.0f, - .permeability = 0.1f - }); - - RegisterBlock("hull", { - .basePath = "blocks/smooth_stone" - }); - - RegisterBlock("snow", { - .basePath = "blocks/snow", - .isSmooth = true, - .permeability = 0.5f - }); - - RegisterBlock("stone", { - .basePath = "blocks/cobblestone", - .isSmooth = true, - .density = 4.0f - }); - - RegisterBlock("stone_mossy", { - .basePath = "blocks/mossy_cobblestone", - .isSmooth = true, - }); - - RegisterBlock("forcefield", { - .basePath = "blocks/forcefield", - .hasCollisions = false, - .isTransparent = true - }); - - RegisterBlock("planks", { - .basePath = "blocks/planks", - }); - - RegisterBlock("stone_bricks", { - .basePath = "blocks/stone_bricks", - }); - - RegisterBlock("copper_block", { - .basePath = "blocks/copper_block", - }); - - RegisterBlock("glass", { - .basePath = "blocks/glass", - .isDoubleSided = true, - .isSmooth = true, - .isTransparent = true - }); - - RegisterBlock("water", { - .layerName = "water", - .basePath = "blocks/water", - .isDoubleSided = true, - .isSmooth = true, - .isTransparent = true, - }); - - RegisterBlock("bark", { - .basePath = "blocks/bark", - .isSmooth = true - }); - - RegisterBlock("cliff_rocks", { - .basePath = "blocks/cliff_rocks", - .isSmooth = true + static constexpr auto s_objectLayers = frozen::make_unordered_map({ + { "Dynamic", tsom::Constants::ObjectLayerDynamic }, + { "DynamicNoCollision", tsom::Constants::ObjectLayerDynamicNoCollision }, + { "DynamicNoPlayer", tsom::Constants::ObjectLayerDynamicNoPlayer }, + { "DynamicTrigger", tsom::Constants::ObjectLayerDynamicTrigger }, + { "Player", tsom::Constants::ObjectLayerPlayer }, + { "PlayerOnlyTrigger", tsom::Constants::ObjectLayerPlayerOnlyTrigger }, + { "Static", tsom::Constants::ObjectLayerStatic }, + { "StaticNoplayer", tsom::Constants::ObjectLayerStaticNoPlayer }, + { "StaticTrigger", tsom::Constants::ObjectLayerStaticTrigger }, + { "StaticWater", tsom::Constants::ObjectLayerStaticWater } }); + } +} - RegisterBlock("rock", { - .basePath = "blocks/rock", - .isSmooth = true - }); +namespace nlohmann +{ + template + void from_json(const BasicJsonType& j, tsom::BlockLibrary::PhysicsLayer& layerContainer) + { + std::string_view layerName = j; - RegisterBlock("wood_floor", { - .basePath = "blocks/wood_floor", - }); + if (auto it = tsom::s_objectLayers.find(layerName); it != tsom::s_objectLayers.end()) + layerContainer.layer = it->second; + else + throw std::runtime_error(fmt::format("invalid physics layer \"{}\"", layerName)); + } - RegisterBlock("white_bricks", { - .basePath = "blocks/white_bricks", - }); + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(tsom::BlockLibrary::BlockInfo, \ + layerName, basePath, hasCollisions, isDoubleSided, \ + isSmooth, isTransparent, density, \ + metalness, permeability, roughness \ + ); + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(tsom::BlockLibrary::LayerInfo, \ + physicsLayer, isBlended, isFluid, isPhysicsTrigger, renderLayer + ); +} - RegisterBlock("gold", { - .basePath = "blocks/gold", - }); +namespace tsom +{ + bool BlockLibrary::LoadFromString(std::string_view content, bool merge) + { + nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); - RegisterBlock("metal", { - .basePath = "blocks/metal", - }); + if (!merge) + Clear(); - RegisterBlock("metal_plates", { - .basePath = "blocks/metal_plates", - }); + for (const auto& [layerName, layerEntryDoc] : doc["layers"].items()) + RegisterLayer(layerName, layerEntryDoc); - RegisterBlock("brickswall", { - .basePath = "blocks/brickswall", - }); + for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) + RegisterBlock(blockName, blockEntryDoc); - RegisterBlock("floor_tiles", { - .basePath = "blocks/floor_tiles", - }); + return true; } BlockIndex BlockLibrary::RegisterBlock(std::string name, BlockInfo blockInfo) @@ -181,7 +106,7 @@ namespace tsom layerData.isFluid = layerInfo.isFluid; layerData.isPhysicsTrigger = layerInfo.isPhysicsTrigger; layerData.name = name; - layerData.physicsLayer = layerInfo.physicsLayer; + layerData.physicsLayer = layerInfo.physicsLayer.layer; layerData.renderLayer = layerInfo.renderLayer; assert(!m_layerIndices.contains(name)); diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index dbda3446..72d7e038 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -308,6 +308,12 @@ namespace tsom graphics->GetShaderModuleResolver()->RegisterDirectory(Nz::Utf8Path("assets/shaders"), true); m_blockLibrary.emplace(app); + + filesystem.GetFileContent("assets/blocks.json", [&](const void* ptr, Nz::UInt64 size) + { + m_blockLibrary->LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + return true; + }); /*ClientAssetCooker assetCooker(app); if (auto result = assetCooker.Cook(*m_blockLibrary); !result) diff --git a/src/Server/main.cpp b/src/Server/main.cpp index 8d5a8c73..d4c564ce 100644 --- a/src/Server/main.cpp +++ b/src/Server/main.cpp @@ -38,7 +38,7 @@ int ServerMain(int argc, char* argv[]) auto& serverInstanceAppComponent = app.AddComponent(); auto& filesystem = app.AddComponent(); - for (const char* directory : { "database", "scripts" }) + for (const char* directory : { "assets", "database", "scripts" }) { std::filesystem::path dirPath = Nz::Utf8Path(directory); if (!std::filesystem::is_directory(dirPath)) diff --git a/src/ServerLib/ServerInstance.cpp b/src/ServerLib/ServerInstance.cpp index 41fd1202..b77ab587 100644 --- a/src/ServerLib/ServerInstance.cpp +++ b/src/ServerLib/ServerInstance.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,14 @@ namespace tsom m_config(std::move(config)), m_scriptingContext(application) { + auto& fs = m_application.GetComponent(); + + fs.GetFileContent("assets/blocks.json", [&](const void* ptr, Nz::UInt64 size) + { + m_blockLibrary.LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + return true; + }); + m_entityRegistry.RegisterClassLibrary(m_application, m_blockLibrary); m_entityRegistry.RegisterClassLibrary(m_application); From ab669cbd68b4c9a017dcc521ddeb1fb724fe1b3b Mon Sep 17 00:00:00 2001 From: SirLynix Date: Mon, 13 Apr 2026 23:02:42 +0200 Subject: [PATCH 13/23] Shaders: Fix spaceship triplanar mapping --- assets/shaders/BlockPBR.nzsl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index c136d176..c0356a39 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -99,8 +99,9 @@ struct VertOut { [location(0)] worldPos: vec3[f32], [location(1)] triplanarPos: vec3[f32], - [location(2), interp(flat)] blockIndex: u32, - [location(3)] normal: vec3[f32], + [location(2)] triplanarNormal: vec3[f32], + [location(3), interp(flat)] blockIndex: u32, + [location(4)] normal: vec3[f32], [builtin(position)] position: vec4[f32], } @@ -197,7 +198,7 @@ fn ComputeColor(input: VertOut) -> vec4[f32] let uvY = input.triplanarPos.xz; let uvZ = input.triplanarPos.xy; - color = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + color = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.baseColorMapIndices); } const if (AlphaTest) @@ -261,7 +262,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let color = blockData.baseColorFallback; if (blockData.baseColorMapIndices.x >= 0) { - color = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + color = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.baseColorMapIndices); color.rgb = Color.sRGBToLinear(color.rgb); } @@ -297,7 +298,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] if (blockData.roughnessMetalnessMapIndices.x >= 0) { - let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; + let texData = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; roughness = texData.x; if (blockData.roughnessMetalnessMapIndices.y > 2) metallic = texData.y; @@ -305,7 +306,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] if (blockData.ambientOcclusionHeightMapIndices.x >= 0) { - let texData = TriplanarSample(input.normal, uvX, uvY, uvZ, blockData.ambientOcclusionHeightMapIndices).xy; + let texData = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.ambientOcclusionHeightMapIndices).xy; ao = texData.x; } @@ -439,6 +440,7 @@ fn VertMain(input: VertIn) -> VertOut let output: VertOut; output.worldPos = worldPosition.xyz; output.position = viewerData.viewProjMatrix * worldPosition; + output.triplanarNormal = input.normal; output.triplanarPos = MaterialData.settings.TriplanarOffset + input.pos; output.normal = rotationMatrix * input.normal; output.blockIndex = input.blockIndex; From 05a6da7816c8abc80129532892e189a139d7f499 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 14 Apr 2026 01:11:01 +0200 Subject: [PATCH 14/23] Fix scripting after LuaJIT switch --- .../CommonLib/Scripting/ScriptingUtils.inl | 106 +++++++++++++++--- scripts/commands/create_planet.lua | 12 +- scripts/commands/flood_fill.lua | 15 ++- scripts/commands/link_planets.lua | 8 +- scripts/libraries/eulerangles.lua | 2 +- scripts/libraries/quaternion.lua | 5 +- scripts/libraries/table.lua | 1 + scripts/libraries/vec2.lua | 8 ++ scripts/libraries/vec3.lua | 8 ++ scripts/planets/torus.lua | 2 +- 10 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 scripts/libraries/table.lua diff --git a/include/CommonLib/Scripting/ScriptingUtils.inl b/include/CommonLib/Scripting/ScriptingUtils.inl index c329c9d1..1be7f156 100644 --- a/include/CommonLib/Scripting/ScriptingUtils.inl +++ b/include/CommonLib/Scripting/ScriptingUtils.inl @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -188,6 +190,12 @@ namespace tsom namespace sol { + template + struct lua_size> : std::integral_constant {}; + + template + struct lua_size> : std::integral_constant {}; + template struct lua_size> : std::integral_constant {}; @@ -197,6 +205,12 @@ namespace sol template struct lua_size> : std::integral_constant {}; + template + struct lua_type_of> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + template struct lua_type_of> : std::integral_constant {}; @@ -206,6 +220,40 @@ namespace sol template struct lua_type_of> : std::integral_constant {}; + template + inline Nz::Box sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table box = sol::stack::get(L, absoluteIndex); + T x = box["x"]; + T y = box["y"]; + T z = box["z"]; + T width = box["width"]; + T height = box["height"]; + T depth = box["depth"]; + + tracking.use(1); + + return Nz::Box(x, y, z, width, height, depth); + } + + template + inline Nz::Quaternion sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table quat = sol::stack::get(L, absoluteIndex); + T x = quat["x"]; + T y = quat["y"]; + T z = quat["z"]; + T w = quat["w"]; + + tracking.use(1); + + return Nz::Quaternion(w, x, y, z); + } + template inline Nz::Rect sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) { @@ -252,41 +300,71 @@ namespace sol } + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::Box& box) + { + lua_createtable(L, 0, 6); + luaL_setmetatable(L, "box"); + sol::stack_table boxTable(L); + boxTable["x"] = box.x; + boxTable["y"] = box.y; + boxTable["z"] = box.z; + boxTable["width"] = box.width; + boxTable["height"] = box.height; + boxTable["depth"] = box.height; + + return 1; + } + + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::Quaternion& quat) + { + lua_createtable(L, 0, 4); + luaL_setmetatable(L, "quaternion"); + sol::stack_table quatTable(L); + quatTable["x"] = quat.x; + quatTable["y"] = quat.y; + quatTable["z"] = quat.z; + quatTable["w"] = quat.w; + + return 1; + } + template int sol_lua_push(sol::types>, lua_State* L, const Nz::Rect& rect) { lua_createtable(L, 0, 4); luaL_setmetatable(L, "rect"); - sol::stack_table vec(L); - vec["x"] = rect.x; - vec["y"] = rect.y; - vec["width"] = rect.width; - vec["height"] = rect.height; + sol::stack_table rectTable(L); + rectTable["x"] = rect.x; + rectTable["y"] = rect.y; + rectTable["width"] = rect.width; + rectTable["height"] = rect.height; return 1; } template - int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector2& v) + int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector2& vec) { lua_createtable(L, 0, 2); luaL_setmetatable(L, "vec2"); - sol::stack_table vec(L); - vec["x"] = v.x; - vec["y"] = v.y; + sol::stack_table vecTable(L); + vecTable["x"] = vec.x; + vecTable["y"] = vec.y; return 1; } template - int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector3& v) + int sol_lua_push(sol::types>, lua_State* L, const Nz::Vector3& vec) { lua_createtable(L, 0, 3); luaL_setmetatable(L, "vec3"); - sol::stack_table vec(L); - vec["x"] = v.x; - vec["y"] = v.y; - vec["z"] = v.z; + sol::stack_table vecTable(L); + vecTable["x"] = vec.x; + vecTable["y"] = vec.y; + vecTable["z"] = vec.z; return 1; } diff --git a/scripts/commands/create_planet.lua b/scripts/commands/create_planet.lua index 5b6c71cc..8b312296 100644 --- a/scripts/commands/create_planet.lua +++ b/scripts/commands/create_planet.lua @@ -32,10 +32,10 @@ return function (opt) currentPos = playerNode:GetPosition() end - local id = serverDatabase.CreatePlanet(planetData) + local id = ServerDatabase.CreatePlanet(planetData) local planetEnv = PlanetEnvironment.new(id, planetData.generatorName, planetData.seed, planetData.chunkCount, 1.0, planetData.cornerRadius) - server.RegisterDatabaseEnvironment(id, planetEnv) + Server.RegisterDatabaseEnvironment(id, planetEnv) print("new planet id: " .. id) @@ -47,14 +47,14 @@ return function (opt) } print("Store link 1") - serverDatabase.StorePlanetLink(linkData) + ServerDatabase.StorePlanetLink(linkData) -- reverse link print("Store link 2") - serverDatabase.StorePlanetLink({ sourcePlanet = linkData.destinationPlanet, destinationPlanet = linkData.sourcePlanet, position = -linkData.position }) + ServerDatabase.StorePlanetLink({ sourcePlanet = linkData.destinationPlanet, destinationPlanet = linkData.sourcePlanet, position = -linkData.position }) print("Server link database environments") - server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) - server.LinkDatabaseEnvironments(linkData.destinationPlanet, linkData.sourcePlanet, -linkData.position) + Server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) + Server.LinkDatabaseEnvironments(linkData.destinationPlanet, linkData.sourcePlanet, -linkData.position) end end diff --git a/scripts/commands/flood_fill.lua b/scripts/commands/flood_fill.lua index d8d16ee9..66241ad4 100644 --- a/scripts/commands/flood_fill.lua +++ b/scripts/commands/flood_fill.lua @@ -72,7 +72,7 @@ return function () local pendingList = { blockIndices } local function AddNeighbor(chunkContainer, blockIndices, axis, dir, factor, checkAxis) - local nextBlockIndices = Vec3i(blockIndices.x, blockIndices.y, blockIndices.z) + local nextBlockIndices = Vec3(blockIndices.x, blockIndices.y, blockIndices.z) nextBlockIndices[axis[dir]] = nextBlockIndices[axis[dir]] + axis[dir .. "Dir"] * factor local nextChunkIndices, nextInnerCoordinates = chunkContainer:GetChunkIndicesByBlockIndices(nextBlockIndices) @@ -84,7 +84,7 @@ return function () local nextAxis = GetUpAxis(nextChunk, nextInnerCoordinates) if checkAxis and axis ~= nextAxis then - nextBlockIndices = Vec3i(blockIndices.x, blockIndices.y, blockIndices.z) + nextBlockIndices = Vec3(blockIndices.x, blockIndices.y, blockIndices.z) nextBlockIndices[nextAxis[dir]] = nextBlockIndices[nextAxis[dir]] + nextAxis[dir .. "Dir"] * factor end @@ -98,12 +98,11 @@ return function () return end - server.ScheduleForNextTick(updateCallback) + Server.ScheduleForNextTick(updateCallback) local updatedChunks = {} - local toProcess = math.max(math.floor(remaining / 50), 1) - for i = 1, toProcess do + for i = 1, remaining do local firstBlock = table.remove(pendingList, 1) local chunkIndices, innerCoordinates = chunkContainer:GetChunkIndicesByBlockIndices(firstBlock) @@ -125,14 +124,14 @@ return function () AddNeighbor(chunkContainer, firstBlock, axis, "right", -1, true) AddNeighbor(chunkContainer, firstBlock, axis, "up", -1) end - + updatedChunks[tostring(chunkIndices)] = true - if table.count(updatedChunks) > 3 then + if table.count(updatedChunks) > 10 then -- limit concurrent chunk updates per tick return end end end - server.ScheduleForNextTick(updateCallback) + Server.ScheduleForNextTick(updateCallback) end \ No newline at end of file diff --git a/scripts/commands/link_planets.lua b/scripts/commands/link_planets.lua index 7f91ef62..f011bf4e 100644 --- a/scripts/commands/link_planets.lua +++ b/scripts/commands/link_planets.lua @@ -19,10 +19,10 @@ return function (opt) } if not opt.ephemeral then - serverDatabase.StorePlanetLink(linkData) + ServerDatabase.StorePlanetLink(linkData) end - server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) + Server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) if opt.dual then linkData.position = -linkData.position @@ -32,10 +32,10 @@ return function (opt) linkData.destinationPlanet = temp if not opt.ephemeral then - serverDatabase.StorePlanetLink(linkData) + ServerDatabase.StorePlanetLink(linkData) end - server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) + Server.LinkDatabaseEnvironments(linkData.sourcePlanet, linkData.destinationPlanet, linkData.position) end print("link created") diff --git a/scripts/libraries/eulerangles.lua b/scripts/libraries/eulerangles.lua index 356c1484..c715ec44 100644 --- a/scripts/libraries/eulerangles.lua +++ b/scripts/libraries/eulerangles.lua @@ -2,7 +2,7 @@ local EulerAnglesMt = CreateMetatable("eulerangles") EulerAnglesMt.__index = EulerAnglesMt -function Color(pitch, yaw, roll) +function EulerAngles(pitch, yaw, roll) return setmetatable({ pitch = pitch, yaw = yaw, diff --git a/scripts/libraries/quaternion.lua b/scripts/libraries/quaternion.lua index 0c4fb7b7..872cd15c 100644 --- a/scripts/libraries/quaternion.lua +++ b/scripts/libraries/quaternion.lua @@ -7,7 +7,6 @@ function QuaternionMt:GetConjugate() end function QuaternionMt:__mul(quat) - assert(type(quat) == "userdata") local mt = getmetatable(quat) if mt == QuaternionMt then return Quaternion( @@ -18,8 +17,8 @@ function QuaternionMt:__mul(quat) ) else local quatVec = Vec3(self.x, self.y, self.z) - local uv = quatVec.CrossProduct(quat) - local uuv = quatVec.CrossProduct(uv) + local uv = quatVec:CrossProduct(quat) + local uuv = quatVec:CrossProduct(uv) uv = uv * 2.0 * self.w uuv = uuv * 2.0 diff --git a/scripts/libraries/table.lua b/scripts/libraries/table.lua new file mode 100644 index 00000000..ae3eabef --- /dev/null +++ b/scripts/libraries/table.lua @@ -0,0 +1 @@ +table.new = require("table.new") diff --git a/scripts/libraries/vec2.lua b/scripts/libraries/vec2.lua index 9d58e484..28edef70 100644 --- a/scripts/libraries/vec2.lua +++ b/scripts/libraries/vec2.lua @@ -51,6 +51,14 @@ function Vec2Mt:__div(vec) end end +function Vec2Mt:__unm() + return Vec3(-self.x, -self.y) +end + +function Vec2Mt:__tostring() + return string.format("Vec2(%f, %f)", self.x, self.y) +end + local Vec2ClassMt = {} function Vec2ClassMt.__call(t, x, y) diff --git a/scripts/libraries/vec3.lua b/scripts/libraries/vec3.lua index 96d3b63e..1c3015a7 100644 --- a/scripts/libraries/vec3.lua +++ b/scripts/libraries/vec3.lua @@ -55,6 +55,14 @@ function Vec3Mt:__div(vec) end end +function Vec3Mt:__unm() + return Vec3(-self.x, -self.y, -self.z) +end + +function Vec3Mt:__tostring() + return string.format("Vec3(%f, %f, %f)", self.x, self.y, self.z) +end + local Vec3ClassMt = {} function Vec3ClassMt.__call(t, x, y, z) diff --git a/scripts/planets/torus.lua b/scripts/planets/torus.lua index 6ea66320..2b10ca19 100644 --- a/scripts/planets/torus.lua +++ b/scripts/planets/torus.lua @@ -47,7 +47,7 @@ return function (chunk, seed, chunkcount) local planet = chunk:GetContainer() local chunkIndices = chunk:GetIndices() - local maxHeight = (Vec3i(chunkcount.x, chunkcount.y, chunkcount.z) + Vec3i(1)) / 2 + local maxHeight = (Vec3(chunkcount.x, chunkcount.y, chunkcount.z) + Vec3(1)) / 2 maxHeight = maxHeight * chunksize local content = {} From 3868a21375c591bb949f3688975ff465434edeec Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 14 Apr 2026 01:11:22 +0200 Subject: [PATCH 15/23] Rework bob planet generation (especially caves) --- scripts/planets/bob.lua | 152 +++++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 50 deletions(-) diff --git a/scripts/planets/bob.lua b/scripts/planets/bob.lua index a995ebad..d30ebcf6 100644 --- a/scripts/planets/bob.lua +++ b/scripts/planets/bob.lua @@ -1,5 +1,6 @@ -- Do not touch to this 2 variables local perlin = PerlinNoise() +local cavePerlin = PerlinNoise() local chunksize = 32 local minGrenerationFreeHeight = 0 -- Generation height limit used to make generation faster if we want empty chunks to allow players to build tall things @@ -19,6 +20,7 @@ end return function (chunk, seed, chunkDims) perlin:reseed(seed) + cavePerlin:reseed(seed * seed) local blockSize = chunk:GetBlockSize() @@ -36,7 +38,7 @@ return function (chunk, seed, chunkDims) local forcefieldBlock = blockLibrary:GetBlockIndex("forcefield") local planksBlock = blockLibrary:GetBlockIndex("planks") local stoneBricksBlock = blockLibrary:GetBlockIndex("stone_bricks") - local copperBlock = blockLibrary:GetBlockIndex("copper_block") + local goldBlock = blockLibrary:GetBlockIndex("gold") local glassBlock = blockLibrary:GetBlockIndex("glass") local waterBlock = blockLibrary:GetBlockIndex("water") local rockBlock = blockLibrary:GetBlockIndex("rock") @@ -49,74 +51,91 @@ return function (chunk, seed, chunkDims) local maxHeight = (chunksize * chunkDims.x)/2 * blockSize local maxGenerationHeight = maxHeight - minGrenerationFreeHeight local baseHeight = maxHeight - baseFreeHeight -- Only works for planets with the same number of chunks in all the directions - + local caveBaseHeight = baseHeight - 12.0 + local terrainVariation1Scale = 0.06 * baseHeight local terrainVariation2Scale = 0.16 * baseHeight local moutainScale = 0.035 * baseHeight local spikeScale = 0.1 * baseHeight - local caveScale = 0.06 -- Other scale unit - - local content = {} - + local smallCaveScale = 0.10 -- Other scale unit + local bigCaveScale = 0.03 -- Other scale unit + local stoneScale = 0.35 * baseHeight + + local content = table.new(chunksize * chunksize * chunksize, 0) + for z = 0, chunksize - 1 do for y = 0, chunksize - 1 do for x = 0, chunksize - 1 do local blockPos = GetBlockIndices(chunkIndices, Vec3(x, y, z)) local blockPosScaled = blockPos * blockSize local blockPosNorm, distToCenter = blockPosScaled:GetNormal() + + -- center of the planet + if distToCenter < 16.0 then + table.insert(content, distToCenter < 2.0 and goldBlock or emptyBlock) + goto continue + end + --distToCenter = math.max(math.abs(blockPos.x * 0.5 + 0.5), math.abs(blockPos.y * 0.5 + 0.5), math.abs(blockPos.z * 0.5 + 0.5)) distToCenter = sdRoundBox(blockPosScaled, Vec3(baseHeight), 32.0) - local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) + local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) + local mountainous + if baseMountainous < 0.6 then + mountainous = 0 + elseif baseMountainous < 0.8 then + mountainous = 5*baseMountainous-3 + else + mountainous = 1 + end - if distToCenter <= 0.0 then - if blockPresence >= 0.3 and blockPresence <= 0.7 then - table.insert(content, stoneBlock) - else - table.insert(content, emptyBlock) - end + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) + local heightVariation2 = 50 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) + + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) + local spikeHeight + if baseSpikeHeight < 0.7 then + spikeHeight = 0 + elseif baseSpikeHeight < 0.9 then + spikeHeight = 5*baseSpikeHeight-3.5 else - local baseMountainous = perlin:normalizedOctave3D_01((blockPosNorm.x * moutainScale)+10, blockPosNorm.y * moutainScale, blockPosNorm.z * moutainScale, 4, 0.1) - local mountainous - if baseMountainous < 0.6 then - mountainous = 0 - elseif baseMountainous < 0.8 then - mountainous = 5*baseMountainous-3 - else - mountainous = 1 + spikeHeight = 1 + end + spikeHeight = (1-mountainous) * spikeHeight * 15 + + local height = heightVariation1 + heightVariation2 + spikeHeight + + local waterScale = 0.01 * baseHeight + local waterNoise = perlin:normalizedOctave3D((blockPosNorm.x * waterScale)+10, blockPosNorm.y * waterScale, blockPosNorm.z * waterScale, 4, 0.1) + + local blockType = emptyBlock + + if waterNoise > 0.5 then + height = -5 + if distToCenter < 0.0 then + blockType = waterBlock end - - local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) - local heightVariation2 = 50 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) - - local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) - local spikeHeight - if baseSpikeHeight < 0.7 then - spikeHeight = 0 - elseif baseSpikeHeight < 0.9 then - spikeHeight = 5*baseSpikeHeight-3.5 + end + + if distToCenter <= height then + if distToCenter >= height - spikeHeight then + blockType = rockBlock + elseif mountainous > 0.5 and heightVariation2 > 0.5 then + blockType = snowBlock + elseif mountainous > 0.1 then + blockType = cliffRock + elseif baseMountainous < 0.6 then + local stoneNoise = perlin:normalizedOctave3D((blockPosNorm.x * stoneScale)+10, blockPosNorm.y * stoneScale, blockPosNorm.z * stoneScale, 4, 0.1) + blockType = stoneNoise >= 0.4 and stoneBlock or grassBlock else - spikeHeight = 1 + blockType = rockBlock end - spikeHeight = (1-mountainous) * spikeHeight * 15 - - local height = heightVariation1 + heightVariation2 + spikeHeight - - if distToCenter <= height then - if distToCenter >= height - spikeHeight then - table.insert(content, rockBlock) - elseif mountainous > 0.5 and heightVariation2 > 0.5 then - table.insert(content, snowBlock) - elseif mountainous > 0.1 then - table.insert(content, cliffRock) - elseif baseMountainous < 0.6 then - table.insert(content, grassBlock) - else - table.insert(content, rockBlock) - end - else - table.insert(content, emptyBlock) + if distToCenter < 0.0 then + blockType = dirtBlock end + table.insert(content, blockType) + else + table.insert(content, blockType) end ::continue:: @@ -124,5 +143,38 @@ return function (chunk, seed, chunkDims) end end + -- Caves + for z = 0, chunksize - 1 do + for y = 0, chunksize - 1 do + for x = 0, chunksize - 1 do + local blockPos = GetBlockIndices(chunkIndices, Vec3(x, y, z)) + local blockPosScaled = blockPos * blockSize + local distToCenter = sdRoundBox(blockPosScaled, Vec3(caveBaseHeight), 32.0) + + local smallCavePresence = cavePerlin:normalizedOctave3D(blockPosScaled.x * smallCaveScale, blockPosScaled.y * smallCaveScale, blockPosScaled.z * smallCaveScale, 4, 0.1) + local bigCavePresence = cavePerlin:normalizedOctave3D(blockPosScaled.x * bigCaveScale, blockPosScaled.y * bigCaveScale, blockPosScaled.z * bigCaveScale, 4, 0.1) + + local smallCaveMinValue = 0.5 + local bigCaveMinValue = 0.4 + if distToCenter > 16.0 then + smallCaveMinValue = smallCaveMinValue + distToCenter / 4.0 * 0.1 + end + if distToCenter > 0.0 then + bigCavePresence = bigCavePresence * 2 + bigCaveMinValue = bigCaveMinValue + distToCenter * 0.1 + end + + if math.abs(smallCavePresence) > smallCaveMinValue or math.abs(bigCavePresence) > bigCaveMinValue then + local blockIndex = z * chunksize * chunksize + y * chunksize + x + 1 + if content[blockIndex] == waterBlock then + content[blockIndex] = dirtBlock + else + content[blockIndex] = emptyBlock + end + end + end + end + end + return content end From f12488587205c48ff5b3e591c180ca831c5939c3 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 14 Apr 2026 01:24:15 +0200 Subject: [PATCH 16/23] Fix compilation --- src/CommonLib/BlockLibrary.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommonLib/BlockLibrary.cpp b/src/CommonLib/BlockLibrary.cpp index 44681a47..3e608eee 100644 --- a/src/CommonLib/BlockLibrary.cpp +++ b/src/CommonLib/BlockLibrary.cpp @@ -34,9 +34,9 @@ namespace nlohmann template void from_json(const BasicJsonType& j, tsom::BlockLibrary::PhysicsLayer& layerContainer) { - std::string_view layerName = j; + const std::string& layerName = j; - if (auto it = tsom::s_objectLayers.find(layerName); it != tsom::s_objectLayers.end()) + if (auto it = tsom::s_objectLayers.find(frozen::string(layerName)); it != tsom::s_objectLayers.end()) layerContainer.layer = it->second; else throw std::runtime_error(fmt::format("invalid physics layer \"{}\"", layerName)); From 3f4a7dd644a1438c1c26b389941a8211d45b2af9 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 21 Apr 2026 00:36:39 +0200 Subject: [PATCH 17/23] Add AssetCooker --- assets/assets.json | 129 ++++++ assets/shaders/PlayerPBR.nzsl | 12 +- include/ClientLib/ClientAssetCooker.hpp | 42 -- include/ClientLib/ClientBlockLibrary.hpp | 6 +- include/ClientLib/ClientBlockLibrary.inl | 2 +- .../CookedBlockRegistry.hpp} | 28 +- .../CookedBlockRegistry.inl} | 0 src/AssetCooker/BlockCooker.cpp | 376 ++++++++++++++++++ src/AssetCooker/BlockCooker.hpp | 60 +++ .../AssetCooker/Cooker.cpp | 7 +- src/AssetCooker/Cooker.hpp | 35 ++ src/AssetCooker/CopyCooker.cpp | 54 +++ src/AssetCooker/CopyCooker.hpp | 31 ++ src/AssetCooker/CubemapCooker.cpp | 70 ++++ src/AssetCooker/CubemapCooker.hpp | 31 ++ src/AssetCooker/CubemapSplitFacesCooker.cpp | 94 +++++ src/AssetCooker/CubemapSplitFacesCooker.hpp | 34 ++ src/AssetCooker/ShaderCooker.cpp | 54 +++ src/AssetCooker/ShaderCooker.hpp | 31 ++ src/AssetCooker/TextureCooker.cpp | 279 +++++++++++++ src/AssetCooker/TextureCooker.hpp | 52 +++ src/AssetCooker/main.cpp | 136 +++++++ src/ClientLib/ClientAssetCooker.cpp | 302 -------------- src/ClientLib/ClientBlockLibrary.cpp | 24 +- src/ClientLib/ClientChunkEntities.cpp | 10 +- src/ClientLib/ClientSessionHandler.cpp | 16 +- src/CommonLib/BlockLibrary.cpp | 2 +- .../CookedBlockRegistry.cpp} | 30 +- src/CommonLib/Planet.cpp | 12 +- src/Game/GameAppComponent.cpp | 11 +- src/Game/States/BackgroundState.cpp | 24 +- src/Game/States/GameState.cpp | 6 +- src/Game/States/MenuState.cpp | 4 +- src/Game/States/PlanetEditorState.cpp | 6 +- xmake.lua | 14 +- xmake/actions/checkfiles.lua | 1 + 36 files changed, 1565 insertions(+), 460 deletions(-) create mode 100644 assets/assets.json delete mode 100644 include/ClientLib/ClientAssetCooker.hpp rename include/{ClientLib/ClientAssetCookRegistry.hpp => CommonLib/CookedBlockRegistry.hpp} (59%) rename include/{ClientLib/ClientAssetCookRegistry.inl => CommonLib/CookedBlockRegistry.inl} (100%) create mode 100644 src/AssetCooker/BlockCooker.cpp create mode 100644 src/AssetCooker/BlockCooker.hpp rename include/ClientLib/ClientAssetCooker.inl => src/AssetCooker/Cooker.cpp (71%) create mode 100644 src/AssetCooker/Cooker.hpp create mode 100644 src/AssetCooker/CopyCooker.cpp create mode 100644 src/AssetCooker/CopyCooker.hpp create mode 100644 src/AssetCooker/CubemapCooker.cpp create mode 100644 src/AssetCooker/CubemapCooker.hpp create mode 100644 src/AssetCooker/CubemapSplitFacesCooker.cpp create mode 100644 src/AssetCooker/CubemapSplitFacesCooker.hpp create mode 100644 src/AssetCooker/ShaderCooker.cpp create mode 100644 src/AssetCooker/ShaderCooker.hpp create mode 100644 src/AssetCooker/TextureCooker.cpp create mode 100644 src/AssetCooker/TextureCooker.hpp create mode 100644 src/AssetCooker/main.cpp delete mode 100644 src/ClientLib/ClientAssetCooker.cpp rename src/{ClientLib/ClientAssetCookRegistry.cpp => CommonLib/CookedBlockRegistry.cpp} (64%) diff --git a/assets/assets.json b/assets/assets.json new file mode 100644 index 00000000..9cc136ac --- /dev/null +++ b/assets/assets.json @@ -0,0 +1,129 @@ +{ + "assets": [ + { + "input": "blocks.json", + "output": "Blocks", + "method": "Blocks", + "size": 2048 + }, + { + "method": "CubemapSplitFaces", + "output": "Textures/Skybox/MenuSkybox.dds", + "faces": { + "+x": "PurpleNebulaSkybox/purple_nebula_skybox_right1.png", + "-x": "PurpleNebulaSkybox/purple_nebula_skybox_left2.png", + "+y": "PurpleNebulaSkybox/purple_nebula_skybox_top3.png", + "-y": "PurpleNebulaSkybox/purple_nebula_skybox_bottom4.png", + "+z": "PurpleNebulaSkybox/purple_nebula_skybox_front5.png", + "-z": "PurpleNebulaSkybox/purple_nebula_skybox_back6.png" + }, + "sRGB": false + }, + { + "input": "skybox-space.png", + "output": "Textures/Skybox/GameSkybox.dds", + "method": "Cubemap" + }, + { + "input": "dev/grey.png", + "output": "Textures/Dev/grey.dds", + "method": "Texture" + }, + { + "input": "fonts/axaxax bd.otf", + "output": "Fonts/axaxax bd.otf", + "method": "Copy" + }, + { + "input": "Player/Idle.fbx", + "output": "Models/Player/Idle.fbx", + "method": "Copy" + }, + { + "input": "Player/Running.fbx", + "output": "Models/Player/Running.fbx", + "method": "Copy" + }, + { + "input": "Player/Walking.fbx", + "output": "Models/Player/Walking.fbx", + "method": "Copy" + }, + { + "input": "Player/Textures/Soldier_AlbedoTransparency.png", + "output": "Models/Player/Textures/Soldier_AlbedoTransparency.dds", + "method": "Texture" + }, + { + "input": "Player/Textures/Soldier_AO.png", + "output": "Models/Player/Textures/Soldier_AO.dds", + "method": "Texture", + "type": "Greyscale" + }, + { + "input": "Player/Textures/Soldier_MetallicSmoothness.png", + "output": "Models/Player/Textures/Soldier_MetallicSmoothness.dds", + "method": "Texture", + "type": "BiGreyscale", + "channel0": "Red", + "channel1": "Alpha" + }, + { + "input": "Player/Textures/Soldier_Normal.png", + "output": "Models/Player/Textures/Soldier_Normal.dds", + "method": "Texture", + "type": "Normal" + }, + { + "input": "Shaders/BlockPBR.nzsl", + "output": "Shaders/BlockPBR.nzslb", + "method": "Shader" + }, + { + "input": "Shaders/Logo.nzsl", + "output": "Shaders/Logo.nzslb", + "method": "Shader" + }, + { + "input": "Shaders/PlanetAtmosphere.nzsl", + "output": "Shaders/PlanetAtmosphere.nzslb", + "method": "Shader" + }, + { + "input": "Shaders/PlayerPBR.nzsl", + "output": "Shaders/PlayerPBR.nzslb", + "method": "Shader" + }, + { + "input": "Shaders/SkyboxMaterial.nzsl", + "output": "Shaders/SkyboxMaterial.nzslb", + "method": "Shader" + }, + { + "input": "logo.png", + "output": "Textures/Logo.dds", + "method": "Texture", + "generateMipmaps": false + }, + { + "input": "2d.passlist", + "output": "Passes/2d.passlist", + "method": "Copy" + }, + { + "input": "3d.passlist", + "output": "Passes/3d.passlist", + "method": "Copy" + }, + { + "input": "3d_dev.passlist", + "output": "Passes/3d_dev.passlist", + "method": "Copy" + }, + { + "input": "skybox.passlist", + "output": "Passes/skybox.passlist", + "method": "Copy" + } + ] +} \ No newline at end of file diff --git a/assets/shaders/PlayerPBR.nzsl b/assets/shaders/PlayerPBR.nzsl index d0b7de65..c9bcbb2a 100644 --- a/assets/shaders/PlayerPBR.nzsl +++ b/assets/shaders/PlayerPBR.nzsl @@ -171,8 +171,8 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] let B = cross(N, T); let tbnMatrix = mat3[f32](T, B, N); - normal = (MaterialNormalMap.Sample(input.uv).xyz * 2.0 - (1.0).rrr); - normal.y *= -1.0; + normal = vec3[f32](MaterialNormalMap.Sample(input.uv).xy * 2.0 - (1.0).rr, 0.0); + normal.z = sqrt(1.0 - normal.x - normal.y); normal = normalize(tbnMatrix * normal); } else @@ -180,13 +180,13 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] let albedo = color.xyz; let metallic = 0.0; - let roughness = 0.8; + let roughness = 0.3; const if (HasMetalnessSmoothnessTexture) { - let ms = MaterialMetalnessSmoothness.Sample(input.uv).xy; + let ms = MaterialMetalnessSmoothness.Sample(input.uv).xyza; metallic = ms.x; - roughness = 1.0 - ms.y; + roughness = (1.0 - ms.y); } const if (HasMetallicTexture) @@ -255,7 +255,7 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] ambient *= MaterialAmbientOcclusionMap.Sample(input.uv).x; let finalColor = ambient + lightRadiance; - finalColor = finalColor / (finalColor + vec3[f32](1.0, 1.0, 1.0)); + //finalColor = finalColor / (finalColor + vec3[f32](1.0, 1.0, 1.0)); return finalColor; } diff --git a/include/ClientLib/ClientAssetCooker.hpp b/include/ClientLib/ClientAssetCooker.hpp deleted file mode 100644 index ac700918..00000000 --- a/include/ClientLib/ClientAssetCooker.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) -// This file is part of the "This Space Of Mine" project -// For conditions of distribution and use, see copyright notice in LICENSE - -#pragma once - -#ifndef TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP -#define TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP - -#include -#include - -namespace Nz -{ - class ApplicationBase; -} - -namespace tsom -{ - class ClientBlockLibrary; - - class TSOM_CLIENTLIB_API ClientAssetCooker - { - public: - inline ClientAssetCooker(Nz::ApplicationBase& app); - ClientAssetCooker(const ClientAssetCooker&) = delete; - ClientAssetCooker(ClientAssetCooker&&) = default; - ~ClientAssetCooker() = default; - - Nz::Result Cook(ClientBlockLibrary& blockLibrary); - - ClientAssetCooker& operator=(const ClientAssetCooker&) = delete; - ClientAssetCooker& operator=(ClientAssetCooker&&) = default; - - private: - Nz::ApplicationBase& m_app; - }; -} - -#include - -#endif // TSOM_CLIENTLIB_CLIENTASSETCOOKER_HPP diff --git a/include/ClientLib/ClientBlockLibrary.hpp b/include/ClientLib/ClientBlockLibrary.hpp index 13197847..0e40ceba 100644 --- a/include/ClientLib/ClientBlockLibrary.hpp +++ b/include/ClientLib/ClientBlockLibrary.hpp @@ -8,8 +8,8 @@ #define TSOM_CLIENTLIB_CLIENTBLOCKLIBRARY_HPP #include -#include #include +#include namespace Nz { @@ -30,14 +30,14 @@ namespace tsom void BuildTexture(Nz::RenderDevice& renderDevice); - inline const std::shared_ptr& GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const; + inline const std::shared_ptr& GetBlockTexture(CookedBlockRegistry::TextureType textureType) const; inline const std::shared_ptr& GetGlobalBlockBuffer() const; inline const std::shared_ptr& GetPreviewTexture(BlockIndex blockIndex) const; private: std::shared_ptr m_globalBlockBuffer; std::vector> m_previewTextures; - Nz::EnumArray> m_blockTextures; + Nz::EnumArray> m_blockTextures; Nz::ApplicationBase& m_applicationBase; void* m_globalBlockBufferPtr; }; diff --git a/include/ClientLib/ClientBlockLibrary.inl b/include/ClientLib/ClientBlockLibrary.inl index d37dcd3b..3b469df9 100644 --- a/include/ClientLib/ClientBlockLibrary.inl +++ b/include/ClientLib/ClientBlockLibrary.inl @@ -14,7 +14,7 @@ namespace tsom m_blocks[idx].hasCollisions = true; } - inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(ClientAssetCookRegistry::TextureType textureType) const + inline const std::shared_ptr& ClientBlockLibrary::GetBlockTexture(CookedBlockRegistry::TextureType textureType) const { return m_blockTextures[textureType]; } diff --git a/include/ClientLib/ClientAssetCookRegistry.hpp b/include/CommonLib/CookedBlockRegistry.hpp similarity index 59% rename from include/ClientLib/ClientAssetCookRegistry.hpp rename to include/CommonLib/CookedBlockRegistry.hpp index 11984137..0b25b38e 100644 --- a/include/ClientLib/ClientAssetCookRegistry.hpp +++ b/include/CommonLib/CookedBlockRegistry.hpp @@ -4,10 +4,10 @@ #pragma once -#ifndef TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP -#define TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP +#ifndef TSOM_COMMONLIB_COOKEDBLOCKREGISTRY_HPP +#define TSOM_COMMONLIB_COOKEDBLOCKREGISTRY_HPP -#include +#include #include #include #include @@ -17,15 +17,15 @@ namespace tsom { - class TSOM_CLIENTLIB_API ClientAssetCookRegistry + class TSOM_COMMONLIB_API CookedBlockRegistry { public: struct BlockEntry; - ClientAssetCookRegistry() = default; - ClientAssetCookRegistry(const ClientAssetCookRegistry&) = delete; - ClientAssetCookRegistry(ClientAssetCookRegistry&&) = default; - ~ClientAssetCookRegistry() = default; + CookedBlockRegistry() = default; + CookedBlockRegistry(const CookedBlockRegistry&) = delete; + CookedBlockRegistry(CookedBlockRegistry&&) = default; + ~CookedBlockRegistry() = default; void AddBlock(std::string blockName, BlockEntry blockEntry); @@ -33,11 +33,11 @@ namespace tsom bool SaveToFile(const std::filesystem::path& path) const; - ClientAssetCookRegistry& operator=(const ClientAssetCookRegistry&) = delete; - ClientAssetCookRegistry& operator=(ClientAssetCookRegistry&&) = default; + CookedBlockRegistry& operator=(const CookedBlockRegistry&) = delete; + CookedBlockRegistry& operator=(CookedBlockRegistry&&) = default; - static std::optional LoadFromString(std::string_view content); - static std::optional LoadFromFile(const std::filesystem::path& path); + static std::optional LoadFromString(std::string_view content); + static std::optional LoadFromFile(const std::filesystem::path& path); enum class TextureType { @@ -73,6 +73,4 @@ namespace tsom }; } -#include - -#endif // TSOM_CLIENTLIB_CLIENTASSETCOOKREGISTRY_HPP +#endif // TSOM_COMMONLIB_COOKEDBLOCKREGISTRY_HPP diff --git a/include/ClientLib/ClientAssetCookRegistry.inl b/include/CommonLib/CookedBlockRegistry.inl similarity index 100% rename from include/ClientLib/ClientAssetCookRegistry.inl rename to include/CommonLib/CookedBlockRegistry.inl diff --git a/src/AssetCooker/BlockCooker.cpp b/src/AssetCooker/BlockCooker.cpp new file mode 100644 index 00000000..c04161e5 --- /dev/null +++ b/src/AssetCooker/BlockCooker.cpp @@ -0,0 +1,376 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + BlockCooker::BlockCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_outputDir(outputDir / Nz::Utf8Path(doc.at("output").get())), + m_textureSize(doc.at("size")) + { + std::filesystem::path inputFile = inputDir / Nz::Utf8Path(doc.at("input").get()); + + std::optional> inputContent = Nz::File::ReadWhole(inputFile); + if (!inputContent) + throw std::runtime_error(fmt::format("failed to read {}", Nz::PathToString(inputFile))); + + if (!m_blockLibrary.LoadFromString(std::string_view(reinterpret_cast(inputContent->data()), inputContent->size()))) + throw std::runtime_error(fmt::format("failed to load block library", Nz::PathToString(inputFile))); + + const auto& blocks = m_blockLibrary.GetBlocks(); + m_inputs.reserve(blocks.size()); + + std::string textureInputDir = Nz::PathToString(inputFile.parent_path()); + for (const BlockLibrary::BlockData& blockData : blocks) + { + auto& inputData = m_inputs.emplace_back(); + inputData.files[TextureType::BaseColor] = Nz::Utf8Path(fmt::format("{}/{}.png", textureInputDir, blockData.basePath)); + inputData.files[TextureType::AmbientOcclusion] = Nz::Utf8Path(fmt::format("{}/{}_ao.png", textureInputDir, blockData.basePath)); + inputData.files[TextureType::Height] = Nz::Utf8Path(fmt::format("{}/{}_height.png", textureInputDir, blockData.basePath)); + inputData.files[TextureType::Metalness] = Nz::Utf8Path(fmt::format("{}/{}_metallic.png", textureInputDir, blockData.basePath)); + inputData.files[TextureType::Normal] = Nz::Utf8Path(fmt::format("{}/{}_normal.png", textureInputDir, blockData.basePath)); + inputData.files[TextureType::Roughness] = Nz::Utf8Path(fmt::format("{}/{}_roughness.png", textureInputDir, blockData.basePath)); + + for (auto&& [textureType, texturePath] : inputData.files.iter_kv()) + { + if (std::filesystem::is_regular_file(texturePath)) + m_inputFiles.push_back(texturePath); + else + texturePath.clear(); + } + + if (!inputData.files[TextureType::BaseColor].empty()) + m_outputFiles.push_back(m_outputDir / Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name))); + + if (!inputData.files[TextureType::Normal].empty()) + m_outputFiles.push_back(m_outputDir / Nz::Utf8Path(fmt::format("{}_normal.dds", blockData.name))); + + if (!inputData.files[TextureType::Roughness].empty() && !inputData.files[TextureType::Metalness].empty()) + m_outputFiles.push_back(m_outputDir / Nz::Utf8Path(fmt::format("{}_roughness_metalness.dds", blockData.name))); + else if (!inputData.files[TextureType::Roughness].empty()) + m_outputFiles.push_back(m_outputDir / Nz::Utf8Path(fmt::format("{}_roughness.dds", blockData.name))); + + if (!inputData.files[TextureType::AmbientOcclusion].empty()) + m_outputFiles.push_back(m_outputDir / Nz::Utf8Path(fmt::format("{}_ao.dds", blockData.name))); + } + } + + void BlockCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + // TODO: Split in more subtasks + taskScheduler.AddTask([this] + { + if (!std::filesystem::is_directory(m_outputDir)) + std::filesystem::create_directories(m_outputDir); + + CookedBlockRegistry registry; + + const auto& blocks = m_blockLibrary.GetBlocks(); + for (std::size_t blockIndex = 0; blockIndex < blocks.size(); ++blockIndex) + { + const auto& inputData = m_inputs[blockIndex]; + const auto& blockData = blocks[blockIndex]; + + CookedBlockRegistry::BlockEntry blockEntry; + + // Handle color map + if (!inputData.files[TextureType::BaseColor].empty()) + { + std::shared_ptr baseColor = Nz::Image::LoadFromFile(inputData.files[TextureType::BaseColor]); + if (!baseColor) + { + spdlog::error("failed to load {} base color", blockData.name); + return; + } + + if (baseColor->GetFormat() != Nz::PixelFormat::RGB8 && baseColor->GetFormat() != Nz::PixelFormat::RGBA8) + { + spdlog::error("{} color map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(baseColor->GetFormat())); + return; + } + + blockEntry.baseColorFallback = Nz::Color::sRGBToLinear(baseColor->ComputeAverageColor()); + spdlog::debug("{} base color map average color: {};{};{};{}", blockData.name, blockEntry.baseColorFallback.r, blockEntry.baseColorFallback.g, blockEntry.baseColorFallback.b, blockEntry.baseColorFallback.a); + + bool hasAlpha = baseColor->HasAlpha(); // test before potential resize (faster) + + if (baseColor->GetSize() != Nz::Vector3ui32(m_textureSize, m_textureSize, 1)) + { + spdlog::warn("{} base color has an unexpected size", blockData.name); + baseColor->Resize(m_textureSize, m_textureSize); + } + + baseColor->GenerateMipmaps(); + + std::string colorFilename = fmt::format("{}_color.dds", blockData.name); + + Nz::Image compressedBaseColor; + if (hasAlpha) + { + // Compress using BC3 + // TODO: Detect 1bit alpha + spdlog::debug("{} base color map has alpha", blockData.name); + + compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC3(*baseColor); + blockEntry.baseColorTexture = { CookedBlockRegistry::TextureType::BC3, colorFilename }; + } + else + { + // Compress using BC1 + spdlog::debug("{} base color map has no alpha", blockData.name); + + if (baseColor->GetFormat() == Nz::PixelFormat::RGBA8) + compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC1(*baseColor); + else + compressedBaseColor = Nz::ImageCompressor::RGB8ToBC1(*baseColor); + + blockEntry.baseColorTexture = { CookedBlockRegistry::TextureType::BC1, colorFilename }; + } + + std::filesystem::path targetBaseColorPath = m_outputDir / Nz::Utf8Path(colorFilename); + if (!compressedBaseColor.SaveToFile(targetBaseColorPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(targetBaseColorPath)); + return; + } + } + + // Handle normal maps + if (!inputData.files[TextureType::Normal].empty()) + { + std::shared_ptr normalMap = Nz::Image::LoadFromFile(inputData.files[TextureType::Normal]); + if (!normalMap) + { + spdlog::error("failed to load {} normal map", blockData.name); + return; + } + + if (normalMap->GetFormat() != Nz::PixelFormat::RGB8 && normalMap->GetFormat() != Nz::PixelFormat::RGBA8) + { + spdlog::error("{} normal map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(normalMap->GetFormat())); + return; + } + + if (normalMap->GetSize() != Nz::Vector3ui32(m_textureSize, m_textureSize, 1)) + { + spdlog::error("{} normal map has an unexpected size", blockData.name); + return; + } + + Nz::Image cookedNormalMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, m_textureSize, m_textureSize); + + const Nz::UInt8* sourcePixels = normalMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedNormalMap.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(normalMap->GetFormat()); + + for (std::size_t y = 0; y < m_textureSize; ++y) + { + for (std::size_t x = 0; x < m_textureSize; ++x) + { + if (sourcePixels[2] < 127) + spdlog::debug("{} normal map {};{} has Z value < 127: {}", blockData.name, x, y, sourcePixels[2]); + + cookedPixels[0] = sourcePixels[0]; + cookedPixels[1] = sourcePixels[1]; + + sourcePixels += bpp; + cookedPixels += 2; + } + } + cookedNormalMap.GenerateMipmaps(); + + cookedNormalMap = Nz::ImageCompressor::RG8ToBC5(cookedNormalMap); + + std::string normalFilename = fmt::format("{}_normal.dds", blockData.name); + blockEntry.normalMapTexture = { CookedBlockRegistry::TextureType::BC5, normalFilename }; + + std::filesystem::path targetPath = m_outputDir / Nz::Utf8Path(normalFilename); + if (!cookedNormalMap.SaveToFile(targetPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(targetPath)); + return; + } + } + + // Roughness/metalness + blockEntry.metalnessFallback = blockData.metalness; + blockEntry.roughnessFallback = blockData.roughness; + + if (!inputData.files[TextureType::Roughness].empty()) + { + std::shared_ptr roughnessMap = Nz::Image::LoadFromFile(inputData.files[TextureType::Roughness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!roughnessMap) + { + spdlog::error("failed to load {} roughness map", blockData.name); + return; + } + + if (roughnessMap->GetSize() != Nz::Vector3ui32(m_textureSize, m_textureSize, 1)) + { + spdlog::error("{} roughness map has an unexpected size", blockData.name); + return; + } + + blockEntry.roughnessFallback = roughnessMap->ComputeAverageColor().r; + + if (!inputData.files[TextureType::Metalness].empty()) + { + // BC5 roughness/metalness + std::shared_ptr metalnessMap = Nz::Image::LoadFromFile(inputData.files[TextureType::Metalness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!metalnessMap) + { + spdlog::error("failed to load {} metalness map", blockData.name); + return; + } + + if (metalnessMap->GetSize() != Nz::Vector3ui32(m_textureSize, m_textureSize, 1)) + { + spdlog::error("{} metalness map has an unexpected size", blockData.name); + return; + } + + blockEntry.metalnessFallback = metalnessMap->ComputeAverageColor().r; + + Nz::Image cookedRoughnessMetalnessMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, m_textureSize, m_textureSize); + + const Nz::UInt8* roughnessPixels = roughnessMap->GetConstPixels(); + const Nz::UInt8* metalnessPixels = metalnessMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedRoughnessMetalnessMap.GetPixels(); + for (std::size_t y = 0; y < m_textureSize; ++y) + { + for (std::size_t x = 0; x < m_textureSize; ++x) + { + cookedPixels[0] = *roughnessPixels++; + cookedPixels[1] = *metalnessPixels++; + + cookedPixels += 2; + } + } + cookedRoughnessMetalnessMap.GenerateMipmaps(); + + cookedRoughnessMetalnessMap = Nz::ImageCompressor::RG8ToBC5(cookedRoughnessMetalnessMap); + + std::string roughnessMetalnessFilename = fmt::format("{}_roughness_metalness.dds", blockData.name); + blockEntry.roughnessMetalnessTexture = { CookedBlockRegistry::TextureType::BC5, roughnessMetalnessFilename }; + + std::filesystem::path targetPath = m_outputDir / Nz::Utf8Path(roughnessMetalnessFilename); + if (!cookedRoughnessMetalnessMap.SaveToFile(targetPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(targetPath)); + return; + } + } + else + { + // BC4 roughness + Nz::Image cookedRoughnessMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, m_textureSize, m_textureSize); + + const Nz::UInt8* sourcePixels = roughnessMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedRoughnessMap.GetPixels(); + for (std::size_t y = 0; y < m_textureSize; ++y) + { + for (std::size_t x = 0; x < m_textureSize; ++x) + *cookedPixels++ = *sourcePixels++; + } + cookedRoughnessMap.GenerateMipmaps(); + + cookedRoughnessMap = Nz::ImageCompressor::R8ToBC4(cookedRoughnessMap); + + std::string roughnessMetalnessFilename = fmt::format("{}_roughness.dds", blockData.name); + blockEntry.roughnessMetalnessTexture = { CookedBlockRegistry::TextureType::BC4, roughnessMetalnessFilename }; + + std::filesystem::path targetPath = m_outputDir / Nz::Utf8Path(roughnessMetalnessFilename); + if (!cookedRoughnessMap.SaveToFile(targetPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(targetPath)); + return; + } + } + } + else if (!inputData.files[TextureType::Metalness].empty()) + { + spdlog::warn("{} block has no roughness map but has a metalness map, this is unexpected, no roughness-metalness map will be cooked", blockData.name); + } + + // Ambient Occlusion (+ Heightmap once used) + blockEntry.ambientOcclusionFallback = 1.0f; + + if (!inputData.files[TextureType::AmbientOcclusion].empty()) + { + // BC4 + std::shared_ptr aoMap = Nz::Image::LoadFromFile(inputData.files[TextureType::AmbientOcclusion], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); + if (!aoMap) + { + spdlog::error("failed to load {} ambient occlusion map", blockData.name); + return; + } + + if (aoMap->GetSize() != Nz::Vector3ui32(m_textureSize, m_textureSize, 1)) + { + spdlog::error("{} ambient occlusion map has an unexpected size", blockData.name); + return; + } + + blockEntry.ambientOcclusionFallback = aoMap->ComputeAverageColor().r; + + Nz::Image cookedAOMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, m_textureSize, m_textureSize); + + const Nz::UInt8* aoPixels = aoMap->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedAOMap.GetPixels(); + for (std::size_t y = 0; y < m_textureSize; ++y) + { + for (std::size_t x = 0; x < m_textureSize; ++x) + *cookedPixels++ = *aoPixels++; + } + + cookedAOMap.GenerateMipmaps(); + + cookedAOMap = Nz::ImageCompressor::R8ToBC4(cookedAOMap); + + std::string aoHeightFilename = fmt::format("{}_ao.dds", blockData.name); + blockEntry.ambientOcclusionHeightTexture = { CookedBlockRegistry::TextureType::BC4, aoHeightFilename }; + + std::filesystem::path targetPath = m_outputDir / Nz::Utf8Path(aoHeightFilename); + if (!cookedAOMap.SaveToFile(targetPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(targetPath)); + return; + } + } + + registry.AddBlock(blockData.name, std::move(blockEntry)); + } + + std::filesystem::path registryPath = m_outputDir / Nz::Utf8Path("registry.json"); + if (!registry.SaveToFile(registryPath)) + { + spdlog::error("failed to save file {}", Nz::PathToString(registryPath)); + return; + } + }); + } + + auto BlockCooker::GetInputFiles() const -> InputFileList + { + return m_inputFiles; + } + + auto BlockCooker::GetOutputFiles() const -> OutputFileList + { + return m_outputFiles; + } + +} diff --git a/src/AssetCooker/BlockCooker.hpp b/src/AssetCooker/BlockCooker.hpp new file mode 100644 index 00000000..c1921f3d --- /dev/null +++ b/src/AssetCooker/BlockCooker.hpp @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_BLOCKCOOKER_HPP +#define TSOM_ASSETCOOKER_BLOCKCOOKER_HPP + +#include +#include +#include + +namespace tsom +{ + class BlockCooker : public Cooker + { + public: + BlockCooker(const std::filesystem::path& inputFile, const std::filesystem::path& outputFile, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + enum class TextureType + { + AmbientOcclusion, + BaseColor, + Height, + Metalness, + Normal, + Roughness, + + Max = Roughness + }; + + enum class CookedTextureType + { + BaseColor, + Normal, + MaterialData + }; + + struct InputData + { + Nz::EnumArray files; + }; + + std::filesystem::path m_outputDir; + std::vector m_inputs; + BlockLibrary m_blockLibrary; + InputFileList m_inputFiles; + OutputFileList m_outputFiles; + Nz::UInt32 m_textureSize; + }; +} + +#endif // TSOM_ASSETCOOKER_BLOCKCOOKER_HPP diff --git a/include/ClientLib/ClientAssetCooker.inl b/src/AssetCooker/Cooker.cpp similarity index 71% rename from include/ClientLib/ClientAssetCooker.inl rename to src/AssetCooker/Cooker.cpp index e0613dd3..fda86c16 100644 --- a/include/ClientLib/ClientAssetCooker.inl +++ b/src/AssetCooker/Cooker.cpp @@ -2,10 +2,9 @@ // This file is part of the "This Space Of Mine" project // For conditions of distribution and use, see copyright notice in LICENSE +#include + namespace tsom { - inline ClientAssetCooker::ClientAssetCooker(Nz::ApplicationBase& app) : - m_app(app) - { - } + Cooker::~Cooker() = default; } diff --git a/src/AssetCooker/Cooker.hpp b/src/AssetCooker/Cooker.hpp new file mode 100644 index 00000000..ea53fbd6 --- /dev/null +++ b/src/AssetCooker/Cooker.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_COOKER_HPP +#define TSOM_ASSETCOOKER_COOKER_HPP + +#include +#include + +namespace Nz +{ + class TaskScheduler; +} + +namespace tsom +{ + class Cooker + { + public: + using InputFileList = Nz::HybridVector; + using OutputFileList = Nz::HybridVector; + + virtual ~Cooker(); + + virtual void Cook(Nz::TaskScheduler& taskScheduler) = 0; + + virtual InputFileList GetInputFiles() const = 0; + virtual OutputFileList GetOutputFiles() const = 0; + }; +} + +#endif // TSOM_ASSETCOOKER_COOKER_HPP diff --git a/src/AssetCooker/CopyCooker.cpp b/src/AssetCooker/CopyCooker.cpp new file mode 100644 index 00000000..538d3b11 --- /dev/null +++ b/src/AssetCooker/CopyCooker.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include + +namespace tsom +{ + CopyCooker::CopyCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_inputFile(inputDir / Nz::Utf8Path(doc.at("input").get())), + m_outputFile(outputDir / Nz::Utf8Path(doc.at("output").get())) + { + } + + void CopyCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + taskScheduler.AddTask([this] + { + std::filesystem::path outputDir = m_outputFile.parent_path(); + + std::error_code ec; + std::filesystem::create_directories(outputDir, ec); + + if (ec && ec != std::errc::is_a_directory) + { + spdlog::error("failed to create {}: {}", Nz::PathToString(outputDir), ec.message()); + return; + } + + ec = {}; + std::filesystem::copy_file(m_inputFile, m_outputFile, std::filesystem::copy_options::overwrite_existing, ec); + + if (ec) + { + spdlog::error("failed to copy file from {} to {}: {}", Nz::PathToString(m_inputFile), Nz::PathToString(m_outputFile), ec.message()); + return; + } + }); + } + + auto CopyCooker::GetInputFiles() const -> InputFileList + { + return { m_inputFile }; + } + + auto CopyCooker::GetOutputFiles() const -> OutputFileList + { + return { m_outputFile }; + } +} diff --git a/src/AssetCooker/CopyCooker.hpp b/src/AssetCooker/CopyCooker.hpp new file mode 100644 index 00000000..9569bfca --- /dev/null +++ b/src/AssetCooker/CopyCooker.hpp @@ -0,0 +1,31 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_COPYCOOKER_HPP +#define TSOM_ASSETCOOKER_COPYCOOKER_HPP + +#include +#include + +namespace tsom +{ + class CopyCooker : public Cooker + { + public: + CopyCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + std::filesystem::path m_inputFile; + std::filesystem::path m_outputFile; + }; +} + +#endif // TSOM_ASSETCOOKER_COPYCOOKER_HPP diff --git a/src/AssetCooker/CubemapCooker.cpp b/src/AssetCooker/CubemapCooker.cpp new file mode 100644 index 00000000..48f47aa9 --- /dev/null +++ b/src/AssetCooker/CubemapCooker.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + CubemapCooker::CubemapCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_inputFile(inputDir / Nz::Utf8Path(doc.at("input").get())), + m_outputFile(outputDir / Nz::Utf8Path(doc.at("output").get())) + { + } + + void CubemapCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + taskScheduler.AddTask([this] + { + std::filesystem::path outputDir = m_outputFile.parent_path(); + + std::error_code ec; + std::filesystem::create_directories(outputDir, ec); + + if (ec && ec != std::errc::is_a_directory) + { + spdlog::error("failed to create {}: {}", Nz::PathToString(outputDir), ec.message()); + return; + } + + Nz::ImageParams imageParams; + imageParams.loadFormat = Nz::PixelFormat::RGBA8_SRGB; + + std::shared_ptr inputImage = Nz::Image::LoadFromFile(m_inputFile, imageParams, Nz::CubemapParams{}); + if (!inputImage) + { + spdlog::error("failed to load image from {}", Nz::PathToString(m_inputFile)); + return; + } + + if (!inputImage->GenerateMipmaps()) + { + spdlog::error("failed to generate mipmaps for {}", Nz::PathToString(m_outputFile)); + return; + } + + Nz::Image compressedImage = Nz::ImageCompressor::RGBA8ToBC1(*inputImage); + if (!compressedImage.SaveToFile(m_outputFile)) + { + spdlog::error("failed to save file to {}", Nz::PathToString(m_outputFile)); + return; + } + }); + } + + auto CubemapCooker::GetInputFiles() const -> InputFileList + { + return { m_inputFile }; + } + + auto CubemapCooker::GetOutputFiles() const -> OutputFileList + { + return { m_outputFile }; + } +} diff --git a/src/AssetCooker/CubemapCooker.hpp b/src/AssetCooker/CubemapCooker.hpp new file mode 100644 index 00000000..414c7dca --- /dev/null +++ b/src/AssetCooker/CubemapCooker.hpp @@ -0,0 +1,31 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_CUBEMAPCOOKER_HPP +#define TSOM_ASSETCOOKER_CUBEMAPCOOKER_HPP + +#include +#include + +namespace tsom +{ + class CubemapCooker : public Cooker + { + public: + CubemapCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + std::filesystem::path m_inputFile; + std::filesystem::path m_outputFile; + }; +} + +#endif // TSOM_ASSETCOOKER_CUBEMAPCOOKER_HPP diff --git a/src/AssetCooker/CubemapSplitFacesCooker.cpp b/src/AssetCooker/CubemapSplitFacesCooker.cpp new file mode 100644 index 00000000..bd63cf74 --- /dev/null +++ b/src/AssetCooker/CubemapSplitFacesCooker.cpp @@ -0,0 +1,94 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + CubemapSplitFacesCooker::CubemapSplitFacesCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_outputFile(outputDir / Nz::Utf8Path(doc.at("output").get())), + m_sRGB(doc.value("sRGB", true)) + { + nlohmann::json faces = doc["faces"]; + m_inputFiles[Nz::CubemapFace::PositiveX] = inputDir / Nz::Utf8Path(faces.at("+x").get()); + m_inputFiles[Nz::CubemapFace::NegativeX] = inputDir / Nz::Utf8Path(faces.at("-x").get()); + m_inputFiles[Nz::CubemapFace::PositiveY] = inputDir / Nz::Utf8Path(faces.at("+y").get()); + m_inputFiles[Nz::CubemapFace::NegativeY] = inputDir / Nz::Utf8Path(faces.at("-y").get()); + m_inputFiles[Nz::CubemapFace::PositiveZ] = inputDir / Nz::Utf8Path(faces.at("+z").get()); + m_inputFiles[Nz::CubemapFace::NegativeZ] = inputDir / Nz::Utf8Path(faces.at("-z").get()); + } + + void CubemapSplitFacesCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + taskScheduler.AddTask([this] + { + std::filesystem::path outputDir = m_outputFile.parent_path(); + + std::error_code ec; + std::filesystem::create_directories(outputDir, ec); + + if (ec && ec != std::errc::is_a_directory) + { + spdlog::error("failed to create {}: {}", Nz::PathToString(outputDir), ec.message()); + return; + } + + Nz::EnumArray> faceImages; + + Nz::ImageParams imageParams; + imageParams.loadFormat = (m_sRGB) ? Nz::PixelFormat::RGBA8_SRGB : Nz::PixelFormat::RGBA8; + + for (auto&& [cubemapFace, path] : m_inputFiles.iter_kv()) + { + faceImages[cubemapFace] = Nz::Image::LoadFromFile(path, imageParams); + if (!faceImages[cubemapFace]) + { + spdlog::error("failed to load image from {}", Nz::PathToString(path)); + return; + } + } + + const Nz::Image& referenceImage = *faceImages.front(); + Nz::Image image(Nz::ImageType::Cubemap, imageParams.loadFormat, referenceImage.GetWidth(), referenceImage.GetHeight()); + for (auto&& [cubemapFace, faceImage] : faceImages.iter_kv()) + { + if (!image.LoadFaceFromImage(cubemapFace, *faceImage)) + { + spdlog::error("failed to load face image from {}", Nz::PathToString(m_inputFiles[cubemapFace])); + return; + } + } + + if (!image.GenerateMipmaps()) + { + spdlog::error("failed to generate mipmaps for {}", Nz::PathToString(m_outputFile)); + return; + } + + image = Nz::ImageCompressor::RGBA8ToBC1(image); + + if (!image.SaveToFile(m_outputFile)) + { + spdlog::error("failed to save file to {}", Nz::PathToString(m_outputFile)); + return; + } + }); + } + + auto CubemapSplitFacesCooker::GetInputFiles() const -> InputFileList + { + return { m_inputFiles.begin(), m_inputFiles.end() }; + } + + auto CubemapSplitFacesCooker::GetOutputFiles() const -> OutputFileList + { + return { m_outputFile }; + } +} diff --git a/src/AssetCooker/CubemapSplitFacesCooker.hpp b/src/AssetCooker/CubemapSplitFacesCooker.hpp new file mode 100644 index 00000000..1e6dcd05 --- /dev/null +++ b/src/AssetCooker/CubemapSplitFacesCooker.hpp @@ -0,0 +1,34 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_CUBEMAPSPLITFACESCOOKER_HPP +#define TSOM_ASSETCOOKER_CUBEMAPSPLITFACESCOOKER_HPP + +#include +#include +#include +#include + +namespace tsom +{ + class CubemapSplitFacesCooker : public Cooker + { + public: + CubemapSplitFacesCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + Nz::EnumArray m_inputFiles; + std::filesystem::path m_outputFile; + bool m_sRGB; + }; +} + +#endif // TSOM_ASSETCOOKER_CUBEMAPSPLITFACESCOOKER_HPP diff --git a/src/AssetCooker/ShaderCooker.cpp b/src/AssetCooker/ShaderCooker.cpp new file mode 100644 index 00000000..9d285bfd --- /dev/null +++ b/src/AssetCooker/ShaderCooker.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include + +namespace tsom +{ + ShaderCooker::ShaderCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_inputFile(inputDir / Nz::Utf8Path(doc.at("input").get())), + m_outputFile(outputDir / Nz::Utf8Path(doc.at("output").get())) + { + } + + void ShaderCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + taskScheduler.AddTask([this] + { + std::filesystem::path outputDir = m_outputFile.parent_path(); + + std::error_code ec; + std::filesystem::create_directories(outputDir, ec); + + if (ec && ec != std::errc::is_a_directory) + { + spdlog::error("failed to create {}: {}", Nz::PathToString(outputDir), ec.message()); + return; + } + + ec = {}; + std::filesystem::copy_file(m_inputFile, m_outputFile, std::filesystem::copy_options::overwrite_existing, ec); + + if (ec) + { + spdlog::error("failed to copy file from {} to {}: {}", Nz::PathToString(m_inputFile), Nz::PathToString(m_outputFile), ec.message()); + return; + } + }); + } + + auto ShaderCooker::GetInputFiles() const -> InputFileList + { + return { m_inputFile }; + } + + auto ShaderCooker::GetOutputFiles() const -> OutputFileList + { + return { m_outputFile }; + } +} diff --git a/src/AssetCooker/ShaderCooker.hpp b/src/AssetCooker/ShaderCooker.hpp new file mode 100644 index 00000000..7033e559 --- /dev/null +++ b/src/AssetCooker/ShaderCooker.hpp @@ -0,0 +1,31 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_SHADERCOOKER_HPP +#define TSOM_ASSETCOOKER_SHADERCOOKER_HPP + +#include +#include + +namespace tsom +{ + class ShaderCooker : public Cooker + { + public: + ShaderCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + std::filesystem::path m_inputFile; + std::filesystem::path m_outputFile; + }; +} + +#endif // TSOM_ASSETCOOKER_SHADERCOOKER_HPP diff --git a/src/AssetCooker/TextureCooker.cpp b/src/AssetCooker/TextureCooker.cpp new file mode 100644 index 00000000..50b462bd --- /dev/null +++ b/src/AssetCooker/TextureCooker.cpp @@ -0,0 +1,279 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include + +namespace nlohmann +{ + NLOHMANN_JSON_SERIALIZE_ENUM(tsom::TextureCooker::ChannelSource, { + {tsom::TextureCooker::ChannelSource::Red, "Red"}, + {tsom::TextureCooker::ChannelSource::Green, "Green"}, + {tsom::TextureCooker::ChannelSource::Blue, "Blue"}, + {tsom::TextureCooker::ChannelSource::Alpha, "Alpha"} + }); + + NLOHMANN_JSON_SERIALIZE_ENUM(tsom::TextureCooker::TextureType, { + {tsom::TextureCooker::TextureType::Color, "Color"}, + {tsom::TextureCooker::TextureType::Normal, "Normal"}, + {tsom::TextureCooker::TextureType::Greyscale, "Greyscale"}, + {tsom::TextureCooker::TextureType::BiGreyscale, "BiGreyscale"} + }); +} + +namespace tsom +{ + TextureCooker::TextureCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) : + m_inputFile(inputDir / Nz::Utf8Path(doc.at("input").get())), + m_outputFile(outputDir / Nz::Utf8Path(doc.at("output").get())), + m_compress(doc.value("compress", true)), + m_generateMipmaps(doc.value("generateMipmaps", true)), + m_textureType(doc.value("type", TextureType::Color)) + { + m_channelSources[0] = doc.value("channel0", ChannelSource::Red); + m_channelSources[1] = doc.value("channel1", ChannelSource::Green); + m_channelSources[2] = doc.value("channel2", ChannelSource::Blue); + m_channelSources[3] = doc.value("channel3", ChannelSource::Alpha); + } + + void TextureCooker::Cook(Nz::TaskScheduler& taskScheduler) + { + taskScheduler.AddTask([this] + { + std::filesystem::path outputDir = m_outputFile.parent_path(); + + std::error_code ec; + std::filesystem::create_directories(outputDir, ec); + + if (ec && ec != std::errc::is_a_directory) + { + spdlog::error("failed to create {}: {}", Nz::PathToString(outputDir), ec.message()); + return; + } + + bool noSwizzling = m_channelSources[0] == ChannelSource::Red && m_channelSources[1] == ChannelSource::Green && m_channelSources[2] == ChannelSource::Blue && m_channelSources[3] == ChannelSource::Alpha; + + Nz::ImageParams imageParams; + switch (m_textureType) + { + case TextureType::Color: + imageParams.loadFormat = Nz::PixelFormat::RGBA8_SRGB; + break; + + case TextureType::Greyscale: + imageParams.loadFormat = (noSwizzling) ? Nz::PixelFormat::R8 : Nz::PixelFormat::RGBA8; + break; + + case TextureType::BiGreyscale: + imageParams.loadFormat = (noSwizzling) ? Nz::PixelFormat::RG8 : Nz::PixelFormat::RGBA8; + break; + + case TextureType::Normal: + imageParams.loadFormat = (noSwizzling) ? Nz::PixelFormat::RGB8 : Nz::PixelFormat::RGBA8; + break; + } + + std::shared_ptr inputImage = Nz::Image::LoadFromFile(m_inputFile, imageParams); + if (!inputImage) + { + spdlog::error("failed to load image from {}", Nz::PathToString(m_inputFile)); + return; + } + + Nz::Image cookedImage; + switch (m_textureType) + { + case TextureType::Color: + { + if (noSwizzling) + { + // Image is already good + cookedImage = std::move(*inputImage); + inputImage.reset(); + break; + } + + // Apply swizzling + Nz::UInt32 width = inputImage->GetWidth(); + Nz::UInt32 height = inputImage->GetHeight(); + cookedImage.Create(Nz::ImageType::E2D, Nz::PixelFormat::RGBA8, width, height); + + const Nz::UInt8* sourcePixels = inputImage->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedImage.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(inputImage->GetFormat()); + + Nz::UInt32 channelR = Nz::UnderlyingCast(m_channelSources[0]); + Nz::UInt32 channelG = Nz::UnderlyingCast(m_channelSources[1]); + Nz::UInt32 channelB = Nz::UnderlyingCast(m_channelSources[2]); + Nz::UInt32 channelA = Nz::UnderlyingCast(m_channelSources[3]); + + for (std::size_t y = 0; y < height; ++y) + { + for (std::size_t x = 0; x < width; ++x) + { + cookedPixels[0] = sourcePixels[channelR]; + cookedPixels[1] = sourcePixels[channelG]; + cookedPixels[2] = sourcePixels[channelB]; + cookedPixels[3] = sourcePixels[channelA]; + + sourcePixels += bpp; + cookedPixels += 4; + } + } + + break; + } + + case TextureType::Greyscale: + { + if (noSwizzling) + { + // Image is already good + cookedImage = std::move(*inputImage); + inputImage.reset(); + break; + } + + // Apply swizzling + Nz::UInt32 width = inputImage->GetWidth(); + Nz::UInt32 height = inputImage->GetHeight(); + cookedImage.Create(Nz::ImageType::E2D, Nz::PixelFormat::R8, width, height); + + const Nz::UInt8* sourcePixels = inputImage->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedImage.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(inputImage->GetFormat()); + + Nz::UInt32 channelR = Nz::UnderlyingCast(m_channelSources[0]); + + for (std::size_t y = 0; y < height; ++y) + { + for (std::size_t x = 0; x < width; ++x) + { + *cookedPixels++ = sourcePixels[channelR]; + sourcePixels += bpp; + } + } + break; + } + + case TextureType::BiGreyscale: + { + if (noSwizzling) + { + // Image is already good + cookedImage = std::move(*inputImage); + inputImage.reset(); + break; + } + + // Apply swizzling + Nz::UInt32 width = inputImage->GetWidth(); + Nz::UInt32 height = inputImage->GetHeight(); + cookedImage.Create(Nz::ImageType::E2D, Nz::PixelFormat::RG8, width, height); + + const Nz::UInt8* sourcePixels = inputImage->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedImage.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(inputImage->GetFormat()); + + Nz::UInt32 channelR = Nz::UnderlyingCast(m_channelSources[0]); + Nz::UInt32 channelG = Nz::UnderlyingCast(m_channelSources[1]); + + for (std::size_t y = 0; y < height; ++y) + { + for (std::size_t x = 0; x < width; ++x) + { + cookedPixels[0] = sourcePixels[channelR]; + cookedPixels[1] = sourcePixels[channelG]; + + cookedPixels += 2; + sourcePixels += bpp; + } + } + break; + } + + case TextureType::Normal: + { + Nz::UInt32 width = inputImage->GetWidth(); + Nz::UInt32 height = inputImage->GetHeight(); + cookedImage.Create(Nz::ImageType::E2D, Nz::PixelFormat::RG8, width, height); + + const Nz::UInt8* sourcePixels = inputImage->GetConstPixels(); + Nz::UInt8* cookedPixels = cookedImage.GetPixels(); + Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(inputImage->GetFormat()); + + Nz::UInt32 channel0 = Nz::UnderlyingCast(m_channelSources[0]); + Nz::UInt32 channel1 = Nz::UnderlyingCast(m_channelSources[1]); + Nz::UInt32 channel2 = Nz::UnderlyingCast(m_channelSources[2]); + + for (std::size_t y = 0; y < height; ++y) + { + for (std::size_t x = 0; x < width; ++x) + { + if (sourcePixels[channel2] < 127) + spdlog::warn("normal map pixel at ({};{}) has Z value < 127: {}", x, y, sourcePixels[2]); + + cookedPixels[0] = sourcePixels[channel0]; + cookedPixels[1] = sourcePixels[channel1]; + + sourcePixels += bpp; + cookedPixels += 2; + } + } + break; + } + } + + if (m_generateMipmaps) + { + if (!cookedImage.GenerateMipmaps()) + { + spdlog::error("failed to generate mipmaps for {}", Nz::PathToString(m_outputFile)); + return; + } + } + + switch (m_textureType) + { + case TextureType::Color: + if (cookedImage.HasAlpha()) + cookedImage = Nz::ImageCompressor::RGBA8ToBC3(cookedImage); + else + cookedImage = Nz::ImageCompressor::RGBA8ToBC1(cookedImage); + + break; + + case TextureType::Greyscale: + cookedImage = Nz::ImageCompressor::R8ToBC4(cookedImage); + break; + + case TextureType::BiGreyscale: + case TextureType::Normal: + cookedImage = Nz::ImageCompressor::RG8ToBC5(cookedImage); + break; + } + + if (!cookedImage.SaveToFile(m_outputFile)) + { + spdlog::error("failed to save file to {}", Nz::PathToString(m_outputFile)); + return; + } + }); + } + + auto TextureCooker::GetInputFiles() const -> InputFileList + { + return { m_inputFile }; + } + + auto TextureCooker::GetOutputFiles() const -> OutputFileList + { + return { m_outputFile }; + } +} diff --git a/src/AssetCooker/TextureCooker.hpp b/src/AssetCooker/TextureCooker.hpp new file mode 100644 index 00000000..a12efdb5 --- /dev/null +++ b/src/AssetCooker/TextureCooker.hpp @@ -0,0 +1,52 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_ASSETCOOKER_TEXTURECOOKER_HPP +#define TSOM_ASSETCOOKER_TEXTURECOOKER_HPP + +#include +#include +#include + +namespace tsom +{ + class TextureCooker : public Cooker + { + public: + enum class ChannelSource + { + Red, + Green, + Blue, + Alpha + }; + + enum class TextureType + { + Color, + Normal, + Greyscale, + BiGreyscale + }; + + TextureCooker(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + + void Cook(Nz::TaskScheduler& taskScheduler) override; + + InputFileList GetInputFiles() const override; + OutputFileList GetOutputFiles() const override; + + private: + std::array m_channelSources; + std::filesystem::path m_inputFile; + std::filesystem::path m_outputFile; + bool m_compress; + bool m_generateMipmaps; + TextureType m_textureType; + }; +} + +#endif // TSOM_ASSETCOOKER_TEXTURECOOKER_HPP diff --git a/src/AssetCooker/main.cpp b/src/AssetCooker/main.cpp new file mode 100644 index 00000000..a2ef545d --- /dev/null +++ b/src/AssetCooker/main.cpp @@ -0,0 +1,136 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include
+#include +#include +#include +#include +#include +#include + +using CookMethodBuilder = std::unique_ptr(*)(const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc); + +struct CookMethodData +{ + CookMethodBuilder builder; + + template + static constexpr CookMethodData Build() + { + return CookMethodData { + .builder = [](const std::filesystem::path& inputDir, const std::filesystem::path& outputDir, const nlohmann::json& doc) -> std::unique_ptr { return std::make_unique(inputDir, outputDir, doc); } + }; + } +}; + +constexpr auto s_cookMethods = frozen::make_unordered_map({ + { "Blocks", CookMethodData::Build() }, + { "Copy", CookMethodData::Build() }, + { "Cubemap", CookMethodData::Build() }, + { "CubemapSplitFaces", CookMethodData::Build() }, + { "Shader", CookMethodData::Build() }, + { "Texture", CookMethodData::Build() } +}); + +int CookerMain(int argc, char* argv[]) +{ + Nz::Application app(argc, argv); + auto& taskScheduler = app.AddComponent(4); + + std::filesystem::path sourcePath = Nz::Utf8Path("assets"); + std::filesystem::path destinationPath = Nz::Utf8Path("cache/CookedAssets"); + + std::ifstream assetFile(sourcePath / Nz::Utf8Path("assets.json")); + nlohmann::json assetListDoc = nlohmann::json::parse(assetFile); + + std::vector> cookers; + for (const nlohmann::json& cookDoc : assetListDoc["assets"]) + { + const std::string& method = cookDoc["method"]; + + auto cookMethodIt = s_cookMethods.find(frozen::string(method)); + if (cookMethodIt == s_cookMethods.end()) + { + spdlog::error("invalid method \"{}\"", method); + continue; + } + + const CookMethodData& cookMethodData = cookMethodIt->second; + + std::unique_ptr cooker = cookMethodData.builder(sourcePath, destinationPath, cookDoc); + + std::filesystem::file_time_type outputTime; + + bool canSkip = true; + tsom::Cooker::OutputFileList outputFiles = cooker->GetOutputFiles(); + for (const std::filesystem::path& outputFile : outputFiles) + { + std::error_code ec; + std::filesystem::file_time_type lastWriteTime = std::filesystem::last_write_time(outputFile, ec); + + if (ec) + { + if (ec != std::errc::no_such_file_or_directory) + spdlog::warn("failed to get mtime of output file {}: {}", Nz::PathToString(outputFile), ec.message()); + + canSkip = false; + break; + } + + outputTime = std::max(outputTime, lastWriteTime); + } + + if (canSkip) + { + for (const std::filesystem::path& inputFile : cooker->GetInputFiles()) + { + std::error_code ec; + std::filesystem::file_time_type lastWriteTime = std::filesystem::last_write_time(inputFile, ec); + + if (ec) + { + spdlog::error("failed to get mtime of input file {}: {}", Nz::PathToString(inputFile), ec.message()); + canSkip = false; + break; + } + + if (lastWriteTime > outputTime) + { + spdlog::debug("{} mtime is greater than output mtime"); + canSkip = false; + break; + } + } + } + + if (!canSkip) + { + spdlog::info("processing {}", Nz::PathToString(cooker->GetInputFiles().front())); + cooker->Cook(taskScheduler); + cookers.emplace_back(std::move(cooker)); + } + else + spdlog::info("skipped {} cook (up to date)", Nz::PathToString(outputFiles.front())); + } + + taskScheduler.WaitForTasks(); + return 0; +} + +TSOMMain(CookerMain) diff --git a/src/ClientLib/ClientAssetCooker.cpp b/src/ClientLib/ClientAssetCooker.cpp deleted file mode 100644 index b5bbae6f..00000000 --- a/src/ClientLib/ClientAssetCooker.cpp +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) -// This file is part of the "This Space Of Mine" project -// For conditions of distribution and use, see copyright notice in LICENSE - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tsom -{ - namespace - { - enum class TextureType - { - AmbientOcclusion, - BaseColor, - Height, - Metalness, - Normal, - Roughness, - - Max = Roughness - }; - - enum class CookedTextureType - { - BaseColor, - Normal, - MaterialData - }; - } - - Nz::Result ClientAssetCooker::Cook(ClientBlockLibrary& blockLibrary) - { - auto& fs = m_app.GetComponent(); - - const auto& blocks = blockLibrary.GetBlocks(); - - std::filesystem::path cacheDir = Nz::Utf8Path("cache"); - std::filesystem::path cookedAssetsDir = Nz::Utf8Path("CookedAssets"); - std::filesystem::path blockDir = Nz::Utf8Path("Blocks"); - - std::filesystem::path cookedAssetsPath = cacheDir / cookedAssetsDir; - std::filesystem::path cookedBlockPath = cookedAssetsPath / blockDir; - if (!std::filesystem::is_directory(cookedBlockPath)) - std::filesystem::create_directories(cookedBlockPath); - - constexpr Nz::UInt32 imageSize = 2048; - - ClientAssetCookRegistry registry; - - for (const ClientBlockLibrary::BlockData& blockData : blocks) - { - ClientAssetCookRegistry::BlockEntry blockEntry; - - Nz::EnumArray> streams; - streams[TextureType::BaseColor] = fs.GetFile(fmt::format("assets/{}.png", blockData.basePath)); - streams[TextureType::AmbientOcclusion] = fs.GetFile(fmt::format("assets/{}_ao.png", blockData.basePath)); - streams[TextureType::Height] = fs.GetFile(fmt::format("assets/{}_height.png", blockData.basePath)); - streams[TextureType::Metalness] = fs.GetFile(fmt::format("assets/{}_metallic.png", blockData.basePath)); - streams[TextureType::Normal] = fs.GetFile(fmt::format("assets/{}_normal.png", blockData.basePath)); - streams[TextureType::Roughness] = fs.GetFile(fmt::format("assets/{}_roughness.png", blockData.basePath)); - - // Handle color map - if (streams[TextureType::BaseColor]) - { - if (!streams[TextureType::BaseColor]) - return Nz::Err(fmt::format("failed to open {} base color", blockData.name)); - - std::shared_ptr baseColor = Nz::Image::LoadFromStream(*streams[TextureType::BaseColor]); - if (!baseColor) - return Nz::Err(fmt::format("failed to load {} base color", blockData.name)); - - if (baseColor->GetFormat() != Nz::PixelFormat::RGB8 && baseColor->GetFormat() != Nz::PixelFormat::RGBA8) - return Nz::Err(fmt::format("{} color map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(baseColor->GetFormat()))); - - blockEntry.baseColorFallback = Nz::Color::sRGBToLinear(baseColor->ComputeAverageColor()); - spdlog::debug("{} base color map average color: {};{};{};{}", blockData.name, blockEntry.baseColorFallback.r, blockEntry.baseColorFallback.g, blockEntry.baseColorFallback.b, blockEntry.baseColorFallback.a); - - bool hasAlpha = baseColor->HasAlpha(); // test before potential resize (faster) - - if (baseColor->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) - { - spdlog::warn("{} base color has an unexpected size", blockData.name); - baseColor->Resize(imageSize, imageSize); - } - - baseColor->GenerateMipmaps(); - - std::filesystem::path colorFilename = Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name)); - - Nz::Image compressedBaseColor; - if (hasAlpha) - { - // Compress using BC3 - // TODO: Detect 1bit alpha - spdlog::debug("{} base color map has alpha", blockData.name); - - compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC3(*baseColor); - blockEntry.baseColorTexture = { ClientAssetCookRegistry::TextureType::BC3, Nz::PathToString(blockDir / colorFilename) }; - } - else - { - // Compress using BC1 - spdlog::debug("{} base color map has no alpha", blockData.name); - - if (baseColor->GetFormat() == Nz::PixelFormat::RGBA8) - compressedBaseColor = Nz::ImageCompressor::RGBA8ToBC1(*baseColor); - else - compressedBaseColor = Nz::ImageCompressor::RGB8ToBC1(*baseColor); - - blockEntry.baseColorTexture = { ClientAssetCookRegistry::TextureType::BC1, Nz::PathToString(blockDir / colorFilename) }; - } - - std::filesystem::path targetBaseColorPath = cookedBlockPath / Nz::Utf8Path(fmt::format("{}_color.dds", blockData.name)); - if (!compressedBaseColor.SaveToFile(cookedBlockPath / colorFilename)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetBaseColorPath))); - } - - // Handle normal maps - if (streams[TextureType::Normal]) - { - std::shared_ptr normalMap = Nz::Image::LoadFromStream(*streams[TextureType::Normal]); - if (!normalMap) - return Nz::Err(fmt::format("failed to load {} normal map", blockData.name)); - - if (normalMap->GetFormat() != Nz::PixelFormat::RGB8 && normalMap->GetFormat() != Nz::PixelFormat::RGBA8) - return Nz::Err(fmt::format("{} normal map is not RGB8 nor RGBA8 (got {})", blockData.name, Nz::PixelFormatInfo::GetName(normalMap->GetFormat()))); - - if (normalMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) - return Nz::Err(fmt::format("{} normal map has an unexpected size", blockData.name)); - - Nz::Image cookedNormalMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, imageSize, imageSize); - - const Nz::UInt8* sourcePixels = normalMap->GetConstPixels(); - Nz::UInt8* cookedPixels = cookedNormalMap.GetPixels(); - Nz::UInt8 bpp = Nz::PixelFormatInfo::GetBytesPerPixel(normalMap->GetFormat()); - - for (std::size_t y = 0; y < imageSize; ++y) - { - for (std::size_t x = 0; x < imageSize; ++x) - { - if (sourcePixels[2] < 127) - spdlog::debug("{} normal map {};{} has Z value < 127: {}", blockData.name, x, y, sourcePixels[2]); - - cookedPixels[0] = sourcePixels[0]; - cookedPixels[1] = sourcePixels[1]; - - sourcePixels += bpp; - cookedPixels += 2; - } - } - cookedNormalMap.GenerateMipmaps(); - - cookedNormalMap = Nz::ImageCompressor::RG8ToBC5(cookedNormalMap); - - std::filesystem::path normalFilename = Nz::Utf8Path(fmt::format("{}_normal.dds", blockData.name)); - blockEntry.normalMapTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / normalFilename) }; - - std::filesystem::path targetPath = cookedBlockPath / normalFilename; - if (!cookedNormalMap.SaveToFile(targetPath)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); - } - - // Roughness/metalness - blockEntry.metalnessFallback = blockData.metalness; - blockEntry.roughnessFallback = blockData.roughness; - - if (streams[TextureType::Roughness]) - { - std::shared_ptr roughnessMap = Nz::Image::LoadFromStream(*streams[TextureType::Roughness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); - if (!roughnessMap) - return Nz::Err(fmt::format("failed to load {} roughness map", blockData.name)); - - if (roughnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) - return Nz::Err(fmt::format("{} roughness map has an unexpected size", blockData.name)); - - blockEntry.roughnessFallback = roughnessMap->ComputeAverageColor().r; - - if (streams[TextureType::Metalness]) - { - // BC5 roughness/metalness - std::shared_ptr metalnessMap = Nz::Image::LoadFromStream(*streams[TextureType::Metalness], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); - if (!metalnessMap) - return Nz::Err(fmt::format("failed to load {} metalness map", blockData.name)); - - if (metalnessMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) - return Nz::Err(fmt::format("{} metalness map has an unexpected size", blockData.name)); - - blockEntry.metalnessFallback = metalnessMap->ComputeAverageColor().r; - - Nz::Image cookedRoughnessMetalnessMap(Nz::ImageType::E2D, Nz::PixelFormat::RG8, imageSize, imageSize); - - const Nz::UInt8* roughnessPixels = roughnessMap->GetConstPixels(); - const Nz::UInt8* metalnessPixels = metalnessMap->GetConstPixels(); - Nz::UInt8* cookedPixels = cookedRoughnessMetalnessMap.GetPixels(); - for (std::size_t y = 0; y < imageSize; ++y) - { - for (std::size_t x = 0; x < imageSize; ++x) - { - cookedPixels[0] = *roughnessPixels++; - cookedPixels[1] = *metalnessPixels++; - - cookedPixels += 2; - } - } - cookedRoughnessMetalnessMap.GenerateMipmaps(); - - cookedRoughnessMetalnessMap = Nz::ImageCompressor::RG8ToBC5(cookedRoughnessMetalnessMap); - - std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness_metalness.dds", blockData.name)); - std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; - - blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC5, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; - - if (!cookedRoughnessMetalnessMap.SaveToFile(targetPath)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); - } - else - { - // BC4 roughness - Nz::Image cookedRoughnessMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, imageSize, imageSize); - - const Nz::UInt8* sourcePixels = roughnessMap->GetConstPixels(); - Nz::UInt8* cookedPixels = cookedRoughnessMap.GetPixels(); - for (std::size_t y = 0; y < imageSize; ++y) - { - for (std::size_t x = 0; x < imageSize; ++x) - *cookedPixels++ = *sourcePixels++; - } - cookedRoughnessMap.GenerateMipmaps(); - - cookedRoughnessMap = Nz::ImageCompressor::R8ToBC4(cookedRoughnessMap); - - std::filesystem::path roughnessMetalnessFilename = Nz::Utf8Path(fmt::format("{}_roughness.dds", blockData.name)); - std::filesystem::path targetPath = cookedBlockPath / roughnessMetalnessFilename; - - blockEntry.roughnessMetalnessTexture = { ClientAssetCookRegistry::TextureType::BC4, Nz::PathToString(blockDir / roughnessMetalnessFilename) }; - - if (!cookedRoughnessMap.SaveToFile(targetPath)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); - } - } - else if (streams[TextureType::Metalness]) - { - spdlog::warn("{} block has no roughness map but has a metalness map, this is unexpected, no roughness-metalness map will be cooked"); - } - - // Ambient Occlusion (+ Heightmap once used) - blockEntry.ambientOcclusionFallback = 1.0f; - - if (streams[TextureType::AmbientOcclusion]) - { - // BC4 - std::shared_ptr aoMap = Nz::Image::LoadFromStream(*streams[TextureType::AmbientOcclusion], Nz::ImageParams{ .loadFormat = Nz::PixelFormat::L8 }); - if (!aoMap) - return Nz::Err(fmt::format("failed to load {} ambient occlusion map", blockData.name)); - - if (aoMap->GetSize() != Nz::Vector3ui32(imageSize, imageSize, 1)) - return Nz::Err(fmt::format("{} ambient occlusion map has an unexpected size", blockData.name)); - - blockEntry.ambientOcclusionFallback = aoMap->ComputeAverageColor().r; - - Nz::Image cookedAOMap(Nz::ImageType::E2D, Nz::PixelFormat::R8, imageSize, imageSize); - - const Nz::UInt8* aoPixels = aoMap->GetConstPixels(); - Nz::UInt8* cookedPixels = cookedAOMap.GetPixels(); - for (std::size_t y = 0; y < imageSize; ++y) - { - for (std::size_t x = 0; x < imageSize; ++x) - *cookedPixels++ = *aoPixels++; - } - - cookedAOMap.GenerateMipmaps(); - - cookedAOMap = Nz::ImageCompressor::R8ToBC4(cookedAOMap); - - std::filesystem::path aoHeightFilename = Nz::Utf8Path(fmt::format("{}_ao.dds", blockData.name)); - blockEntry.ambientOcclusionHeightTexture = { ClientAssetCookRegistry::TextureType::BC4, Nz::PathToString(blockDir / aoHeightFilename) }; - - std::filesystem::path targetPath = cookedBlockPath / aoHeightFilename; - if (!cookedAOMap.SaveToFile(targetPath)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(targetPath))); - } - - registry.AddBlock(blockData.name, std::move(blockEntry)); - } - - std::filesystem::path registryPath = cookedAssetsPath / Nz::Utf8Path("registry.json"); - if (!registry.SaveToFile(registryPath)) - return Nz::Err(fmt::format("failed to save file {}", Nz::PathToString(registryPath))); - - return Nz::Ok(); - } -} diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index af027858..ced806c3 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -88,10 +88,10 @@ namespace tsom auto& fs = m_applicationBase.GetComponent(); - std::optional cookRegistry; - fs.GetFileContent("CookedAssets/registry.json", [&](const void* ptr, Nz::UInt64 size) + std::optional cookRegistry; + fs.GetFileContent("CookedAssets/Blocks/registry.json", [&](const void* ptr, Nz::UInt64 size) { - cookRegistry = ClientAssetCookRegistry::LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); + cookRegistry = CookedBlockRegistry::LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); return true; }); @@ -100,12 +100,12 @@ namespace tsom struct BlockTexture { - Nz::EnumArray textureData; + Nz::EnumArray textureData; }; Nz::UInt8* blockBufferPtr = static_cast(m_globalBlockBufferPtr) + s_blockBufferOffsets.entries; - Nz::EnumArray textureCount; + Nz::EnumArray textureCount; textureCount.fill(0); Nz::UInt32 sliceCount = 0; @@ -135,7 +135,7 @@ namespace tsom for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) { - if (textureData->type != ClientAssetCookRegistry::TextureType::None) + if (textureData->type != CookedBlockRegistry::TextureType::None) textureCount[textureData->type]++; } } @@ -144,14 +144,14 @@ namespace tsom // BC1/BC3 sRGB formats are not well supported with OpenGL, sRGB to linear conversion is done in shader // TODO: Add a shader option to use sRGB formats if supported to avoid conversion cost - constexpr Nz::EnumArray textureFormat = { + constexpr Nz::EnumArray textureFormat = { Nz::PixelFormat::BC1_RGBA_Unorm, Nz::PixelFormat::BC3_Unorm, Nz::PixelFormat::BC4_Unorm, Nz::PixelFormat::BC5_Unorm // since BC5 is used for normal maps and roughness/metalness maps we can't use Snorm }; - Nz::EnumArray> blockTextures; + Nz::EnumArray> blockTextures; for (auto&& [type, sliceCount] : textureCount.iter_kv()) { if (sliceCount > 0) @@ -173,10 +173,10 @@ namespace tsom s_blockBufferEntryOffsets.roughnessMetalnessMapIndices }; - Nz::EnumArray textureSlice; + Nz::EnumArray textureSlice; textureSlice.fill(0); - std::vector> previewTextures(m_blocks.size()); + std::vector> previewTextures(m_blocks.size()); for (std::size_t blockIndex = 0; blockIndex < remainingBlockTextures.size(); ++blockIndex) { @@ -184,10 +184,10 @@ namespace tsom for (const auto& [textureType, textureData] : blockTexture.textureData.iter_kv()) { - if (textureData->type == ClientAssetCookRegistry::TextureType::None) + if (textureData->type == CookedBlockRegistry::TextureType::None) continue; - std::string texturePath = fmt::format("CookedAssets/{}", textureData->path); + std::string texturePath = fmt::format("CookedAssets/Blocks/{}", textureData->path); std::shared_ptr stream = fs.GetFile(texturePath); if (!stream) { diff --git a/src/ClientLib/ClientChunkEntities.cpp b/src/ClientLib/ClientChunkEntities.cpp index fc0713cc..d4f4563f 100644 --- a/src/ClientLib/ClientChunkEntities.cpp +++ b/src/ClientLib/ClientChunkEntities.cpp @@ -108,10 +108,10 @@ namespace tsom m_chunkMaterial = blockMaterial->Instantiate(); m_chunkMaterial->SetBufferProperty("GlobalBlockData", blockLibrary.GetGlobalBlockBuffer()); - m_chunkMaterial->SetTextureProperty("BlockTexture1", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC1), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture2", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC3), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture3", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC4), blockSampler); - m_chunkMaterial->SetTextureProperty("BlockTexture4", blockLibrary.GetBlockTexture(ClientAssetCookRegistry::TextureType::BC5), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture1", blockLibrary.GetBlockTexture(CookedBlockRegistry::TextureType::BC1), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture2", blockLibrary.GetBlockTexture(CookedBlockRegistry::TextureType::BC3), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture3", blockLibrary.GetBlockTexture(CookedBlockRegistry::TextureType::BC4), blockSampler); + m_chunkMaterial->SetTextureProperty("BlockTexture4", blockLibrary.GetBlockTexture(CookedBlockRegistry::TextureType::BC5), blockSampler); m_chunkMaterial->SetValueProperty("ShadowPosScale", 1.f); m_chunkMaterial->SetValueProperty("AlphaTest", true); m_chunkMaterial->UpdatePassesStates({ "ShadowPass", "DistanceShadowPass" }, [](Nz::RenderStates& states) @@ -305,6 +305,8 @@ namespace tsom renderDevice.SubmitAsyncCommands(std::move(asyncTransfer)); } + else if (auto* gfxComponent = visualEntity.try_get()) + gfxComponent->Clear(); UpdateChunkDebugCollider(chunkIndices); }; diff --git a/src/ClientLib/ClientSessionHandler.cpp b/src/ClientLib/ClientSessionHandler.cpp index d9c2c344..dcf8dbaf 100644 --- a/src/ClientLib/ClientSessionHandler.cpp +++ b/src/ClientLib/ClientSessionHandler.cpp @@ -675,7 +675,7 @@ namespace tsom params.mesh.vertexRotation = Nz::Quaternionf(Nz::TurnAnglef(0.5f), Nz::Vector3f::Up()); params.mesh.vertexScale = Nz::Vector3f(1.f / 10.f); - m_playerModel->model = fs.Load("assets/Player/Idle.fbx", params); + m_playerModel->model = fs.Load("CookedAssets/Models/Player/Idle.fbx", params); if (m_playerModel->model) { assert(m_playerAnimAssets->referenceSkeleton.IsValid()); @@ -719,16 +719,16 @@ namespace tsom auto playerMaterial = std::make_shared(std::move(settings), "TSOM.PlayerPBR"); std::shared_ptr playerMat = playerMaterial->Instantiate(); - playerMat->SetTextureProperty("BaseColorMap", fs.Open("assets/Player/Textures/Soldier_AlbedoTransparency.png", { .sRGB = true })); - playerMat->SetTextureProperty("AmbientOcclusionMap", fs.Open("assets/Player/Textures/Soldier_AO.png")); - playerMat->SetTextureProperty("MetalnessSmoothnessMap", fs.Open("assets/Player/Textures/Soldier_Normal.png")); - playerMat->SetTextureProperty("NormalMap", fs.Open("assets/Player/Textures/Soldier_Normal.png")); + playerMat->SetTextureProperty("BaseColorMap", fs.Open("CookedAssets/Models/Player/Textures/Soldier_AlbedoTransparency.dds", { .sRGB = true })); + playerMat->SetTextureProperty("AmbientOcclusionMap", fs.Open("CookedAssets/Models/Player/Textures/Soldier_AO.dds")); + playerMat->SetTextureProperty("MetalnessSmoothnessMap", fs.Open("CookedAssets/Models/Player/Textures/Soldier_MetallicSmoothness.dds")); + playerMat->SetTextureProperty("NormalMap", fs.Open("CookedAssets/Models/Player/Textures/Soldier_Normal.dds")); m_playerModel->model->SetMaterial(0, std::move(playerMat)); - m_playerAnimAssets->idleAnimation = fs.Load("assets/Player/Idle.fbx", animParams); - m_playerAnimAssets->runningAnimation = fs.Load("assets/Player/Running.fbx", animParams); - m_playerAnimAssets->walkingAnimation = fs.Load("assets/Player/Walking.fbx", animParams); + m_playerAnimAssets->idleAnimation = fs.Load("CookedAssets/Models/Player/Idle.fbx", animParams); + m_playerAnimAssets->runningAnimation = fs.Load("CookedAssets/Models/Player/Running.fbx", animParams); + m_playerAnimAssets->walkingAnimation = fs.Load("CookedAssets/Models/Player/Walking.fbx", animParams); } else { diff --git a/src/CommonLib/BlockLibrary.cpp b/src/CommonLib/BlockLibrary.cpp index 3e608eee..983867d0 100644 --- a/src/CommonLib/BlockLibrary.cpp +++ b/src/CommonLib/BlockLibrary.cpp @@ -47,7 +47,7 @@ namespace nlohmann isSmooth, isTransparent, density, \ metalness, permeability, roughness \ ); - + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(tsom::BlockLibrary::LayerInfo, \ physicsLayer, isBlended, isFluid, isPhysicsTrigger, renderLayer ); diff --git a/src/ClientLib/ClientAssetCookRegistry.cpp b/src/CommonLib/CookedBlockRegistry.cpp similarity index 64% rename from src/ClientLib/ClientAssetCookRegistry.cpp rename to src/CommonLib/CookedBlockRegistry.cpp index 217ae686..30bcded7 100644 --- a/src/ClientLib/ClientAssetCookRegistry.cpp +++ b/src/CommonLib/CookedBlockRegistry.cpp @@ -2,7 +2,7 @@ // This file is part of the "This Space Of Mine" project // For conditions of distribution and use, see copyright notice in LICENSE -#include +#include #include #include @@ -17,12 +17,12 @@ namespace nlohmann color.a = j.value("a", 1.0f); } - NLOHMANN_JSON_SERIALIZE_ENUM(tsom::ClientAssetCookRegistry::TextureType, { - {tsom::ClientAssetCookRegistry::TextureType::None, "none"}, - {tsom::ClientAssetCookRegistry::TextureType::BC1, "bc1"}, - {tsom::ClientAssetCookRegistry::TextureType::BC3, "bc3"}, - {tsom::ClientAssetCookRegistry::TextureType::BC4, "bc4"}, - {tsom::ClientAssetCookRegistry::TextureType::BC5, "bc5"}, + NLOHMANN_JSON_SERIALIZE_ENUM(tsom::CookedBlockRegistry::TextureType, { + {tsom::CookedBlockRegistry::TextureType::None, "none"}, + {tsom::CookedBlockRegistry::TextureType::BC1, "bc1"}, + {tsom::CookedBlockRegistry::TextureType::BC3, "bc3"}, + {tsom::CookedBlockRegistry::TextureType::BC4, "bc4"}, + {tsom::CookedBlockRegistry::TextureType::BC5, "bc5"}, }) template @@ -38,9 +38,9 @@ namespace nlohmann j["a"] = color.a; } - NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::ClientAssetCookRegistry::Texture, path, type) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::CookedBlockRegistry::Texture, path, type) - NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::ClientAssetCookRegistry::BlockEntry, + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::CookedBlockRegistry::BlockEntry, ambientOcclusionFallback, ambientOcclusionHeightTexture, baseColorFallback, baseColorTexture, metalnessFallback, normalMapTexture, @@ -50,18 +50,18 @@ namespace nlohmann namespace tsom { - void ClientAssetCookRegistry::AddBlock(std::string blockName, BlockEntry blockEntry) + void CookedBlockRegistry::AddBlock(std::string blockName, BlockEntry blockEntry) { NazaraAssertMsg(!m_blockEntries.contains(blockName), "block %s is already present", blockName.c_str()); m_blockEntries.emplace(std::move(blockName), std::move(blockEntry)); } - auto ClientAssetCookRegistry::GetBlock(std::string_view blockName) const -> const BlockEntry& + auto CookedBlockRegistry::GetBlock(std::string_view blockName) const -> const BlockEntry& { return Nz::Retrieve(m_blockEntries, blockName); } - bool ClientAssetCookRegistry::SaveToFile(const std::filesystem::path& path) const + bool CookedBlockRegistry::SaveToFile(const std::filesystem::path& path) const { nlohmann::ordered_json blockEntries; for (const auto& [blockName, blockEntry] : m_blockEntries) @@ -75,18 +75,18 @@ namespace tsom return Nz::File::WriteWhole(path, content.data(), content.size()); } - std::optional ClientAssetCookRegistry::LoadFromString(std::string_view content) + std::optional CookedBlockRegistry::LoadFromString(std::string_view content) { nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); - ClientAssetCookRegistry cookRegistry; + CookedBlockRegistry cookRegistry; for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) cookRegistry.AddBlock(blockName, blockEntryDoc); return std::move(cookRegistry); } - std::optional ClientAssetCookRegistry::LoadFromFile(const std::filesystem::path& path) + std::optional CookedBlockRegistry::LoadFromFile(const std::filesystem::path& path) { std::optional> contentOpt = Nz::File::ReadWhole(path); if (!contentOpt) diff --git a/src/CommonLib/Planet.cpp b/src/CommonLib/Planet.cpp index 45cab520..ed04b751 100644 --- a/src/CommonLib/Planet.cpp +++ b/src/CommonLib/Planet.cpp @@ -387,7 +387,7 @@ namespace tsom end local blockPresence = perlin:normalizedOctave3D_01(blockPosScaled.x * caveScale, blockPosScaled.y * caveScale, blockPosScaled.z * caveScale, 4, 0.1) - + if distToCenter <= -32.0 then if blockPresence >= 0.3 and blockPresence <= 0.7 then if distToCenter <= -5 then @@ -408,10 +408,10 @@ namespace tsom else mountainous = 1 end - + local heightVariation1 = 10 * perlin:normalizedOctave3D_01(blockPosNorm.x * terrainVariation1Scale, blockPosNorm.y * terrainVariation1Scale, blockPosNorm.z * terrainVariation1Scale, 4, 0.1) local heightVariation2 = 40 * mountainous * perlin:normalizedOctave3D_01((blockPosNorm.x * terrainVariation2Scale)+20, blockPosNorm.y * terrainVariation2Scale, blockPosNorm.z * terrainVariation2Scale, 4, 0.1) - + local baseSpikeHeight = perlin:normalizedOctave3D_01((blockPosNorm.x * spikeScale)+30, blockPosNorm.y * spikeScale, blockPosNorm.z * spikeScale, 4, 0.1) local spikeHeight if baseSpikeHeight < 0.7 then @@ -422,9 +422,9 @@ namespace tsom spikeHeight = 1 end spikeHeight = (1-mountainous) * spikeHeight * 20 - + local height = heightVariation1 + heightVariation2 + spikeHeight - + if distToCenter <= height then if distToCenter >= height - spikeHeight then table.insert(content, stoneMossyBlock) @@ -441,7 +441,7 @@ namespace tsom table.insert(content, emptyBlock) end end - + ::continue:: end end diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index 72d7e038..aeb4be2a 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -3,7 +3,6 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include -#include #include #include #include @@ -308,21 +307,13 @@ namespace tsom graphics->GetShaderModuleResolver()->RegisterDirectory(Nz::Utf8Path("assets/shaders"), true); m_blockLibrary.emplace(app); - + filesystem.GetFileContent("assets/blocks.json", [&](const void* ptr, Nz::UInt64 size) { m_blockLibrary->LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); return true; }); - /*ClientAssetCooker assetCooker(app); - if (auto result = assetCooker.Cook(*m_blockLibrary); !result) - { - spdlog::critical("failed to cook assets: {}!", result.GetError()); - app.Quit(); - return false; - }*/ - m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); return true; diff --git a/src/Game/States/BackgroundState.cpp b/src/Game/States/BackgroundState.cpp index 5129dbf8..9b394b1c 100644 --- a/src/Game/States/BackgroundState.cpp +++ b/src/Game/States/BackgroundState.cpp @@ -64,28 +64,8 @@ namespace tsom std::shared_ptr skyboxMaterial = std::make_shared(std::move(skyboxSettings), "SkyboxMaterial"); // Load skybox - Nz::TextureInfo textureInfo = { - .pixelFormat = Nz::PixelFormat::RGBA8, - .type = Nz::ImageType::Cubemap, - .layerCount = 6, - .height = 2048, - .width = 2048, - }; - - std::shared_ptr skyboxTexture = Nz::TextureAsset::CreateWithBuilder(textureInfo, [stateData](Nz::RenderDevice&, const Nz::TextureAssetParams& /*params*/) - { - auto& filesystem = stateData->app->GetComponent(); - - Nz::Image skyboxImage(Nz::ImageType::Cubemap, Nz::PixelFormat::RGBA8_SRGB, 2048, 2048); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::PositiveX, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_right1.png")); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::NegativeX, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_left2.png")); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::PositiveY, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_top3.png")); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::NegativeY, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_bottom4.png")); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::PositiveZ, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_front5.png")); - skyboxImage.LoadFaceFromImage(Nz::CubemapFace::NegativeZ, *filesystem.Load("assets/PurpleNebulaSkybox/purple_nebula_skybox_back6.png")); - - return skyboxImage; - }); + std::shared_ptr skybox = filesystem.Load("CookedAssets/Textures/Skybox/MenuSkybox.dds"); + std::shared_ptr skyboxTexture = Nz::TextureAsset::CreateFromImage(std::move(*skybox)); // Instantiate the material to use it, and configure it (texture + cull front faces as the render is from the inside) std::shared_ptr skyboxMat = skyboxMaterial->Instantiate(); diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 9b13aca8..5898e632 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -83,9 +83,9 @@ namespace tsom auto& cameraNode = m_cameraEntity.emplace(); #ifdef TSOM_DEV_TOOLS - auto passList = filesystem.Load(stateData.imgui ? "assets/3d_dev.passlist" : "assets/3d.passlist"); + auto passList = filesystem.Load(stateData.imgui ? "CookedAssets/Passes/3d_dev.passlist" : "CookedAssets/Passes/3d.passlist"); #else - auto passList = filesystem.Load("assets/3d.passlist"); + auto passList = filesystem.Load("CookedAssets/Passes/3d.passlist"); #endif auto& cameraComponent = m_cameraEntity.emplace(stateData.renderTarget, std::move(passList)); @@ -156,7 +156,7 @@ namespace tsom // Instantiate the material to use it, and configure it (texture + cull front faces as the render is from the inside) std::shared_ptr skyboxMat = skyboxMaterial->Instantiate(); - skyboxMat->SetTextureProperty("BaseColorMap", filesystem.Open("assets/skybox-space.png", { .sRGB = true }, Nz::CubemapParams{})); + skyboxMat->SetTextureProperty("BaseColorMap", filesystem.Open("CookedAssets/Textures/Skybox/GameSkybox.dds", { .sRGB = true })); skyboxMat->UpdatePassesStates([](Nz::RenderStates& states) { states.faceCulling = Nz::FaceCulling::Front; diff --git a/src/Game/States/MenuState.cpp b/src/Game/States/MenuState.cpp index a4fc9dc3..711ed15b 100644 --- a/src/Game/States/MenuState.cpp +++ b/src/Game/States/MenuState.cpp @@ -23,7 +23,7 @@ namespace tsom auto& fs = GetStateData().app->GetComponent(); std::shared_ptr logoMat = Nz::MaterialInstance::Instantiate(Nz::MaterialType::Basic); - logoMat->SetTextureProperty("BaseColorMap", fs.Open("assets/logo.png", { .sRGB = true })); + logoMat->SetTextureProperty("BaseColorMap", fs.Open("CookedAssets/Textures/Logo.dds", { .sRGB = true })); m_logo = CreateWidget(logoMat); m_logo->Resize({ 512, 512 }); @@ -32,7 +32,7 @@ namespace tsom m_title = CreateWidget(); auto& filesystem = GetStateData().app->GetComponent(); - std::shared_ptr titleFont = filesystem.Open("assets/fonts/axaxax bd.otf"); + std::shared_ptr titleFont = filesystem.Open("CookedAssets/Fonts/axaxax bd.otf"); m_title->UpdateDrawer([&](Nz::SimpleTextDrawer& textDrawer) { diff --git a/src/Game/States/PlanetEditorState.cpp b/src/Game/States/PlanetEditorState.cpp index 2478cfd5..579d9da8 100644 --- a/src/Game/States/PlanetEditorState.cpp +++ b/src/Game/States/PlanetEditorState.cpp @@ -52,9 +52,9 @@ namespace tsom auto& cameraNode = m_cameraEntity.emplace(Nz::Vector3f(0.f, 20.f, -75.f), m_cameraRotation); #ifdef TSOM_DEV_TOOLS - auto passList = filesystem.Load(stateData.imgui ? "assets/3d_dev.passlist" : "assets/3d.passlist"); + auto passList = filesystem.Load(stateData.imgui ? "CookedAssets/Passes/3d_dev.passlist" : "CookedAssets/Passes/3d.passlist"); #else - auto passList = filesystem.Load("assets/3d.passlist"); + auto passList = filesystem.Load("CookedAssets/Passes/3d.passlist"); #endif auto& cameraComponent = m_cameraEntity.emplace(stateData.renderTarget, std::move(passList)); @@ -99,7 +99,7 @@ namespace tsom // Instantiate the material to use it, and configure it (texture + cull front faces as the render is from the inside) std::shared_ptr skyboxMat = skyboxMaterial->Instantiate(); - skyboxMat->SetTextureProperty("BaseColorMap", filesystem.Open("assets/skybox-space.png", { .sRGB = true }, Nz::CubemapParams{})); + skyboxMat->SetTextureProperty("BaseColorMap", filesystem.Open("CookedAssets/Textures/Skybox/GameSkybox.dds", { .sRGB = true })); skyboxMat->UpdatePassesStates([](Nz::RenderStates& states) { states.faceCulling = Nz::FaceCulling::Front; diff --git a/xmake.lua b/xmake.lua index b9f70ec2..38d28733 100644 --- a/xmake.lua +++ b/xmake.lua @@ -37,7 +37,8 @@ add_requires( "perlinnoise", "sol2[includes_lua=n]", "spdlog[fmt_external=y,header_only=n]", - "sqlitecpp[sqlite3_external]" + "sqlitecpp[sqlite3_external]", + "tiny-process-library" ) if has_config("serveronly") then @@ -56,6 +57,7 @@ if has_config("serveronly") then plugin_imgui = false } }) + add_requires("nzsl") end if is_plat("macosx") then @@ -276,6 +278,16 @@ target("TSOMServer", function () add_rpathdirs("@executable_path") end) +target("TSOMAssetCooker", function () + set_group("Executable") + set_basename("ThisCookerOfMine") + add_deps("CommonLib", "Main") + add_packages("frozen", "nzsl", "tiny-process-library") + + add_headerfiles("src/AssetCooker/**.hpp", "src/AssetCooker/**.inl") + add_files("src/AssetCooker/**.cpp") +end) + if not has_config("serveronly") then target("ClientLib", function () set_group("Common") diff --git a/xmake/actions/checkfiles.lua b/xmake/actions/checkfiles.lua index 8955a42b..3c0b9e00 100644 --- a/xmake/actions/checkfiles.lua +++ b/xmake/actions/checkfiles.lua @@ -2,6 +2,7 @@ local modules = { ClientLib = "library", CommonLib = "library", ServerLib = "library", + AssetCooker = "standalone", Game = "standalone", Server = "standalone" } From 9c5ec5399bedb3ca3bdc6055c78e927cadf6f022 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 21 Apr 2026 00:37:09 +0200 Subject: [PATCH 18/23] ScriptingUtils: Expose EulerAngles --- .../CommonLib/Scripting/ScriptingUtils.inl | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/include/CommonLib/Scripting/ScriptingUtils.inl b/include/CommonLib/Scripting/ScriptingUtils.inl index 1be7f156..8eae384c 100644 --- a/include/CommonLib/Scripting/ScriptingUtils.inl +++ b/include/CommonLib/Scripting/ScriptingUtils.inl @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -193,6 +194,9 @@ namespace sol template struct lua_size> : std::integral_constant {}; + template + struct lua_size> : std::integral_constant {}; + template struct lua_size> : std::integral_constant {}; @@ -208,6 +212,9 @@ namespace sol template struct lua_type_of> : std::integral_constant {}; + template + struct lua_type_of> : std::integral_constant {}; + template struct lua_type_of> : std::integral_constant {}; @@ -221,7 +228,7 @@ namespace sol struct lua_type_of> : std::integral_constant {}; template - inline Nz::Box sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + Nz::Box sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) { int absoluteIndex = lua_absindex(L, index); @@ -239,7 +246,22 @@ namespace sol } template - inline Nz::Quaternion sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + Nz::EulerAngles sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + { + int absoluteIndex = lua_absindex(L, index); + + sol::table angles = sol::stack::get(L, absoluteIndex); + T pitch = angles["pitch"]; + T yaw = angles["yaw"]; + T roll = angles["roll"]; + + tracking.use(1); + + return Nz::EulerAngles(Nz::DegreeAngle(pitch), Nz::DegreeAngle(yaw), Nz::DegreeAngle(roll)); + } + + template + Nz::Quaternion sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) { int absoluteIndex = lua_absindex(L, index); @@ -255,7 +277,7 @@ namespace sol } template - inline Nz::Rect sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) + Nz::Rect sol_lua_get(sol::types>, lua_State* L, int index, sol::stack::record& tracking) { int absoluteIndex = lua_absindex(L, index); @@ -316,6 +338,19 @@ namespace sol return 1; } + template + int sol_lua_push(sol::types>, lua_State* L, const Nz::EulerAngles& angles) + { + lua_createtable(L, 0, 3); + luaL_setmetatable(L, "eulerangles"); + sol::stack_table anglesTable(L); + anglesTable["pitch"] = angles.pitch.ToDegrees(); + anglesTable["yaw"] = angles.yaw.ToDegrees(); + anglesTable["roll"] = angles.roll.ToDegrees(); + + return 1; + } + template int sol_lua_push(sol::types>, lua_State* L, const Nz::Quaternion& quat) { From 6faf16ef7232e10edba8cba58d445d4026e0e098 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 21 Apr 2026 00:51:07 +0200 Subject: [PATCH 19/23] Fix redundant move (squash 3f4a7dd64) --- src/CommonLib/CookedBlockRegistry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonLib/CookedBlockRegistry.cpp b/src/CommonLib/CookedBlockRegistry.cpp index 30bcded7..a0c3eafc 100644 --- a/src/CommonLib/CookedBlockRegistry.cpp +++ b/src/CommonLib/CookedBlockRegistry.cpp @@ -83,7 +83,7 @@ namespace tsom for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) cookRegistry.AddBlock(blockName, blockEntryDoc); - return std::move(cookRegistry); + return cookRegistry; } std::optional CookedBlockRegistry::LoadFromFile(const std::filesystem::path& path) From 7e4dc53ca07d6470b9d22f9dc1e83186ad383371 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 23 Apr 2026 00:39:11 +0200 Subject: [PATCH 20/23] AssetCooker: Fix ShaderCooker --- src/AssetCooker/ShaderCooker.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/AssetCooker/ShaderCooker.cpp b/src/AssetCooker/ShaderCooker.cpp index 9d285bfd..48303bf4 100644 --- a/src/AssetCooker/ShaderCooker.cpp +++ b/src/AssetCooker/ShaderCooker.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,18 @@ namespace tsom void ShaderCooker::Cook(Nz::TaskScheduler& taskScheduler) { + if (m_inputFile.stem() != m_outputFile.stem()) + { + spdlog::error("currently the input file and the output file must have the same name"); + return; + } + + if (m_outputFile.extension() != Nz::Utf8Path(".nzslb")) + { + spdlog::error("only .nzslb output extension is supported"); + return; + } + taskScheduler.AddTask([this] { std::filesystem::path outputDir = m_outputFile.parent_path(); @@ -31,12 +44,23 @@ namespace tsom return; } - ec = {}; - std::filesystem::copy_file(m_inputFile, m_outputFile, std::filesystem::copy_options::overwrite_existing, ec); + std::string output; + auto ReadStdout = [&](const char* str, std::size_t size) + { + output.append(str, size); + }; + + std::string errOutput; + auto ReadStderr = [&](const char* str, std::size_t size) + { + errOutput.append(str, size); + }; - if (ec) + TinyProcessLib::Process process({ "nzslc", "--compile", "--partial", "--output", Nz::PathToString(outputDir), Nz::PathToString(m_inputFile) }, {}, ReadStdout, ReadStderr); + int exitCode = process.get_exit_status(); + if (exitCode != 0) { - spdlog::error("failed to copy file from {} to {}: {}", Nz::PathToString(m_inputFile), Nz::PathToString(m_outputFile), ec.message()); + spdlog::error("failed to compile {}: {}", Nz::PathToString(m_inputFile), errOutput); return; } }); From 8062e207a07ea1c7981c960e80467dbbbca4201c Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 23 Apr 2026 00:40:48 +0200 Subject: [PATCH 21/23] Use only CookedAssets --- .gitignore | 1 + assets/assets.json | 25 +++++++++++++++++++++++++ scripts/assets/computer.lua | 4 ++-- scripts/assets/tree.lua | 2 +- src/AssetCooker/main.cpp | 2 +- src/Game/GameAppComponent.cpp | 9 ++++----- src/Game/States/BackgroundState.cpp | 2 +- src/Game/States/GameState.cpp | 2 +- src/Server/main.cpp | 2 +- src/ServerLib/ServerInstance.cpp | 2 +- 10 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 6254db8d..0f7f3316 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ assets/* !assets/*.json !assets/shaders cache/ +CookedAssets/ saves/ gameconfig.lua serverconfig.lua diff --git a/assets/assets.json b/assets/assets.json index 9cc136ac..d6adbdeb 100644 --- a/assets/assets.json +++ b/assets/assets.json @@ -29,6 +29,11 @@ "output": "Textures/Dev/grey.dds", "method": "Texture" }, + { + "input": "crosshair.png", + "output": "Textures/crosshair.dds", + "method": "Texture" + }, { "input": "fonts/axaxax bd.otf", "output": "Fonts/axaxax bd.otf", @@ -74,6 +79,21 @@ "method": "Texture", "type": "Normal" }, + { + "input": "ship/computer/scifi_computer_1_3.obj", + "output": "Models/ShipComputer/scifi_computer_1_3.obj", + "method": "Copy" + }, + { + "input": "ship/computer/digital_displays.png", + "output": "Models/ShipComputer/digital_displays.dds", + "method": "Texture" + }, + { + "input": "tree/shapespark-low-poly-plants-kit.gltf", + "output": "Models/Tree/shapespark-low-poly-plants-kit.gltf", + "method": "Copy" + }, { "input": "Shaders/BlockPBR.nzsl", "output": "Shaders/BlockPBR.nzslb", @@ -124,6 +144,11 @@ "input": "skybox.passlist", "output": "Passes/skybox.passlist", "method": "Copy" + }, + { + "input": "blocks.json", + "output": "BlockData.json", + "method": "Copy" } ] } \ No newline at end of file diff --git a/scripts/assets/computer.lua b/scripts/assets/computer.lua index aa24720f..f38e1acf 100644 --- a/scripts/assets/computer.lua +++ b/scripts/assets/computer.lua @@ -8,10 +8,10 @@ local params = { loadMaterials = false } -local computer = Model.Load("assets/ship/computer/scifi_computer_1_3.obj", params) +local computer = Model.Load("CookedAssets/Models/ShipComputer/scifi_computer_1_3.obj", params) local screenMat = MaterialInstance.Instantiate(MaterialType.Basic, MaterialInstancePresetFlags.AlphaBlended) -screenMat:SetTextureProperty("BaseColorMap", Texture.Load("assets/ship/computer/digital_displays.png")) +screenMat:SetTextureProperty("BaseColorMap", Texture.Load("CookedAssets/Models/ShipComputer/digital_displays.dds")) screenMat:UpdatePassesStates(function (renderStates) renderStates.faceCulling = FaceCulling.None diff --git a/scripts/assets/tree.lua b/scripts/assets/tree.lua index df4ffff0..4d5a451b 100644 --- a/scripts/assets/tree.lua +++ b/scripts/assets/tree.lua @@ -1,4 +1,4 @@ -local allTreeMesh = Mesh.Load("assets/tree/shapespark-low-poly-plants-kit.gltf") +local allTreeMesh = Mesh.Load("CookedAssets/Models/Tree/shapespark-low-poly-plants-kit.gltf") local trees = { ["tree-01-1"] = { diff --git a/src/AssetCooker/main.cpp b/src/AssetCooker/main.cpp index a2ef545d..e63882a4 100644 --- a/src/AssetCooker/main.cpp +++ b/src/AssetCooker/main.cpp @@ -54,7 +54,7 @@ int CookerMain(int argc, char* argv[]) auto& taskScheduler = app.AddComponent(4); std::filesystem::path sourcePath = Nz::Utf8Path("assets"); - std::filesystem::path destinationPath = Nz::Utf8Path("cache/CookedAssets"); + std::filesystem::path destinationPath = Nz::Utf8Path("CookedAssets"); std::ifstream assetFile(sourcePath / Nz::Utf8Path("assets.json")); nlohmann::json assetListDoc = nlohmann::json::parse(assetFile); diff --git a/src/Game/GameAppComponent.cpp b/src/Game/GameAppComponent.cpp index aeb4be2a..a3978a13 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -299,16 +299,15 @@ namespace tsom } auto& filesystem = app.GetComponent(); - filesystem.Mount("assets", assetPath); - filesystem.Mount("CookedAssets", Nz::Utf8Path("cache/CookedAssets")); + filesystem.Mount("CookedAssets", Nz::Utf8Path("CookedAssets")); filesystem.Mount("scripts", scriptPath); Nz::Graphics* graphics = Nz::Graphics::Instance(); - graphics->GetShaderModuleResolver()->RegisterDirectory(Nz::Utf8Path("assets/shaders"), true); + graphics->GetShaderModuleResolver()->RegisterDirectory(Nz::Utf8Path("CookedAssets/Shaders"), true); m_blockLibrary.emplace(app); - filesystem.GetFileContent("assets/blocks.json", [&](const void* ptr, Nz::UInt64 size) + filesystem.GetFileContent("CookedAssets/BlockData.json", [&](const void* ptr, Nz::UInt64 size) { m_blockLibrary->LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); return true; @@ -325,7 +324,7 @@ namespace tsom camera2D.emplace(); auto& filesystem = GetApp().GetComponent(); - auto passList = filesystem.Load("assets/2d.passlist"); + auto passList = filesystem.Load("CookedAssets/Passes/2d.passlist"); auto& cameraComponent = camera2D.emplace(std::move(renderTarget), std::move(passList), Nz::ProjectionType::Orthographic); cameraComponent.UpdateClearColor(Nz::Color(0.f, 0.f, 0.f, 0.f)); diff --git a/src/Game/States/BackgroundState.cpp b/src/Game/States/BackgroundState.cpp index 9b394b1c..0ec8e208 100644 --- a/src/Game/States/BackgroundState.cpp +++ b/src/Game/States/BackgroundState.cpp @@ -34,7 +34,7 @@ namespace tsom auto& cameraNode = m_camera.emplace(); cameraNode.SetRotation(Nz::EulerAnglesf(dis(rd), dis(rd), dis(rd))); - auto skyboxPasses = filesystem.Load("assets/skybox.passlist"); + auto skyboxPasses = filesystem.Load("CookedAssets/Passes/skybox.passlist"); auto& cameraComponent = m_camera.emplace(GetStateData().renderTarget, std::move(skyboxPasses), Nz::ProjectionType::Perspective); cameraComponent.UpdateClearDepth(0.f); diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 5898e632..25e50706 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -102,7 +102,7 @@ namespace tsom m_crosshairEntity = CreateEntity(); { - auto sprite = std::make_shared(filesystem.Load("assets/crosshair.png")); + auto sprite = std::make_shared(filesystem.Load("CookedAssets/Textures/crosshair.dds")); sprite->SetOrigin({ 0.5f, 0.5f }); sprite->SetSize(sprite->GetSize() * 0.15f); diff --git a/src/Server/main.cpp b/src/Server/main.cpp index d4c564ce..b3102602 100644 --- a/src/Server/main.cpp +++ b/src/Server/main.cpp @@ -38,7 +38,7 @@ int ServerMain(int argc, char* argv[]) auto& serverInstanceAppComponent = app.AddComponent(); auto& filesystem = app.AddComponent(); - for (const char* directory : { "assets", "database", "scripts" }) + for (const char* directory : { "CookedAssets", "database", "scripts" }) { std::filesystem::path dirPath = Nz::Utf8Path(directory); if (!std::filesystem::is_directory(dirPath)) diff --git a/src/ServerLib/ServerInstance.cpp b/src/ServerLib/ServerInstance.cpp index b77ab587..4ce7e3ea 100644 --- a/src/ServerLib/ServerInstance.cpp +++ b/src/ServerLib/ServerInstance.cpp @@ -36,7 +36,7 @@ namespace tsom { auto& fs = m_application.GetComponent(); - fs.GetFileContent("assets/blocks.json", [&](const void* ptr, Nz::UInt64 size) + fs.GetFileContent("CookedAssets/BlockData.json", [&](const void* ptr, Nz::UInt64 size) { m_blockLibrary.LoadFromString(std::string_view(reinterpret_cast(ptr), Nz::SafeCast(size))); return true; From 715cdbd60cb1b84b93485e9ce04e6cc514f61d0e Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 23 Apr 2026 00:43:38 +0200 Subject: [PATCH 22/23] Rework planet scattering effect --- assets/shaders/PlanetAtmosphere.nzsl | 274 ++++++++++----------- include/CommonLib/AtmosphereScattering.hpp | 4 +- 2 files changed, 127 insertions(+), 151 deletions(-) diff --git a/assets/shaders/PlanetAtmosphere.nzsl b/assets/shaders/PlanetAtmosphere.nzsl index 03b06ce6..2a554f3c 100644 --- a/assets/shaders/PlanetAtmosphere.nzsl +++ b/assets/shaders/PlanetAtmosphere.nzsl @@ -121,24 +121,34 @@ fn main(input: VertOut) -> FragOut const HEIGHT_SCALE = 64.0; const HEIGHT_SCALE_INV = 1.0 / HEIGHT_SCALE; -// From https://www.shadertoy.com/view/wlBXWK -/* -Next we'll define the main scattering function. -This traces a ray from start to end and takes a certain amount of samples along this ray, in order to calculate the color. -For every sample, we'll also trace a ray in the direction of the light, -because the color that reaches the sample also changes due to scattering -*/ +const waveLengths = vec3[f32](700.0, 530.0, 440.0); +const scatteringCoefficients_1 = (400.0).xxx / waveLengths; +const scatteringCoefficients = scatteringCoefficients_1 * scatteringCoefficients_1 * scatteringCoefficients_1 * scatteringCoefficients_1; + +fn RoundBoxUp(pos: vec3[f32], cornerRadius: f32) -> vec3[f32] +{ + let distToCenter = max(max(abs(pos.x), abs(pos.y)), abs(pos.z)); + + let innerReductionSize = max(distToCenter - max(cornerRadius, 1.0), 0.0); + let innerBoxMin = -innerReductionSize.xxx; + let innerBoxMax = innerReductionSize.xxx; + + let innerPos = clamp(pos, innerBoxMin, innerBoxMax); + return normalize(pos - innerPos); +} + +// Based on https://www.youtube.com/watch?v=DxfEbulyFcY and https://www.shadertoy.com/view/wlBXWK fn calculate_scattering( - start: vec3[f32], // the start of the ray (the camera position) - dir: vec3[f32], // the direction of the ray (the camera vector) - max_dist: f32, // the maximum distance the ray can travel (because something is in the way, like an object) - scene_color: vec3[f32], // the color of the scene - light_dir: vec3[f32], // the direction of the light + viewerPos: vec3[f32], // the start of the ray (the camera position) + viewerDir: vec3[f32], // the direction of the ray (the camera vector) + maxDist: f32, // the maximum distance the ray can travel (because something is in the way, like an object) + sceneColor: vec3[f32], // the color of the scene + sunDir: vec3[f32], // the direction of the light light_intensity: vec3[f32], // how bright the light is, affects the brightness of the atmosphere - planet_position: vec3[f32], // the position of the planet - planet_dims: vec3[f32], // the planet dimensions - planet_corner_radius: f32, // the planet corner radius - atmosphereMaxHeight: f32, // the atmosphere max height (starting from planet dimensions) + planetOrigin: vec3[f32], // the position of the planet + planetDims: vec3[f32], // the planet dimensions + planetCornerRadius: f32, // the planet corner radius + atmosphereRadius: f32, // the atmosphere max height (starting from planet dimensions) beta_ray: vec3[f32], // the amount rayleigh scattering scatters the colors (for earth: causes the blue atmosphere) beta_mie: vec3[f32], // the amount mie scattering scatters colors beta_absorption: vec3[f32], // how much air is absorbed @@ -147,146 +157,88 @@ fn calculate_scattering( height_ray: f32, // how high do you have to go before there is no rayleigh scattering? height_mie: f32, // the same, but for mie height_absorption: f32, // the height at which the most absorption happens - absorption_falloff: f32, // how fast the absorption falls off from the absorption height + densityFallOff: f32, // how fast the absorption falls off from the absorption height steps_i: i32, // the amount of steps along the 'primary' ray, more looks better but slower steps_l: i32 // the amount of steps along the light ray, more looks better but slower ) -> vec3[f32] { - // add an offset to the camera position, so that the atmosphere is in the correct position - start -= planet_position; - - // calculate the start and end position of the ray, as a distance along the ray - // we do this with an AABB intersect - let ray_length = aabbIntersect(start, dir, planet_dims + atmosphereMaxHeight.xxx); - - // stop early if there is no intersect - if (ray_length.x > ray_length.y || ray_length.y < 0.0) - return scene_color; - - // prevent the mie glow from appearing if there's an object in front of the camera - let allow_mie = max_dist > ray_length.y; - // make sure the ray is no longer than allowed - ray_length.y = min(ray_length.y, max_dist); - ray_length.x = max(ray_length.x, 0.0); - // get the step size of the ray - let step_size_i = (ray_length.y - ray_length.x) / f32(steps_i); - - // next, set how far we are along the ray, so we can calculate the position of the sample - // if the camera is outside the atmosphere, the ray should start at the edge of the atmosphere - // if it's inside, it should start at the position of the camera - // the min statement makes sure of that - let ray_pos_i = ray_length.x + step_size_i * 0.5; - - // these are the values we use to gather all the scattered light - let total_ray = (0.0).xxx; // for rayleigh - let total_mie = (0.0).xxx; // for mie - - // initialize the optical depth. This is used to calculate how much air was in the ray - let opt_i = (0.0).xxx; - - // also init the scale height, avoids some vec2's later on - let scale_height = vec2[f32](height_ray, height_mie); - - // Calculate the Rayleigh and Mie phases. - // This is the color that will be scattered for this ray - // mu, mumu and gg are used quite a lot in the calculation, so to speed it up, precalculate them - let mu = dot(dir, light_dir); + let hitInfo = raySphere(planetOrigin, atmosphereRadius, viewerPos, viewerDir); + if (hitInfo.x < 0.0) + return sceneColor; + + let distanceToAtmosphere = hitInfo.x; + let distanceThroughAtmosphere = min(hitInfo.y, maxDist - distanceToAtmosphere); + + if (distanceThroughAtmosphere <= 0.0) + return sceneColor; + + let scatterPoint = viewerPos + viewerDir * distanceToAtmosphere; + + let stepSize = distanceThroughAtmosphere / f32(steps_i - 1); + + let scatteredLight = (0.0).xxx; + let viewRayOpticalDepth = 0.0; + + let allowMie = maxDist > hitInfo.y; + let mu = dot(viewerDir, sunDir); let mumu = mu * mu; let gg = g * g; - let phase_ray = 3.0 / (50.2654824574 /* (16 * pi) */) * (1.0 + mumu); - let phase_mie = select(allow_mie, 3.0 / (25.1327412287 /* (8 * pi) */) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)), 0.0); + let phaseMie = select(allowMie, 3.0 / (25.1327412287 /* (8 * pi) */) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)), 0.0); + + let totalMie = (0.0).xxx; - // now we need to sample the 'primary' ray. this ray gathers the light that gets scattered onto it for i in 0 -> steps_i { - // calculate where we are along this ray - let pos_i = start + dir * ray_pos_i; - - // and how high we are above the surface - let height_i = sdRoundBox(pos_i, planet_dims, planet_corner_radius); - //let height_i = length(pos_i) - planetRadius; - - // now calculate the density of the particles (both for rayleigh and mie) - let density = vec3[f32](exp(-height_i / scale_height), 0.0); - - // and the absorption density. this is for ozone, which scales together with the rayleigh, - // but absorbs the most at a specific height, so use the sech function for a nice curve falloff for this height - // clamp it to avoid it going out of bounds. This prevents weird black spheres on the night side - let denom = (height_absorption - height_i) / absorption_falloff; - density.z = (1.0 / (denom * denom + 1.0)) * density.x; - - // multiply it by the step size here - // we are going to use the density later on as well - density *= step_size_i; - - // Add these densities to the optical depth, so that we know how many particles are on this ray. - opt_i += density * HEIGHT_SCALE; - - // Calculate the step size of the light ray. - ray_length = aabbIntersect(pos_i, light_dir, planet_dims); - - // no early stopping, this one should always be inside the atmosphere - // calculate the ray length - let step_size_l = ray_length.y / f32(steps_l); - - // and the position along this ray - // this time we are sure the ray is in the atmosphere, so set it to 0 - let ray_pos_l = step_size_l * 0.5; - - // and the optical depth of this ray - let opt_l = (0.0).xxx; - - // now sample the light ray - // this is similar to what we did before - for l in 0 -> steps_l - { - // calculate where we are along this ray - let pos_l = pos_i + light_dir * ray_pos_l; - - // the heigth of the position - let height_l = sdRoundBox(pos_l, planet_dims, planet_corner_radius); - //let height_l = length(pos_l) - planetRadius; - - // calculate the particle density, and add it - // this is a bit verbose - // first, set the density for ray and mie - let density_l = vec3[f32](exp(-height_l / scale_height), 0.0); - - // then, the absorption - let denom = (height_absorption - height_l) / absorption_falloff; - density_l.z = (1.0 / (denom * denom + 1.0)) * density_l.x; - - // multiply the density by the step size - density_l *= step_size_l * HEIGHT_SCALE; - - // and add it to the total optical depth - opt_l += density_l; - - // and increment where we are along the light ray. - ray_pos_l += step_size_l; - } - - // Now we need to calculate the attenuation - // this is essentially how much light reaches the current sample point due to scattering - let attn = exp(-beta_ray * (opt_i.x + opt_l.x) * HEIGHT_SCALE_INV - beta_mie * (opt_i.y + opt_l.y) * HEIGHT_SCALE_INV - beta_absorption * (opt_i.z + opt_l.z)); - - // accumulate the scattered light (how much will be scattered towards the camera) - total_ray += density.x * attn; - total_mie += density.y * attn; - - // and increment the position on this ray - ray_pos_i += step_size_i; - } - - // calculate how much light can pass through the atmosphere - let opacity = exp(-(beta_mie * opt_i.y + beta_ray * opt_i.x + beta_absorption * opt_i.z) * HEIGHT_SCALE_INV); - - // calculate and return the final color - return vec3[f32](( - phase_ray * beta_ray * total_ray // rayleigh color - + phase_mie * beta_mie * total_mie // mie - + opt_i.x * beta_ambient // and ambient - ) * light_intensity + scene_color * opacity); // now make sure the background is rendered correctly + let scatterStrength = 1.0; + + let sunRayLength = raySphere(planetOrigin, atmosphereRadius, scatterPoint, sunDir).y; + let sunRayOpticalDepth = computeOpticalDepth(planetOrigin, planetDims, planetCornerRadius, atmosphereRadius, densityFallOff, scatterPoint, sunDir, sunRayLength, steps_l); + viewRayOpticalDepth = computeOpticalDepth(planetOrigin, planetDims, planetCornerRadius, atmosphereRadius, densityFallOff, scatterPoint, -viewerDir, stepSize * f32(i), steps_l); + let transmittance = exp(-(sunRayOpticalDepth + viewRayOpticalDepth) * scatteringCoefficients * scatterStrength); + let localDensity = densityAtPoint(planetOrigin, planetDims, planetCornerRadius, atmosphereRadius, densityFallOff, scatterPoint); + + totalMie += localDensity * transmittance * stepSize / height_mie; + + scatteredLight += localDensity * transmittance * scatteringCoefficients * scatterStrength * stepSize; + scatterPoint += viewerDir * stepSize; + } + + scatteredLight *= clamp(maxDist / (atmosphereRadius * 2.0), 0.0, 1.0); + + let skyColor = scatteredLight + phaseMie.xxx * totalMie; + let skyLuminance = dot(skyColor, vec3[f32](0.2125, 0.7154, 0.0721)); + let sceneColorLuminance = dot(sceneColor, vec3[f32](0.2125, 0.7154, 0.0721)) + clamp((maxDist - atmosphereRadius * 2.0) / atmosphereRadius, 0.0, 1.0); + + let originalColorTransmittance = clamp(exp(-viewRayOpticalDepth) + (1.0 - skyLuminance), 0.0, 1.0); + return originalColorTransmittance * sceneColor + skyColor * sceneColorLuminance; +} + +fn densityAtPoint(planetCenter: vec3[f32], planetDims: vec3[f32], planetCornerRadius: f32, atmosphereRadius: f32, densityFallOff: f32, samplePoint: vec3[f32]) -> f32 +{ + //let heightAboveSurface = length(samplePoint - planetCenter) - 120.0; + let heightAboveSurface = sdRoundBox(samplePoint - planetCenter, planetDims, planetCornerRadius); + let clampedHeight = max(0.0, heightAboveSurface); + let height01 = clampedHeight / (atmosphereRadius); //< FIXME + + return exp(-height01 * densityFallOff) * (1.0 - height01); +} + +fn computeOpticalDepth(planetCenter: vec3[f32], planetDims: vec3[f32], planetCornerRadius: f32, atmosphereRadius: f32, densityFallOff: f32, rayOrigin: vec3[f32], rayDirection: vec3[f32], rayLength: f32, secondaryStepCount: i32) -> f32 +{ + let densitySamplePoint = rayOrigin; + let stepSize = rayLength / f32(secondaryStepCount - 1); + + let opticalDepth = 0.0; + + for i in 0 -> secondaryStepCount + { + let localDensity = densityAtPoint(planetCenter, planetDims, planetCornerRadius, atmosphereRadius, densityFallOff, densitySamplePoint); + + opticalDepth += localDensity * stepSize; + densitySamplePoint += rayDirection * stepSize; + } + + return opticalDepth; } fn sdRoundBox(pos: vec3[f32], dims: vec3[f32], cornerRadius: f32) -> f32 @@ -296,9 +248,9 @@ fn sdRoundBox(pos: vec3[f32], dims: vec3[f32], cornerRadius: f32) -> f32 } // https://iquilezles.org/articles/intersectors/ -fn aabbIntersect(rayOrigin: vec3[f32], rayDir: vec3[f32], boxSize: vec3[f32]) -> vec2[f32] +fn aabbIntersect(rayOrigin: vec3[f32], rayDirection: vec3[f32], boxSize: vec3[f32]) -> vec2[f32] { - let m = 1.0 / rayDir; // can precompute if traversing a set of aligned boxes + let m = 1.0 / rayDirection; // can precompute if traversing a set of aligned boxes let n = m * rayOrigin; // can precompute if traversing a set of aligned boxes let k = abs(m) * boxSize; let t1 = -n - k; @@ -308,3 +260,27 @@ fn aabbIntersect(rayOrigin: vec3[f32], rayDir: vec3[f32], boxSize: vec3[f32]) -> return vec2[f32](tN, tF); } + +fn raySphere(spherePos: vec3[f32], sphereRadius: f32, rayOrigin: vec3[f32], rayDirection: vec3[f32]) -> vec2[f32] +{ + let offset = rayOrigin - spherePos; + let a = 1.0; // Set to dot(rayDirection, rayDirection) if rayDirection might not be normalized + let b = 2.0 * dot(offset, rayDirection); + let c = dot(offset, offset) - sphereRadius * sphereRadius; + let d = b * b - 4.0 * a * c; // Discriminant from quadratic formula + + // Number of intersections: 0 when d < 0; 1 when d = 0; 2 when d > 0 + if (d > 0.0) + { + let s = sqrt(d); + let dstToSphereNear = max(0.0, (-b - s) / (2.0 * a)); + let dstToSphereFar = (-b + s) / (2.0 * a); + + // Ignore intersections that occur behind the ray + if (dstToSphereFar >= 0.0) { + return vec2(dstToSphereNear, dstToSphereFar - dstToSphereNear); + } + } + // Ray did not intersect sphere + return vec2(-1.0, -1.0); +} \ No newline at end of file diff --git a/include/CommonLib/AtmosphereScattering.hpp b/include/CommonLib/AtmosphereScattering.hpp index 5c1abee4..32630183 100644 --- a/include/CommonLib/AtmosphereScattering.hpp +++ b/include/CommonLib/AtmosphereScattering.hpp @@ -27,9 +27,9 @@ namespace tsom float mieScattering = 0.9f; float rayleighHeight = 32.f; - float mieHeight = 60.f; + float mieHeight = 5.f; float heightAbsorption = 30.f; - float absorptionFalloff = 3.5f; + float absorptionFalloff = 20.f; Nz::Int32 primarySteps = 8; Nz::Int32 lightSteps = 8; From 2dd0e1ffc000c5e2eadae371814117293284bbab Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 23 Apr 2026 00:44:05 +0200 Subject: [PATCH 23/23] Make light ambient dependent on sun and depth --- assets/shaders/BlockPBR.nzsl | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index c0356a39..21b38c85 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -218,6 +218,24 @@ fn UnpackNormal(texData: vec2[f32]) -> vec3[f32] return normal; } +fn RoundBoxUp(pos: vec3[f32], cornerRadius: f32) -> vec3[f32] +{ + let distToCenter = max(max(abs(pos.x), abs(pos.y)), abs(pos.z)); + + let innerReductionSize = max(distToCenter - max(cornerRadius, 1.0), 0.0); + let innerBoxMin = -innerReductionSize.xxx; + let innerBoxMax = innerReductionSize.xxx; + + let innerPos = clamp(pos, innerBoxMin, innerBoxMax); + return normalize(pos - innerPos); +} + +fn sdRoundBox(pos: vec3[f32], dims: vec3[f32], cornerRadius: f32) -> f32 +{ + let q = abs(pos) - dims + cornerRadius.xxx; + return length(max(q, (0.0).xxx)) + min(max(q.x, max(q.y, q.z)), 0.0) - cornerRadius; +} + [export] fn ComputeLighting(input: VertOut) -> vec4[f32] { @@ -314,6 +332,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] F0 = albedo * metallic + F0 * (1.0 - metallic); let albedoFactor = albedo / Pi; + let lightAmbient = 0.01; for lightIndex in u32(0) -> lightData.directionalLightCount { @@ -324,6 +343,11 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] let shadowFactor = Shadow.ComputeDirectionalLightShadow(light, shadowMapsDirectional[lightIndex], input.worldPos, lambert, viewerData.viewMatrix); let radiance = CookTorrancePBR.ComputeLightRadiance(light.color.rgb, -light.direction, albedoFactor, eyeVec, F0, normal, metallic, roughness); + let ambient = 0.1 * clamp(0.6 + dot(normalize(input.worldPos), -light.direction), 0.0, 1.0); + + ambient *= clamp(sdRoundBox(input.worldPos, (32.0).xxx, 16.0) / 64.0, 0.0, 1.0); + + lightAmbient = max(lightAmbient, ambient); lightRadiance += shadowFactor * radiance; } @@ -365,7 +389,7 @@ fn ComputeLighting(input: VertOut) -> vec4[f32] lightRadiance += shadowFactor * radiance; } - let ambient = (0.05).rrr * albedo * ao; + let ambient = lightAmbient * albedo * ao; return vec4[f32](ambient + lightRadiance, color.a); }