diff --git a/.gitignore b/.gitignore index b2e929e2..0f7f3316 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ assets/* !assets/*.passlist +!assets/*.json !assets/shaders +cache/ +CookedAssets/ saves/ gameconfig.lua serverconfig.lua 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/assets.json b/assets/assets.json new file mode 100644 index 00000000..d6adbdeb --- /dev/null +++ b/assets/assets.json @@ -0,0 +1,154 @@ +{ + "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": "crosshair.png", + "output": "Textures/crosshair.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": "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", + "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" + }, + { + "input": "blocks.json", + "output": "BlockData.json", + "method": "Copy" + } + ] +} \ No newline at end of file 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/assets/shaders/BlockPBR.nzsl b/assets/shaders/BlockPBR.nzsl index 692e4fd7..21b38c85 100644 --- a/assets/shaders/BlockPBR.nzsl +++ b/assets/shaders/BlockPBR.nzsl @@ -9,52 +9,37 @@ 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; 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], + ambientOcclusion: f32, + metalness: f32, + roughness: f32 +} + +[layout(std430)] +struct GlobalBlockData +{ + blocks: dyn_array[BlockData] +} [layout(std140)] struct MaterialSettings @@ -71,6 +56,9 @@ struct MaterialSettings [tag("BaseColor")] BaseColor: vec4[f32], + + [tag("TriplanarOffset")] + TriplanarOffset: vec3[f32] } // TODO: Add enums @@ -80,18 +68,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, readonly], + + // 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 +98,10 @@ 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)] triplanarNormal: vec3[f32], + [location(3), interp(flat)] blockIndex: u32, + [location(4)] normal: vec3[f32], [builtin(position)] position: vec4[f32], } @@ -129,96 +115,224 @@ 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; - - const if (HasUV) - color.a *= TextureOverlay.Sample(input.uv.xy).r; + // 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 (HasColor) - color *= input.color; +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 (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)); + 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; +} - 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); +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;*/ +} - color *= textureColor; - } +[export] +fn ComputeColor(input: VertOut) -> vec4[f32] +{ + let blockData = MaterialData.globalBlockData.blocks[input.blockIndex]; - const if (HasAlphaTexture) + let color = blockData.baseColorFallback; + 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.triplanarNormal, 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; +} + +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(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) { - let N = normalize(input.normal); - let T = normalize(input.tangent); - let B = cross(N, T); - let tbnMatrix = mat3[f32](T, B, N); + color = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.baseColorMapIndices); + color.rgb = Color.sRGBToLinear(color.rgb); + } - normal = (MaterialNormalMap.Sample(uv).xyz * 2.0 - (1.0).rrr); - normal = normalize(tbnMatrix * normal); + const if (AlphaTest) + { + if (color.w < MaterialData.settings.AlphaThreshold) + discard; + } + + 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 metallic = blockData.metalness; + let roughness = blockData.roughness; + let ao = blockData.ambientOcclusion; + + if (blockData.roughnessMetalnessMapIndices.x >= 0) { - let detail = MaterialDetailMap.Sample(uv).rgb; - roughness = 1.0 - detail.r; - metallic = detail.y; + let texData = TriplanarSample(input.triplanarNormal, uvX, uvY, uvZ, blockData.roughnessMetalnessMapIndices).xy; + roughness = texData.x; + if (blockData.roughnessMetalnessMapIndices.y > 2) + metallic = texData.y; } - const if (HasMetallicTexture) - metallic = MaterialMetallicMap.Sample(uv).x; - - const if (HasRoughnessTexture) - roughness = MaterialRoughnessMap.Sample(uv).x; + if (blockData.ambientOcclusionHeightMapIndices.x >= 0) + { + let texData = TriplanarSample(input.triplanarNormal, 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); let albedoFactor = albedo / Pi; + let lightAmbient = 0.01; for lightIndex in u32(0) -> lightData.directionalLightCount { @@ -229,6 +343,11 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[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; } @@ -270,21 +389,14 @@ fn ComputeLighting(color: vec3[f32], input: VertOut) -> vec3[f32] lightRadiance += shadowFactor * radiance; } - let ambient = (0.05).rrr * albedo; - - let finalColor = ambient + lightRadiance; - finalColor = finalColor / (finalColor + vec3[f32](1.0, 1.0, 1.0)); - - return finalColor; + let ambient = lightAmbient * albedo * ao; + return vec4[f32](ambient + lightRadiance, 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 +435,39 @@ 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] -} - -[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; + [location(2)] + blockIndex: u32, } -[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.triplanarNormal = input.normal; + 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..2a554f3c 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; @@ -124,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 @@ -150,144 +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); +) -> vec3[f32] +{ + 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 @@ -297,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; @@ -309,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/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/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/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/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..12d443f6 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); @@ -19,11 +24,36 @@ 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..0e40ceba 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(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_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..3b469df9 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(CookedBlockRegistry::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..241166f3 --- /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/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/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/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; diff --git a/include/CommonLib/BlockLibrary.hpp b/include/CommonLib/BlockLibrary.hpp index e8bb862c..643331a7 100644 --- a/include/CommonLib/BlockLibrary.hpp +++ b/include/CommonLib/BlockLibrary.hpp @@ -26,17 +26,22 @@ 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; inline BlockIndex GetBlockIndex(std::string_view blockName) const; 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); @@ -44,38 +49,39 @@ 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 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; @@ -95,13 +101,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..94ad84ef 100644 --- a/include/CommonLib/BlockLibrary.inl +++ b/include/CommonLib/BlockLibrary.inl @@ -4,11 +4,24 @@ 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]; } + 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/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/include/CommonLib/CookedBlockRegistry.hpp b/include/CommonLib/CookedBlockRegistry.hpp new file mode 100644 index 00000000..0b25b38e --- /dev/null +++ b/include/CommonLib/CookedBlockRegistry.hpp @@ -0,0 +1,76 @@ +// 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_COOKEDBLOCKREGISTRY_HPP +#define TSOM_COMMONLIB_COOKEDBLOCKREGISTRY_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace tsom +{ + class TSOM_COMMONLIB_API CookedBlockRegistry + { + public: + struct BlockEntry; + + CookedBlockRegistry() = default; + CookedBlockRegistry(const CookedBlockRegistry&) = delete; + CookedBlockRegistry(CookedBlockRegistry&&) = default; + ~CookedBlockRegistry() = default; + + void AddBlock(std::string blockName, BlockEntry blockEntry); + + const BlockEntry& GetBlock(std::string_view blockName) const; + + bool SaveToFile(const std::filesystem::path& path) const; + + 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); + + 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; + float ambientOcclusionFallback; + float roughnessFallback; + float metalnessFallback; + }; + + private: + std::unordered_map, std::equal_to<>> m_blockEntries; + }; +} + +#endif // TSOM_COMMONLIB_COOKEDBLOCKREGISTRY_HPP diff --git a/include/CommonLib/CookedBlockRegistry.inl b/include/CommonLib/CookedBlockRegistry.inl new file mode 100644 index 00000000..aa43beb0 --- /dev/null +++ b/include/CommonLib/CookedBlockRegistry.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/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..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 @@ -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/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 4927b92c..ef0b6037 100644 --- a/include/CommonLib/Scripting/MathScriptingLibrary.hpp +++ b/include/CommonLib/Scripting/MathScriptingLibrary.hpp @@ -26,14 +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 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..8eae384c 100644 --- a/include/CommonLib/Scripting/ScriptingUtils.inl +++ b/include/CommonLib/Scripting/ScriptingUtils.inl @@ -5,6 +5,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include namespace tsom { @@ -182,3 +188,219 @@ 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_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 + struct lua_type_of> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + + template + struct lua_type_of> : std::integral_constant {}; + + template + 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 + 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); + + 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 + 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 vec2 = sol::stack::get(L, absoluteIndex); + T x = vec2["x"]; + T y = vec2["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 vec3 = sol::stack::get(L, absoluteIndex); + T x = vec3["x"]; + T y = vec3["y"]; + T z = vec3["z"]; + + tracking.use(1); + + return Nz::Vector3(x, y, z); + } + + + 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::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) + { + 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 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& vec) + { + lua_createtable(L, 0, 2); + luaL_setmetatable(L, "vec2"); + 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& vec) + { + lua_createtable(L, 0, 3); + luaL_setmetatable(L, "vec3"); + sol::stack_table vecTable(L); + vecTable["x"] = vec.x; + vecTable["y"] = vec.y; + vecTable["z"] = vec.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/assets/computer.lua b/scripts/assets/computer.lua index ef81de30..f38e1acf 100644 --- a/scripts/assets/computer.lua +++ b/scripts/assets/computer.lua @@ -1,17 +1,17 @@ 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 } -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 296e6b57..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"] = { @@ -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/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 80b24cfe..66241ad4 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") @@ -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(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/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..c715ec44 --- /dev/null +++ b/scripts/libraries/eulerangles.lua @@ -0,0 +1,11 @@ + +local EulerAnglesMt = CreateMetatable("eulerangles") +EulerAnglesMt.__index = EulerAnglesMt + +function EulerAngles(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..872cd15c --- /dev/null +++ b/scripts/libraries/quaternion.lua @@ -0,0 +1,32 @@ + +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) + 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/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 new file mode 100644 index 00000000..28edef70 --- /dev/null +++ b/scripts/libraries/vec2.lua @@ -0,0 +1,74 @@ + +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 + +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) + 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..1c3015a7 --- /dev/null +++ b/scripts/libraries/vec3.lua @@ -0,0 +1,79 @@ + +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 + +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) + 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 03483e1a..7bcd12e8 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 = 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 (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..d30ebcf6 100644 --- a/scripts/planets/bob.lua +++ b/scripts/planets/bob.lua @@ -1,12 +1,28 @@ -- 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 -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 -return function (chunk, seed) +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) + cavePerlin:reseed(seed * seed) + + local blockSize = chunk:GetBlockSize() local blockLibrary = chunk:GetBlockLibrary() local blockCount = chunk:GetBlockCount() @@ -16,101 +32,149 @@ 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") 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") + local barkBlock = blockLibrary:GetBlockIndex("bark") + local cliffRock = blockLibrary:GetBlockIndex("cliff_rocks") 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 caveBaseHeight = baseHeight - 12.0 + local terrainVariation1Scale = 0.06 * baseHeight local terrainVariation2Scale = 0.16 * baseHeight - local moutainScale = 0.03 * baseHeight - local spikeScale = 0.2 * baseHeight - local caveScale = 0.06 -- Other scale unit - - local content = {} - + local moutainScale = 0.035 * baseHeight + local spikeScale = 0.1 * baseHeight + 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 = 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 - table.insert(content, emptyBlock) + 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 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 blockPresence = perlin:normalizedOctave3D_01(blockPos.x * caveScale, blockPos.y * caveScale, blockPos.z * caveScale, 4, 0.1) + 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) - if distToCenter <= baseHeight then - if blockPresence >= 0.3 and blockPresence <= 0.7 then - if distToCenter <= baseHeight-5 then - table.insert(content, stoneBlock) - elseif distToCenter <= baseHeight then - table.insert(content, dirtBlock) - end - else - table.insert(content, stoneBlock) - end + 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) - 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 = 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) - 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 * 20 - - local height = baseHeight + 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) + if distToCenter < 0.0 then + blockType = dirtBlock end + table.insert(content, blockType) + else + table.insert(content, blockType) end - + ::continue:: end 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 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..9cfd6273 --- /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 = 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(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) + 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..2b10ca19 --- /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 = Vec2(Vec2(p.x, p.z):GetLength() - t.x, p.y) + return q:GetLength() - t.y +end + +local function sdOctahedron(p, s) + 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 + +--[[ +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 = 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() +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 = (Vec3(chunkcount.x, chunkcount.y, chunkcount.z) + Vec3(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, Vec2(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, Vec2(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/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/src/AssetCooker/Cooker.cpp b/src/AssetCooker/Cooker.cpp new file mode 100644 index 00000000..fda86c16 --- /dev/null +++ b/src/AssetCooker/Cooker.cpp @@ -0,0 +1,10 @@ +// 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 +{ + 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..48303bf4 --- /dev/null +++ b/src/AssetCooker/ShaderCooker.cpp @@ -0,0 +1,78 @@ +// 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 + +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) + { + 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(); + + 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; + } + + 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); + }; + + 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 compile {}: {}", Nz::PathToString(m_inputFile), errOutput); + 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..e63882a4 --- /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("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/BlockSelectionBar.cpp b/src/ClientLib/BlockSelectionBar.cpp index 95507e78..c93e538c 100644 --- a/src/ClientLib/BlockSelectionBar.cpp +++ b/src/ClientLib/BlockSelectionBar.cpp @@ -7,23 +7,23 @@ #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)); diff --git a/src/ClientLib/ClientBlockLibrary.cpp b/src/ClientLib/ClientBlockLibrary.cpp index d64af08a..ced806c3 100644 --- a/src/ClientLib/ClientBlockLibrary.cpp +++ b/src/ClientLib/ClientBlockLibrary.cpp @@ -7,47 +7,225 @@ #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 ambientOcclusionHeightMapIndices; + std::size_t baseColorMapIndices; + std::size_t metalness; + std::size_t normalMapIndices; + std::size_t ambientOcclusion; + std::size_t roughness; + std::size_t roughnessMetalnessMapIndices; + }; + + 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.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); + + 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.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); + auto& fs = m_applicationBase.GetComponent(); - std::size_t sliceCount = m_textureIndices.size() + 1; + std::optional cookRegistry; + fs.GetFileContent("CookedAssets/Blocks/registry.json", [&](const void* ptr, Nz::UInt64 size) + { + cookRegistry = CookedBlockRegistry::LoadFromString(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 remainingBlockTextures; + remainingBlockTextures.reserve(m_blocks.size()); + + for (const BlockData& blockData : m_blocks) + { + const auto& cookedBlockData = cookRegistry->GetBlock(blockData.name); + + 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::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; - constexpr std::size_t texSize = 256; // TODO: use texture size? + for (const auto& [stream, textureData] : blockTexture.textureData.iter_kv()) + { + if (textureData->type != CookedBlockRegistry::TextureType::None) + textureCount[textureData->type]++; + } + } + + 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::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) + { + 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.ambientOcclusionHeightMapIndices, + s_blockBufferEntryOffsets.baseColorMapIndices, + s_blockBufferEntryOffsets.normalMapIndices, + s_blockBufferEntryOffsets.roughnessMetalnessMapIndices + }; - 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)); + std::vector> previewTextures(m_blocks.size()); - for (auto&& [texPath, texIndex] : m_textureIndices) + for (std::size_t blockIndex = 0; blockIndex < remainingBlockTextures.size(); ++blockIndex) { - Nz::ImageParams loadParams; - loadParams.loadFormat = Nz::PixelFormat::RGBA8; + const BlockTexture& blockTexture = remainingBlockTextures[blockIndex]; - 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 == CookedBlockRegistry::TextureType::None) + continue; - 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)); + std::string texturePath = fmt::format("CookedAssets/Blocks/{}", textureData->path); + std::shared_ptr stream = fs.GetFile(texturePath); + if (!stream) + { + spdlog::error("asset {} not found", texturePath); + continue; + } - 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)); + std::shared_ptr image = Nz::Image::LoadFromStream(*stream); + if (!image) + { + spdlog::error("failed to load {}", texturePath); + continue; + } + + for (Nz::UInt8 level = 0; level < image->GetLevelCount(); ++level) + { + blockTextures[textureData->type]->Update([&](void* pixelBuffer) + { + 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]++; + } } - 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)); + 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) @@ -56,10 +234,10 @@ namespace tsom Nz::TextureViewInfo slotTexView = { .viewType = Nz::ImageType::E2D, - .baseArrayLayer = blockData.texIndices[Direction::Up] + .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 4b7538c0..d4f4563f 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 @@ -10,9 +11,10 @@ #include #include #include +#include #include -#include #include +#include #include #include #include @@ -21,11 +23,14 @@ #include #include #include +#include #include +#include #include #include #include #include +#include namespace tsom { @@ -34,7 +39,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.6f); + 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 +106,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", 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) @@ -123,7 +129,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 +144,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 +171,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 +195,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(); @@ -284,20 +285,28 @@ 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)); } + else if (auto* gfxComponent = visualEntity.try_get()) + gfxComponent->Clear(); UpdateChunkDebugCollider(chunkIndices); }; @@ -321,6 +330,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..dcf8dbaf 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); @@ -163,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) @@ -178,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) @@ -199,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) @@ -225,7 +249,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 +269,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 +297,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 +326,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(); @@ -477,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(); @@ -544,7 +610,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()) @@ -609,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()); @@ -653,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/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..983867d0 100644 --- a/src/CommonLib/BlockLibrary.cpp +++ b/src/CommonLib/BlockLibrary.cpp @@ -3,115 +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", { - .baseBackPath = "blocks/debug_back", - .baseDownPath = "blocks/debug_down", - .baseFrontPath = "blocks/debug_front", - .baseLeftPath = "blocks/debug_left", - .baseRightPath = "blocks/debug_right", - .baseUpPath = "blocks/debug_up", - }); - - RegisterBlock("dirt", { - .basePath = "blocks/dirt", - .isSmooth = true, - .density = 1.0f, - .permeability = 0.1f - }); - - RegisterBlock("grass", { - .basePath = "blocks/grass_top", - .baseDownPath = "blocks/dirt", - .baseSidePath = "blocks/grass_side", - .isSmooth = true, - .density = 2.0f, - .permeability = 0.1f - }); - - RegisterBlock("hull", { - .basePath = "blocks/smooth_stone" - }); - - RegisterBlock("hull2", { - .basePath = "blocks/smooth_stone_slab_side" + 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("snow", { - .basePath = "blocks/snow", - .isSmooth = true, - .permeability = 0.5f - }); +namespace nlohmann +{ + template + void from_json(const BasicJsonType& j, tsom::BlockLibrary::PhysicsLayer& layerContainer) + { + const std::string& layerName = j; - RegisterBlock("stone", { - .basePath = "blocks/cobblestone", - .isSmooth = true, - .density = 4.0f - }); + 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)); + } - RegisterBlock("stone_mossy", { - .basePath = "blocks/mossy_cobblestone", - .isSmooth = true, - }); + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(tsom::BlockLibrary::BlockInfo, \ + layerName, basePath, hasCollisions, isDoubleSided, \ + isSmooth, isTransparent, density, \ + metalness, permeability, roughness \ + ); - RegisterBlock("forcefield", { - .basePath = "blocks/forcefield", - .hasCollisions = false, - .isTransparent = true - }); + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(tsom::BlockLibrary::LayerInfo, \ + physicsLayer, isBlended, isFluid, isPhysicsTrigger, renderLayer + ); +} - RegisterBlock("planks", { - .basePath = "blocks/planks", - }); +namespace tsom +{ + bool BlockLibrary::LoadFromString(std::string_view content, bool merge) + { + nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); - RegisterBlock("stone_bricks", { - .basePath = "blocks/stone_bricks", - }); + if (!merge) + Clear(); - RegisterBlock("copper_block", { - .basePath = "blocks/copper_block", - }); + for (const auto& [layerName, layerEntryDoc] : doc["layers"].items()) + RegisterLayer(layerName, layerEntryDoc); - RegisterBlock("glass", { - .basePath = "blocks/glass", - .isDoubleSided = true, - .isSmooth = true, - .isTransparent = true - }); + for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) + RegisterBlock(blockName, blockEntryDoc); - RegisterBlock("water", { - .layerName = "water", - .basePath = "blocks/water", - .isDoubleSided = true, - .isSmooth = true, - .isTransparent = true, - }); + return true; } BlockIndex BlockLibrary::RegisterBlock(std::string name, BlockInfo blockInfo) @@ -124,44 +81,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); @@ -177,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)); @@ -185,16 +114,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/CookedBlockRegistry.cpp b/src/CommonLib/CookedBlockRegistry.cpp new file mode 100644 index 00000000..a0c3eafc --- /dev/null +++ b/src/CommonLib/CookedBlockRegistry.cpp @@ -0,0 +1,97 @@ +// 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::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 + 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; + } + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::CookedBlockRegistry::Texture, path, type) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(tsom::CookedBlockRegistry::BlockEntry, + ambientOcclusionFallback, ambientOcclusionHeightTexture, + baseColorFallback, baseColorTexture, + metalnessFallback, normalMapTexture, + roughnessFallback, roughnessMetalnessTexture + ) +} + +namespace tsom +{ + 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 CookedBlockRegistry::GetBlock(std::string_view blockName) const -> const BlockEntry& + { + return Nz::Retrieve(m_blockEntries, blockName); + } + + bool CookedBlockRegistry::SaveToFile(const std::filesystem::path& path) const + { + nlohmann::ordered_json blockEntries; + for (const auto& [blockName, blockEntry] : m_blockEntries) + blockEntries[blockName] = blockEntry; + + 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 CookedBlockRegistry::LoadFromString(std::string_view content) + { + nlohmann::ordered_json doc = nlohmann::ordered_json::parse(content); + + CookedBlockRegistry cookRegistry; + for (const auto& [blockName, blockEntryDoc] : doc["blocks"].items()) + cookRegistry.AddBlock(blockName, blockEntryDoc); + + return cookRegistry; + } + + std::optional CookedBlockRegistry::LoadFromFile(const std::filesystem::path& path) + { + std::optional> contentOpt = Nz::File::ReadWhole(path); + if (!contentOpt) + return std::nullopt; + + return LoadFromString(std::string_view(reinterpret_cast(contentOpt->data()), contentOpt->size())); + } +} 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..ed04b751 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 @@ -141,9 +142,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 +245,29 @@ 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(); + chunkGenerator.scriptingContext.RegisterLibrary(); + + chunkGenerator.scriptingContext.LoadDirectory("scripts/libraries"); + + 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 +284,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,15 +300,158 @@ 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) { + m_chunkGenerators.Clear(); ForEachChunk([=, this, &taskScheduler](const ChunkIndices& chunkIndices, Chunk& chunk) { if (chunk.HasContent()) 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/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..e44019af 100644 --- a/src/CommonLib/Scripting/MathScriptingLibrary.cpp +++ b/src/CommonLib/Scripting/MathScriptingLibrary.cpp @@ -5,14 +5,8 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include #include +#include #include namespace tsom @@ -20,62 +14,7 @@ namespace tsom void MathScriptingLibrary::Register(sol::state& state) { state["DirectionFromNormal"] = &DirectionFromNormal; - - RegisterBox(state, "Boxf"); - RegisterBox(state, "Boxi"); - RegisterBox(state, "Boxui"); - RegisterColor(state); - RegisterEulerAngles(state, "EulerAnglesf"); RegisterPerlinNoise(state); - RegisterQuaternion(state, "Quaternionf"); - 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) @@ -114,117 +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::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/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..a3978a13 100644 --- a/src/Game/GameAppComponent.cpp +++ b/src/Game/GameAppComponent.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in LICENSE #include +#include #include #include #include @@ -38,7 +39,9 @@ #include #include #include +#include #include +#include #include #ifdef TSOM_DEV_TOOLS @@ -47,6 +50,32 @@ namespace tsom { + namespace + { + enum class MandatoryFeature + { + BC1_sRGB, + BC3_sRGB, + BC4, + BC5, + Depth32F, + PersistentMapping, + StorageBuffers, + + Max = StorageBuffers + }; + + 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" + }; + } + GameAppComponent::GameAppComponent(Nz::ApplicationBase& app) : ApplicationComponent(app) { @@ -56,14 +85,40 @@ 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::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 + }; + + // 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.": "" ) ); @@ -244,14 +299,21 @@ namespace tsom } auto& filesystem = app.GetComponent(); - filesystem.Mount("assets", assetPath); + 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); - m_blockLibrary->BuildTexture(); + + 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; + }); + + m_blockLibrary->BuildTexture(*Nz::Graphics::Instance()->GetRenderDevice()); return true; } @@ -262,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)); @@ -339,7 +401,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/BackgroundState.cpp b/src/Game/States/BackgroundState.cpp index 5129dbf8..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); @@ -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 6003b31b..25e50706 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -15,10 +15,10 @@ #include #include #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 @@ -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)); @@ -95,14 +95,14 @@ 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(); } 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); @@ -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); } @@ -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; @@ -788,6 +788,8 @@ namespace tsom if (!stateData.networkSession) return true; + stateData.sessionHandler->Update(); + m_timerManager.Update(elapsedTime); m_chatBox->Update(); @@ -1119,7 +1121,7 @@ namespace tsom } GetStateData().world->GetSystem().SetCameraPosition(m_cameraEntity.get().GetGlobalPosition()); - + #if defined(TSOM_DEV_TOOLS) && 0 if (stateData.imgui) { 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/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/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..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 : { "database", "scripts" }) + for (const char* directory : { "CookedAssets", "database", "scripts" }) { std::filesystem::path dirPath = Nz::Utf8Path(directory); if (!std::filesystem::is_directory(dirPath)) @@ -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/ServerInstance.cpp b/src/ServerLib/ServerInstance.cpp index 3cf328ec..4ce7e3ea 100644 --- a/src/ServerLib/ServerInstance.cpp +++ b/src/ServerLib/ServerInstance.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -32,9 +34,18 @@ namespace tsom m_config(std::move(config)), m_scriptingContext(application) { + auto& fs = m_application.GetComponent(); + + 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; + }); + 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 +281,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(); 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..38d28733 100644 --- a/xmake.lua +++ b/xmake.lua @@ -29,14 +29,16 @@ 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]" + "sqlitecpp[sqlite3_external]", + "tiny-process-library" ) if has_config("serveronly") then @@ -55,6 +57,7 @@ if has_config("serveronly") then plugin_imgui = false } }) + add_requires("nzsl") end if is_plat("macosx") then @@ -117,7 +120,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) @@ -275,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" }