From 4c8c26bb33819cf6e29b87d1f87209b06a250d06 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 9 Oct 2026 00:42:19 -0700 Subject: [PATCH 1/7] Corrupted CUDA context + Empty sky enum --- .../lnx/material/cycles_nodes/nodes_texture.py | 4 ++++ leenkx/blender/lnx/write_probes.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py index 52029cc2..c59c1f40 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py @@ -386,6 +386,10 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo elif node.sky_type == 'MULTIPLE_SCATTERING': return parse_sky_multiple_scattering(node, state) + elif node.sky_type == '': + state.curshader.add_uniform('vec3 sunDir', link='_sunPosition') + return c.to_vec3([0.0, 0.0, 0.0]) + else: log.error(f'Unsupported sky model: {node.sky_type}!') return c.to_vec3([0.0, 0.0, 0.0]) diff --git a/leenkx/blender/lnx/write_probes.py b/leenkx/blender/lnx/write_probes.py index 482c6b33..d37f9872 100644 --- a/leenkx/blender/lnx/write_probes.py +++ b/leenkx/blender/lnx/write_probes.py @@ -123,7 +123,16 @@ def render_envmap(target_dir: str, world: bpy.types.World) -> str: render_path = os.path.join(target_dir, image_name) scene.render.filepath = render_path - bpy.ops.render.render(write_still=True, scene=scene.name) + try: + bpy.ops.render.render(write_still=True, scene=scene.name) + except RuntimeError as e: + log.warn(f'Environment map render failed ({e}), writing a grey image instead') + img = bpy.data.images.new('_lnx_noworld', 64, 32) + img.pixels = [0.05] * len(img.pixels) + img.filepath_raw = render_path + img.file_format = ENVMAP_FORMAT + img.save() + bpy.data.images.remove(img) return image_name From c320c4bb19d35b82484a0c94566da5ec48ccac6d Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 9 Oct 2026 15:02:20 -0700 Subject: [PATCH 2/7] Batch Meshes --- leenkx/Sources/iron/data/MeshBatch.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/Sources/iron/data/MeshBatch.hx b/leenkx/Sources/iron/data/MeshBatch.hx index fd49a5f4..1edfbcd3 100644 --- a/leenkx/Sources/iron/data/MeshBatch.hx +++ b/leenkx/Sources/iron/data/MeshBatch.hx @@ -71,6 +71,7 @@ class MeshBatch { if (b.meshes.length > 0 && b.meshes[0].cullMaterial(context)) continue; var scontext = b.shader.getContext(context); + if (scontext == null) continue; g.setPipeline(scontext.pipeState); // #if lnx_deinterleaved // TODO // g.setVertexBuffers(b.vertexBuffers); From c1aa2837cab4c9382d33355559de2f6ca29e7b79 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 9 Oct 2026 16:30:11 -0700 Subject: [PATCH 3/7] Deinterleaved Vertex Buffer Update --- leenkx/Sources/iron/data/Geometry.hx | 19 ++++--- leenkx/Sources/iron/data/MeshBatch.hx | 75 ++++++++++++++++++++++++-- leenkx/Sources/iron/data/ShaderData.hx | 17 ++++-- 3 files changed, 97 insertions(+), 14 deletions(-) diff --git a/leenkx/Sources/iron/data/Geometry.hx b/leenkx/Sources/iron/data/Geometry.hx index e111a296..6d07d38e 100644 --- a/leenkx/Sources/iron/data/Geometry.hx +++ b/leenkx/Sources/iron/data/Geometry.hx @@ -14,6 +14,7 @@ import iron.math.Vec4; import iron.math.Mat4; import iron.data.SceneFormat; import iron.data.MeshData; +import iron.data.ShaderData; class Geometry { #if lnx_deinterleaved @@ -208,7 +209,7 @@ class Geometry { for (v in 0...vertexBuffers.length) if (vertexBuffers[v].name == e.name) { vbs.push(vertexBuffers[v].buffer); - continue; + break; } if (e.name == "ipos" && instancedVB != null) { vbs.push(instancedVB); @@ -264,7 +265,7 @@ class Geometry { for (i in 0...vaLength) vertexBuffers[i] = { name: vertexArrays[i].attrib, - buffer: makeDeinterleavedVB(vertexArrays[i].values, vertexArrays[i].attrib, vertexArrays[i].size) + buffer: makeDeinterleavedVB(vertexArrays[i]) }; #else @@ -295,14 +296,20 @@ class Geometry { } #if lnx_deinterleaved - function makeDeinterleavedVB(data: ByteArray, name: String, structLength: Int): VertexBuffer { + function makeDeinterleavedVB(va: TVertexArray): VertexBuffer { var struct = new VertexStructure(); - struct.add(name, structLength == 2 ? VertexData.Short2Norm : VertexData.Short4Norm); + struct.add(va.attrib, ShaderContext.parseData(va.data)); - var vertexBuffer = new VertexBuffer(Std.int(data.byteLength / 2 / structLength), struct, usage); + var vcount = verticesCount(va); + var vertexBuffer = new VertexBuffer(vcount, struct, usage); + var stride = Std.int(struct.byteSize() / 2); var vertices = vertexBuffer.lock(); - for (i in 0...Std.int(vertices.byteLength / 2)) vertices.setInt16(i * 2, data.getInt16(i * 2)); + var di = -1; + for (i in 0...vcount) { + for (o in 0...va.size) vertices.setInt16(++di * 2, va.values[i * va.size + o]); + for (o in va.size...stride) vertices.setInt16(++di * 2, 0); + } vertexBuffer.unlock(); return vertexBuffer; diff --git a/leenkx/Sources/iron/data/MeshBatch.hx b/leenkx/Sources/iron/data/MeshBatch.hx index 1edfbcd3..1e78f29f 100644 --- a/leenkx/Sources/iron/data/MeshBatch.hx +++ b/leenkx/Sources/iron/data/MeshBatch.hx @@ -73,11 +73,11 @@ class MeshBatch { var scontext = b.shader.getContext(context); if (scontext == null) continue; g.setPipeline(scontext.pipeState); - // #if lnx_deinterleaved // TODO - // g.setVertexBuffers(b.vertexBuffers); - // #else + #if lnx_deinterleaved + g.setVertexBuffers(b.getVertexBuffers(scontext.raw.vertex_elements)); + #else g.setVertexBuffer(b.getVertexBuffer(scontext.raw.vertex_elements)); - // #end + #end g.setIndexBuffer(b.indexBuffer); Uniforms.setContextConstants(g, scontext, bindParams); @@ -127,8 +127,12 @@ class Bucket { public var batched = false; public var shader: ShaderData; +#if lnx_deinterleaved + public var vertexBuffers: Array = []; +#else var vertexBuffer: VertexBuffer; var vertexBufferMap: Map = new Map(); +#end public var indexBuffer: IndexBuffer; public var meshes: Array = []; @@ -138,8 +142,12 @@ class Bucket { public function remove() { indexBuffer.delete(); + #if lnx_deinterleaved + for (buf in vertexBuffers) buf.buffer.delete(); + #else // this.vertexBuffer is in the map, so it's also deleted here for (buf in vertexBufferMap) buf.delete(); + #end meshes = []; } @@ -151,6 +159,20 @@ class Bucket { meshes.remove(m); } +#if lnx_deinterleaved + public function getVertexBuffers(elems: Array): Array { + var vbs: Array = []; + for (e in elems) { + for (v in vertexBuffers) { + if (v.name == e.name) { + vbs.push(v.buffer); + break; + } + } + } + return vbs; + } +#else function copyAttribute(attribSize: Int, count: Int, to: ByteArray, toStride: Int, toOffset: Int, from: ByteArray, fromStride: Int, fromOffset: Int) { for (i in 0...count) { for (j in 0...attribSize) { @@ -196,6 +218,7 @@ class Bucket { } return vb; } +#end function vertexCount(g: Geometry, hasUVs: Bool): Int { var vcount = g.getVerticesLength(); @@ -240,6 +263,50 @@ class Bucket { if (mdatas.length == 0) return; +#if lnx_deinterleaved + var attrNames: Array = []; + var attrReps = new Map(); + for (md in mdatas) { + for (va in md.geom.vertexArrays) { + if (attrReps.get(va.attrib) == null) { + attrReps.set(va.attrib, va); + attrNames.push(va.attrib); + } + } + } + for (name in attrNames) { + var rep = attrReps.get(name); + var attrStruct = new VertexStructure(); + attrStruct.add(name, ShaderContext.parseData(rep.data)); + var stride = Std.int(attrStruct.byteSize() / 2); + var vtotal = 0; + for (md in mdatas) vtotal += md.geom.getVerticesCount(); + var vb = new VertexBuffer(vtotal, attrStruct, Usage.StaticUsage); + var vertices = vb.lock(); + var ai = -1; + for (md in mdatas) { + var va: TVertexArray = null; + for (v in md.geom.vertexArrays) { + if (v.attrib == name) { + va = v; + break; + } + } + if (va != null && va.values != null) { + var nverts = md.geom.getVerticesCount(); + for (i in 0...nverts) { + for (o in 0...va.size) vertices.setInt16(++ai * 2, va.values[i * va.size + o]); + for (o in va.size...stride) vertices.setInt16(++ai * 2, 0); + } + } + else { + for (i in 0...md.geom.getVerticesCount() * stride) vertices.setInt16(++ai * 2, 0); + } + } + vb.unlock(); + vertexBuffers.push({name: name, buffer: vb}); + } +#else // Pick UVs if present var vs = mdatas[0].geom.struct; for (md in mdatas) if (md.geom.struct.size() > vs.size()) vs = md.geom.struct; diff --git a/leenkx/Sources/iron/data/ShaderData.hx b/leenkx/Sources/iron/data/ShaderData.hx index 3bcbc8e4..75f55afd 100644 --- a/leenkx/Sources/iron/data/ShaderData.hx +++ b/leenkx/Sources/iron/data/ShaderData.hx @@ -102,8 +102,9 @@ class ShaderContext { constants = []; textureUnits = []; + var instStruct: VertexStructure = null; if (instancingType > 0) { // Instancing - var instStruct = new VertexStructure(); + instStruct = new VertexStructure(); instStruct.add("ipos", VertexData.Float3); if (instancingType == 2 || instancingType == 4) { instStruct.add("irot", VertexData.Float3); @@ -117,11 +118,19 @@ class ShaderContext { this.instanceStride = Std.int(instStruct.byteSize() / 4); instStruct.instanced = true; - pipeState.inputLayout = [structure, instStruct]; } - else { // Regular - pipeState.inputLayout = [structure]; + #if lnx_deinterleaved + var layouts: Array = []; + for (e in structure.elements) { + var s = new VertexStructure(); + s.add(e.name, e.data); + layouts.push(s); } + #else + var layouts = [structure]; + #end + if (instStruct != null) layouts.push(instStruct); + pipeState.inputLayout = layouts; // Depth pipeState.depthWrite = raw.depth_write; From beabf76562bf49e8139fc97e215da4aba2ec687c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 9 Oct 2026 16:33:14 -0700 Subject: [PATCH 4/7] Unclosed conditional --- leenkx/Sources/iron/data/MeshBatch.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/Sources/iron/data/MeshBatch.hx b/leenkx/Sources/iron/data/MeshBatch.hx index 1e78f29f..da880c14 100644 --- a/leenkx/Sources/iron/data/MeshBatch.hx +++ b/leenkx/Sources/iron/data/MeshBatch.hx @@ -324,6 +324,7 @@ class Bucket { var s = ""; for (e in vs.elements) s += e.name; vertexBufferMap.set(s, vertexBuffer); +#end indexBuffer = new IndexBuffer(icount, Usage.StaticUsage); var indices = indexBuffer.lock(); From 32730547662e77d3a32b6623af1d1d940ee57548 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Wed, 14 Oct 2026 14:37:31 -0700 Subject: [PATCH 5/7] Water Shader Update --- .../Shaders/water_pass/water_pass.frag.glsl | 129 ++++++++++++------ 1 file changed, 84 insertions(+), 45 deletions(-) diff --git a/leenkx/Shaders/water_pass/water_pass.frag.glsl b/leenkx/Shaders/water_pass/water_pass.frag.glsl index bce540ca..9dc0ad3d 100644 --- a/leenkx/Shaders/water_pass/water_pass.frag.glsl +++ b/leenkx/Shaders/water_pass/water_pass.frag.glsl @@ -96,41 +96,81 @@ vec3 sampleWaterNormals(vec2 hitXY, float speed, out vec2 tcnor0, out vec2 tcnor return normalize(n0 + n1 - 1.0); } +float waveHeight(vec2 xy, float speed) { + return (sin(xy.x * 10.0 / waterDisplace + speed) * cos(xy.y * 10.0 / waterDisplace + speed) + sin(xy.x * 20.0 / waterDisplace + speed * 1.3) * cos(xy.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) / 50.0 * waterDisplace; +} + void main() { float gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; vec3 vray = normalize(viewRay); float speed = time * 2.0 * waterSpeed; - bool isSky = (gdepth == 1.0); + float farPlane = cameraProj.y / (1.0 - cameraProj.x); + float cosAng = dot(eyeLook, vray); + float domeR = farPlane * 0.94; + float tDome = -3.5 * vray.z + sqrt(max(domeR * domeR - 12.25 * (1.0 - vray.z * vray.z), 0.0)); + float domeDepth01 = cameraProj.x + cameraProj.y / (tDome * cosAng); + bool isSky = (gdepth * 0.5 + 0.5) >= domeDepth01; // Ray-plane intersection with water surface (z = waterLevel) - float denom = dot(vray, vec3(0.0, 0.0, 1.0)); + float denom = vray.z; float tWater = (waterLevel - eye.z) / denom; bool hasWaterHit = (abs(denom) > 0.0001) && (tWater > 0.0); if (eye.z < waterLevel) { vec2 tc = texCoord; - float fogFactor; - - if (hasWaterHit && denom > 0.0) { - // Looking up at water surface - apply normal distortion - vec3 hit = eye + tWater * vray; - vec2 tc0, tc1; - vec3 n2 = sampleWaterNormals(hit.xy * waterFreq, speed, tc0, tc1); - tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; - fogFactor = clamp(tWater * waterDensity, 0.0, 0.95); - } else { - // Looking forward/down - distort via water surface above fragment - vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj); - vec2 wxy = isSky ? (eye.xy + vray.xy * 50.0) : p.xy; - vec2 tc0, tc1; - vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1); - tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; - float waterDist = isSky ? 50.0 : length(p - eye); - fogFactor = clamp(waterDist * waterDensity, 0.0, 0.95); + float depthThick = 1.0 + (waterLevel - eye.z); + vec3 p = vec3(0.0); + float geomDist = farPlane; + if (!isSky) { + p = getPos(eye, eyeLook, vray, gdepth, cameraProj); + geomDist = length(p - eye); } + bool upRay = hasWaterHit && denom > 0.0; + float waterDist = upRay ? min(tWater, geomDist) : geomDist; + + vec2 wxy; + if (upRay && tWater < geomDist) { + wxy = (eye + tWater * vray).xy; + } else if (!isSky) { + wxy = p.xy; + } else { + wxy = eye.xy + vray.xy * 50.0; + } + vec2 tc0, tc1; + vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1); + tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + float ddepth2 = textureLod(gbufferD, tc, 0.0).r * 2.0 - 1.0; + float dist2 = cameraProj.y / ((ddepth2 * 0.5 + 0.5) - cameraProj.x) / cosAng; + if (dist2 > geomDist + 1.0) tc = texCoord; vec3 refracted = textureLod(tex, tc, 0.0).rgb; - fragColor.rgb = mix(refracted, waterColor, fogFactor); + vec3 outCol = refracted; + + if (upRay && tWater < geomDist) { + outCol *= max(1.0 - length(n2.xy) * 1.5, 0.0); + vec3 h2 = normalize(-vray + ld); + outCol += pow(max(dot(n2, h2), 0.0), 80.0) * 0.6; + } + + if (!isSky) { + float fdw = abs(p.z - (waterLevel + waveHeight(p.xy, speed))); + float bandw = 0.1 + waterDist * 0.003; + if (fdw < bandw) { + float foamLod = clamp(waterDist / 15.0, 0.0, 5.0); + vec3 fm0 = textureLod(sfoam, tc0 * 3.0 + vec2(speed / 30.0, speed / 50.0), foamLod).rgb; + vec3 fm1 = textureLod(sfoam, tc1 * 4.0 + vec2(-speed / 35.0, speed / 45.0), foamLod).rgb; + float fstr = clamp(1.0 - fm0.r * 0.5 - fm1.b * 0.5, 0.0, 1.0); + float ffac = clamp(1.0 - fdw / bandw, 0.0, 1.0); + float foamFade = clamp(1.0 - (waterLevel - eye.z) * 0.4, 0.0, 1.0); + vec3 foamCol = mix(waterColor, vec3(1.0), 0.6); + outCol = mix(outCol, foamCol, ffac * fstr * 0.5 * foamFade); + } + } + + float fogFactor = 1.0 - exp(-waterDist * waterDensity * 0.006 * depthThick); + outCol = mix(outCol, waterColor, clamp(fogFactor, 0.0, 1.0)); + + fragColor.rgb = outCol; fragColor.a = 1.0; return; } @@ -144,21 +184,19 @@ void main() { if (isSky) tWater = min(tWater, 100.0); // Clamp to prevent aliasing at horizon vec3 p = isSky ? (eye + tWater * vray) : getPos(eye, eyeLook, vray, gdepth, cameraProj); - float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0); - if (!isSky && p.z > waterLevel) { - fragColor = vec4(0.0); + if (!isSky && p.z > waterLevel - 0.01) { + fragColor = textureLod(tex, texCoord, 0.0); return; } + float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0); // Displace surface float geomZ = p.z; // undisplaced depth for foam/fog tests - p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed) - + sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) - / 50.0 * waterDisplace; + p.z += waveHeight(p.xy, speed); // Hit plane to determine uvs vec3 v = normalize(eye - p); - float t = (waterLevel - eye.z) / dot(v, vec3(0.0, 0.0, 1.0)); + float t = (waterLevel - eye.z) / v.z; vec3 hit = eye + t * v; // Sample normal maps @@ -171,8 +209,8 @@ void main() { tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; } else { float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0; - vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj); - tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + float dist2 = cameraProj.y / ((ddepth * 0.5 + 0.5) - cameraProj.x) / cosAng; + tc = (eye.z + vray.z * dist2) > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; } // Light @@ -222,26 +260,25 @@ void main() { #else fragColor.rgb = mix(refracted, reflectedEnv, waterReflect * fresnel); #endif - // Water color tint - blend rather than multiply to preserve brightness float colorMix = isSky ? 0.7 : 0.5; - fragColor.rgb = mix(fragColor.rgb, fragColor.rgb * waterColor, colorMix * horizonFactor); + fragColor.rgb = mix(fragColor.rgb, waterColor, colorMix); // Blinn-Phong specular using half-vector, faded at horizon vec3 h = normalize(v + ld); float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0); - fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0); - // Depth fog - blend toward waterColor with depth, faded at horizon - float depthFog = clamp(-(geomZ - waterLevel) * waterDensity, 0.0, 0.9); - fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor); + fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0) * horizonFactor; + float waterColumn = isSky ? 1.0 : max(length(p - eye) - tWater, 0.0); + float depthFog = 1.0 - exp(-waterColumn * waterDensity * 0.006); + vec3 surfaceCol = mix(waterColor, reflectedEnv, waterReflect * fresnel); + fragColor.rgb = mix(fragColor.rgb, surfaceCol, min(depthFog, 0.9)); // Alpha fades smoothly at horizon instead of hard cut // Skydome sits 3.5 below the camera (_skydomeMatrix), so its horizon // appears where the ray z reaches -3.5 / domeRadius - float farPlane = cameraProj.y / (1.0 - cameraProj.x); float horizonFade = clamp((-3.5 / (farPlane * 0.95) - vray.z) * 20.0, 0.0, 1.0); - fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0, 0.0, 1.0); - - // Foam - based on actual geometry depth below water surface - float fd = isSky ? 1.0 : abs(geomZ - waterLevel); - if (fd < 0.1) { + fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0 + 0.1, 0.0, 1.0); + float waveH = p.z - geomZ; // displacement already applied above + float band = 0.1 + tWater * 0.003; + float fd = isSky ? 1.0 : abs(geomZ - (waterLevel + waveH)); + if (fd < band) { // Based on foam by Owen Deery // http://fire-face.com/personal/water // Distance-based LOD blurs foam at range to reduce noise @@ -250,8 +287,10 @@ void main() { vec2 foamUV1 = tcnor1 * 4.0 + vec2(-speed / 35.0, speed / 45.0); vec3 foamMask0 = textureLod(sfoam, foamUV0, foamLod).rgb; vec3 foamMask1 = textureLod(sfoam, foamUV1, foamLod).rgb; - float foamStrength = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0); - float fac = (1.0 - (fd * (1.0 / 0.1))) * horizonFactor; - fragColor.rgb = mix(fragColor.rgb, mix(fragColor.rgb, waterColor + 0.2, foamStrength), clamp(fac, 0.0, 1.0) * 0.5); + float foamMask = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0); + float fac = clamp(1.0 - fd / band, 0.0, 1.0); + vec3 foamCol = mix(waterColor, vec3(1.0), 0.6); + fragColor.rgb = mix(fragColor.rgb, foamCol, fac * foamMask * 0.8); + fragColor.a = max(fragColor.a, fac * foamMask); } } From 612a3d0a09167196ba997c143e5b68cfec3a7cc0 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Thu, 15 Oct 2026 02:41:03 -0700 Subject: [PATCH 6/7] Object Occlusion Culling --- .../Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 4 +- leenkx/Sources/iron/RenderPath.hx | 137 +++- leenkx/Sources/iron/culling/BVHNode.hx | 20 + .../Sources/iron/culling/ContributionCull.hx | 62 ++ leenkx/Sources/iron/culling/CullData.hx | 373 +++++++++ leenkx/Sources/iron/culling/CullSystem.hx | 773 ++++++++++++++++++ leenkx/Sources/iron/culling/CullTier.hx | 9 + leenkx/Sources/iron/culling/HiZPyramid.hx | 171 ++++ leenkx/Sources/iron/culling/JobSystem.hx | 60 ++ leenkx/Sources/iron/culling/ObjectBVH.hx | 376 +++++++++ leenkx/Sources/iron/culling/OccluderProxy.hx | 71 ++ leenkx/Sources/iron/culling/SimdOps.hx | 83 ++ .../culling/SoftwareOcclusionRasterizer.hx | 507 ++++++++++++ leenkx/Sources/iron/culling/TemporalState.hx | 79 ++ leenkx/Sources/iron/data/MeshBatch.hx | 31 +- leenkx/Sources/iron/data/SceneFormat.hx | 10 + leenkx/Sources/iron/object/CameraObject.hx | 37 +- leenkx/Sources/iron/object/FrustumPlane.hx | 25 + leenkx/Sources/iron/object/LightObject.hx | 12 +- leenkx/Sources/iron/object/MeshObject.hx | 85 +- leenkx/Sources/iron/system/LnxPack.hx | 1 + .../leenkx/renderpath/RenderPathDeferred.hx | 40 +- leenkx/blender/lnx/exporter.py | 226 ++++- leenkx/blender/lnx/live_patch.py | 10 +- leenkx/blender/lnx/make_renderpath.py | 6 + leenkx/blender/lnx/props.py | 10 + leenkx/blender/lnx/props_renderpath.py | 54 +- leenkx/blender/lnx/props_ui.py | 23 +- leenkx/blender/lnx/write_data.py | 11 + lib/radix/LICENSE_1_0.txt | 23 + lib/radix/README.md | 23 + lib/radix/Sources/radix/Radix.hx | 350 ++++++++ lib/radix/haxelib.json | 11 + 33 files changed, 3629 insertions(+), 84 deletions(-) create mode 100644 leenkx/Sources/iron/culling/BVHNode.hx create mode 100644 leenkx/Sources/iron/culling/ContributionCull.hx create mode 100644 leenkx/Sources/iron/culling/CullData.hx create mode 100644 leenkx/Sources/iron/culling/CullSystem.hx create mode 100644 leenkx/Sources/iron/culling/CullTier.hx create mode 100644 leenkx/Sources/iron/culling/HiZPyramid.hx create mode 100644 leenkx/Sources/iron/culling/JobSystem.hx create mode 100644 leenkx/Sources/iron/culling/ObjectBVH.hx create mode 100644 leenkx/Sources/iron/culling/OccluderProxy.hx create mode 100644 leenkx/Sources/iron/culling/SimdOps.hx create mode 100644 leenkx/Sources/iron/culling/SoftwareOcclusionRasterizer.hx create mode 100644 leenkx/Sources/iron/culling/TemporalState.hx create mode 100644 leenkx/Sources/iron/object/FrustumPlane.hx create mode 100644 lib/radix/LICENSE_1_0.txt create mode 100644 lib/radix/README.md create mode 100644 lib/radix/Sources/radix/Radix.hx create mode 100644 lib/radix/haxelib.json diff --git a/leenkx/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/leenkx/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index c8f71976..3f5adbd1 100644 --- a/leenkx/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/leenkx/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -81,11 +81,11 @@ void main() { vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0); float ior = unpackIOR(gr.x); float transmittance = gr.y; - float surfaceDepth = gr.z; + float surfaceDepth = textureLod(gbufferD, texCoord, 0.0).r; float d = surfaceDepth * 2.0 - 1.0; vec4 sceneSample = textureLod(tex, texCoord, 0.0); - if (surfaceDepth == 0.0 || surfaceDepth == 1.0) { + if (gr.z == 0.0 || surfaceDepth == 1.0) { fragColor = sceneSample; return; } diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index 28b148b2..f90f5101 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -18,6 +18,13 @@ import iron.object.LightObject; import iron.object.MeshObject; import iron.object.Uniforms; import iron.object.Clipmap; +#if lnx_radix +import haxe.io.FPHelper; +import radix.Radix; +#end +#if lnx_occlusion_culling +import iron.culling.CullSystem; +#end #if lnx_vr import iron.math.Vec4; import iron.math.Mat4; @@ -50,10 +57,6 @@ class RenderPath { return vrSimulateMode; } - // TODO: done remove safely - public static inline function debugLog(msg: String, once: Bool = true): Void { - return; - } #end public var frameScissor = false; @@ -81,6 +84,18 @@ class RenderPath { var renderToXRFramebuffer = false; #end public var drawOrder = DrawOrder.Distance; + #if lnx_frustum_culling + public var frustumCulling = true; + #else + public var frustumCulling = false; + #end + + #if lnx_occlusion_culling + public var occlusionCulling = true; + #else + public var occlusionCulling = false; + #end + public var paused = false; public var ready(get, never): Bool; function get_ready(): Bool { return loading == 0; } @@ -95,6 +110,8 @@ class RenderPath { var lastH = 0; var bindParams: Array; var meshesSorted: Bool; + var cullComputed: Bool; + var cullFlagsReset: Bool; var scissorSet = false; var viewportScaled = false; var lastFrameTime = 0.0; @@ -304,6 +321,8 @@ class RenderPath { currentD = 1; currentFace = -1; meshesSorted = false; + cullComputed = false; + cullFlagsReset = false; sun = null; for (l in Scene.active.lights) { @@ -470,6 +489,60 @@ class RenderPath { rt.image.generateMipmaps(1000); } +#if lnx_radix + static var sortDistanceKeys = new Array(); + static var sortDistanceMeshesBuf = new Array(); + static var sortDistanceKeysBuf = new Array(); + + static var sortIndexKeys = new Array(); + static var sortIndexMeshesBuf = new Array(); + static var sortIndexKeysBuf = new Array(); + + public static function sortMeshesDistance(meshes: Array) { + var n = meshes.length; + var keys = sortDistanceKeys; + var meshesBuf = sortDistanceMeshesBuf; + var keysBuf = sortDistanceKeysBuf; + if (keys.length < n) { + keys.resize(n); + meshesBuf.resize(n); + keysBuf.resize(n); + } + for (i in 0...n) { + var m = meshes[i]; + var key = FPHelper.floatToI32(m.cameraDistance) >>> 1; + #if rp_depth_texture + if (m.depthRead) key |= 0x80000000; + #end + keys[i] = key; + } + Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf); + if (meshesBuf.length > n) meshesBuf.resize(n); + } + + public static function sortMeshesIndex(meshes: Array) { + var n = meshes.length; + var keys = sortIndexKeys; + var meshesBuf = sortIndexMeshesBuf; + var keysBuf = sortIndexKeysBuf; + if (keys.length < n) { + keys.resize(n); + meshesBuf.resize(n); + keysBuf.resize(n); + } + for (i in 0...n) { + var m = meshes[i]; + var si = m.data.sortingIndex; + var key = si < -0x40000000 ? 0 : si > 0x3fffffff ? 0x7fffffff : si + 0x40000000; + #if rp_depth_texture + if (m.depthRead) key |= 0x80000000; + #end + keys[i] = key; + } + Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf); + if (meshesBuf.length > n) meshesBuf.resize(n); + } + #else static inline function boolToInt(b: Bool): Int { return b ? 1 : 0; } @@ -501,6 +574,7 @@ class RenderPath { return a.data.name > b.data.name ? 1 : -1; }); } + #end public function drawMeshes(context: String) { var isShadows = context == "shadowmap" || context == "shadowmap_transparent"; @@ -519,6 +593,11 @@ class RenderPath { for (i in 0...LightObject.cascadeCount) { light.setCascade(Scene.active.camera, i); currentG.viewport(i * step, 0, step, step); + #if lnx_occlusion_culling + if (occlusionCulling && CullSystem.active != null) { + CullSystem.active.cullShadow(light); + } + #end submitDraw(context); } drawn = true; @@ -529,7 +608,14 @@ class RenderPath { if (context == "mesh") LightObject.updateClusters(Scene.active.camera); #end - if (!drawn) submitDraw(context); + if (!drawn) { + #if lnx_occlusion_culling + if (isShadows && occlusionCulling && CullSystem.active != null) { + CullSystem.active.cullShadow(light); + } + #end + submitDraw(context); + } // Callbacks to specific context if (contextEvents != null) { @@ -546,7 +632,34 @@ class RenderPath { var meshes = Scene.active.meshes; MeshObject.lastPipeline = null; - if (!meshesSorted && camera != null) { // Order max once per frame for now + #if lnx_occlusion_culling + if (!cullFlagsReset && !occlusionCulling) { + cullFlagsReset = true; + for (m in meshes) { + m.occlusionVisible = true; + m.shadowOcclusionVisible = true; + } + } + #end + + if (!cullComputed && (context == "mesh" || context == "depth")) { + #if lnx_occlusion_culling + if (occlusionCulling) { + if (CullSystem.active == null) CullSystem.init(); + if (CullSystem.active != null && camera != null) { + if (CullSystem.active.proxiesDirty) { + CullSystem.active.loadOccluderProxies(); + CullSystem.active.proxiesDirty = false; + } + CullSystem.active.cull(camera); + } + } + #end + cullComputed = true; + } + + var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent"; + if (!meshesSorted && camera != null && !isShadowCtx) { var camX = camera.transform.worldx(); var camY = camera.transform.worldy(); var camZ = camera.transform.worldz(); @@ -571,8 +684,20 @@ class RenderPath { static inline function meshRenderLoop(g: Graphics, context: String, _bindParams: Array, _meshes: Array) { var isReadingDepth = false; + #if lnx_occlusion_culling + var cullSys = CullSystem.active; + #end for (m in _meshes) { + #if lnx_occlusion_culling + if (cullSys != null && !m.occlusionVisible && (context == "mesh" || context == "depth" || context == "translucent")) { + continue; + } + if (cullSys != null && !m.shadowOcclusionVisible && (context == "shadowmap" || context == "shadowmap_transparent")) { + continue; + } + #end + #if rp_depth_texture // First mesh that reads depth if (!isReadingDepth && m.depthRead) { diff --git a/leenkx/Sources/iron/culling/BVHNode.hx b/leenkx/Sources/iron/culling/BVHNode.hx new file mode 100644 index 00000000..be404adf --- /dev/null +++ b/leenkx/Sources/iron/culling/BVHNode.hx @@ -0,0 +1,20 @@ +package iron.culling; + +class BVHNode { + public var minX: Float = 0; + public var minY: Float = 0; + public var minZ: Float = 0; + public var maxX: Float = 0; + public var maxY: Float = 0; + public var maxZ: Float = 0; + public var centerX: Float = 0; + public var centerY: Float = 0; + public var centerZ: Float = 0; + public var isLeaf: Bool = false; + public var leftChild: Int = -1; + public var rightChild: Int = -1; + public var firstObject: Int = 0; + public var objectCount: Int = 0; + + public function new() {} +} diff --git a/leenkx/Sources/iron/culling/ContributionCull.hx b/leenkx/Sources/iron/culling/ContributionCull.hx new file mode 100644 index 00000000..d77040e2 --- /dev/null +++ b/leenkx/Sources/iron/culling/ContributionCull.hx @@ -0,0 +1,62 @@ +package iron.culling; + +class ContributionCull { + + public static var minPixelRadius: Float = 2.0; + public static var enabled: Bool = false; + + public static function passes( + centerX: Float, centerY: Float, centerZ: Float, + radius: Float, + camX: Float, camY: Float, camZ: Float, + focalLengthPx: Float + ): Bool { + if (!enabled || minPixelRadius <= 0.0 || focalLengthPx <= 0.0) return true; + + var dx = centerX - camX; + var dy = centerY - camY; + var dz = centerZ - camZ; + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= radius) return true; + + var projectedRadiusPx = focalLengthPx * radius / dist; + return projectedRadiusPx >= minPixelRadius; + } + + public static function passesNode( + node: BVHNode, + camX: Float, camY: Float, camZ: Float, + focalLengthPx: Float + ): Bool { + if (!enabled || minPixelRadius <= 0.0 || focalLengthPx <= 0.0) return true; + + var cx = (node.minX + node.maxX) * 0.5; + var cy = (node.minY + node.maxY) * 0.5; + var cz = (node.minZ + node.maxZ) * 0.5; + var ex = (node.maxX - node.minX) * 0.5; + var ey = (node.maxY - node.minY) * 0.5; + var ez = (node.maxZ - node.minZ) * 0.5; + var radius = Math.sqrt(ex * ex + ey * ey + ez * ez); + + if (camX >= node.minX && camX <= node.maxX && + camY >= node.minY && camY <= node.maxY && + camZ >= node.minZ && camZ <= node.maxZ) { + return true; + } + + var dx = cx - camX; + var dy = cy - camY; + var dz = cz - camZ; + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= radius) return true; + + var projectedRadiusPx = focalLengthPx * radius / dist; + return projectedRadiusPx >= minPixelRadius; + } + + public static function computeFocalLengthPx(fov: Float, screenHeight: Int): Float { + return screenHeight / (2.0 * Math.tan(fov * 0.5)); + } +} diff --git a/leenkx/Sources/iron/culling/CullData.hx b/leenkx/Sources/iron/culling/CullData.hx new file mode 100644 index 00000000..304310d5 --- /dev/null +++ b/leenkx/Sources/iron/culling/CullData.hx @@ -0,0 +1,373 @@ +package iron.culling; + +import iron.object.MeshObject; +import iron.Scene; +import kha.arrays.Float32Array; + +class CullData { + + static inline var STRIDE = 16; + static inline var BASIS_STRIDE = 12; + + public var data: Float32Array; + public var prevData: Float32Array; + public var uidData: kha.arrays.Int32Array; + public var prevUidData: kha.arrays.Int32Array; + var basisData: Float32Array; + var prevBasisData: Float32Array; + public var count: Int = 0; + public var prevCount: Int = 0; + public var anyMoved: Bool = false; + public var anyFlagsChanged: Bool = false; + public var orderChanged: Bool = false; + + public var meshes: Array = []; + public var slotsDirty: Bool = true; + public var slotsRebuilt: Bool = false; + + public var visibleList: Array; + public var visibleCount: Int = 0; + + var staticFlagsCache: haxe.ds.IntMap = new haxe.ds.IntMap(); + + var matTranspCache: haxe.ds.IntMap = new haxe.ds.IntMap(); + var matSnapshots: haxe.ds.IntMap> = new haxe.ds.IntMap(); + + var prevUidMap: haxe.ds.IntMap = new haxe.ds.IntMap(); + + public function new() { + visibleList = []; + } + + public inline function stride(): Int { + return STRIDE; + } + + public function ensureCapacity(cap: Int) { + if (data == null || data.length < cap * STRIDE) { + var newCap = cap; + if (data != null) newCap = Std.int(Math.max(cap, data.length / STRIDE * 2)); + var oldData = data; + var oldPrev = prevData; + var oldUid = uidData; + var oldPrevUid = prevUidData; + var oldBasis = basisData; + var oldPrevBasis = prevBasisData; + data = new Float32Array(newCap * STRIDE); + prevData = new Float32Array(newCap * STRIDE); + uidData = new kha.arrays.Int32Array(newCap); + prevUidData = new kha.arrays.Int32Array(newCap); + basisData = new Float32Array(newCap * BASIS_STRIDE); + prevBasisData = new Float32Array(newCap * BASIS_STRIDE); + if (oldData != null) { + var copyLen = oldData.length; + for (i in 0...copyLen) data[i] = oldData[i]; + } + if (oldPrev != null) { + var copyLen = oldPrev.length; + for (i in 0...copyLen) prevData[i] = oldPrev[i]; + } + if (oldUid != null) { + var copyLen = oldUid.length; + for (i in 0...copyLen) uidData[i] = oldUid[i]; + } + if (oldPrevUid != null) { + var copyLen = oldPrevUid.length; + for (i in 0...copyLen) prevUidData[i] = oldPrevUid[i]; + } + if (oldBasis != null) { + var copyLen = oldBasis.length; + for (i in 0...copyLen) basisData[i] = oldBasis[i]; + } + if (oldPrevBasis != null) { + var copyLen = oldPrevBasis.length; + for (i in 0...copyLen) prevBasisData[i] = oldPrevBasis[i]; + } + } + } + + public function syncFromScene() { + var sceneMeshes = Scene.active.meshes; + slotsRebuilt = false; + if (slotsDirty || meshes.length != sceneMeshes.length) { + meshes = sceneMeshes.copy(); + slotsDirty = false; + slotsRebuilt = true; + staticFlagsCache.clear(); + matTranspCache.clear(); + matSnapshots.clear(); + } + count = meshes.length; + ensureCapacity(count); + + var tmpD = prevData; prevData = data; data = tmpD; + var tmpU = prevUidData; prevUidData = uidData; uidData = tmpU; + var tmpB = prevBasisData; prevBasisData = basisData; basisData = tmpB; + + prevUidMap.clear(); + var prevUidMapBuilt = false; + anyMoved = false; + anyFlagsChanged = false; + orderChanged = false; + + for (i in 0...count) { + var mesh = meshes[i]; + var t = mesh.transform; + var uid = mesh.uid; + var p = mesh.parent; + while (p != null) { + p.transform.update(); + p = p.parent; + } + t.update(); + + var off = i * STRIDE; + var r = t.radius; + var wx = t.worldx(); + var wy = t.worldy(); + var wz = t.worldz(); + + var radiusScale = mesh.data.isSkinned ? 2.0 : 1.0; + #if lnx_gpu_particles + if (mesh.particleSystems != null || mesh.particleOwner != null) radiusScale *= 1000; + #end + if (mesh.data.geom.instanced) radiusScale *= 100; + r *= radiusScale; + + data[off + 3] = r; + data[off + 4] = wx; + data[off + 5] = wy; + data[off + 6] = wz; + + var w = t.world; + + var prevIdx: Null = null; + if (prevUidData != null && i < prevCount && prevUidData[i] == uid) { + prevIdx = i; + } else { + if (!prevUidMapBuilt) { + prevUidMapBuilt = true; + if (prevUidData != null) { + for (pi in 0...prevCount) prevUidMap.set(prevUidData[pi], pi); + } + } + prevIdx = prevUidMap.get(uid); + } + + if (prevIdx != null && prevIdx != i) orderChanged = true; + var aabbCached = false; + if (prevIdx != null) { + var poff = prevIdx * STRIDE; + var pb = prevIdx * BASIS_STRIDE; + if (prevData[poff + 4] == wx && prevData[poff + 5] == wy && prevData[poff + 6] == wz && + prevBasisData[pb + 0] == w._00 && prevBasisData[pb + 1] == w._10 && prevBasisData[pb + 2] == w._20 && + prevBasisData[pb + 3] == w._01 && prevBasisData[pb + 4] == w._11 && prevBasisData[pb + 5] == w._21 && + prevBasisData[pb + 6] == w._02 && prevBasisData[pb + 7] == w._12 && prevBasisData[pb + 8] == w._22 && + prevBasisData[pb + 9] == t.dim.x && prevBasisData[pb + 10] == t.dim.y && prevBasisData[pb + 11] == t.dim.z) { + data[off + 0] = prevData[poff + 0]; + data[off + 1] = prevData[poff + 1]; + data[off + 2] = prevData[poff + 2]; + data[off + 8] = prevData[poff + 8]; + data[off + 9] = prevData[poff + 9]; + data[off + 10] = prevData[poff + 10]; + aabbCached = true; + } + } + + if (!aabbCached) { + var r00 = w._00, r10 = w._10, r20 = w._20; + var r01 = w._01, r11 = w._11, r21 = w._21; + var r02 = w._02, r12 = w._12, r22 = w._22; + var lenX = Math.sqrt(r00 * r00 + r01 * r01 + r02 * r02); + var lenY = Math.sqrt(r10 * r10 + r11 * r11 + r12 * r12); + var lenZ = Math.sqrt(r20 * r20 + r21 * r21 + r22 * r22); + if (lenX > 1e-8) { r00 /= lenX; r01 /= lenX; r02 /= lenX; } + if (lenY > 1e-8) { r10 /= lenY; r11 /= lenY; r12 /= lenY; } + if (lenZ > 1e-8) { r20 /= lenZ; r21 /= lenZ; r22 /= lenZ; } + + var hx = t.dim.x * 0.5 * radiusScale; + var hy = t.dim.y * 0.5 * radiusScale; + var hz = t.dim.z * 0.5 * radiusScale; + + var aabbMinX = 1e20, aabbMinY = 1e20, aabbMinZ = 1e20; + var aabbMaxX = -1e20, aabbMaxY = -1e20, aabbMaxZ = -1e20; + for (c in 0...8) { + var cx = (c & 1) != 0 ? hx : -hx; + var cy = (c & 2) != 0 ? hy : -hy; + var cz = (c & 4) != 0 ? hz : -hz; + var px = cx * r00 + cy * r10 + cz * r20 + wx; + var py = cx * r01 + cy * r11 + cz * r21 + wy; + var pz = cx * r02 + cy * r12 + cz * r22 + wz; + if (px < aabbMinX) aabbMinX = px; + if (py < aabbMinY) aabbMinY = py; + if (pz < aabbMinZ) aabbMinZ = pz; + if (px > aabbMaxX) aabbMaxX = px; + if (py > aabbMaxY) aabbMaxY = py; + if (pz > aabbMaxZ) aabbMaxZ = pz; + } + + data[off + 0] = aabbMinX; + data[off + 1] = aabbMinY; + data[off + 2] = aabbMinZ; + + data[off + 8] = aabbMaxX; + data[off + 9] = aabbMaxY; + data[off + 10] = aabbMaxZ; + } + + var flags: Int; + var cachedFlags = staticFlagsCache.get(uid); + if (cachedFlags != null) { + flags = cachedFlags; + } else { + flags = 0; + if (mesh.raw != null && mesh.raw.properties != null) { + for (prop in mesh.raw.properties) { + if (prop.name == "occluder" && prop.value == true) { + flags |= 1; + break; + } + } + } + if ((flags & 1) == 0 && mesh.data != null && + mesh.data.raw != null && mesh.data.raw.occluder_proxy != null) { + flags |= 1; + } + if (mesh.raw != null && mesh.raw.visible_shadow != false) flags |= 4; + staticFlagsCache.set(uid, flags); + } + var mats = mesh.materials; + var snap = matSnapshots.get(uid); + var matsChanged = snap == null || mats == null || snap.length != mats.length; + if (!matsChanged) { + for (mi in 0...mats.length) { + if (snap[mi] != mats[mi]) { matsChanged = true; break; } + } + } + if (matsChanged) { + var tbit = 0; + if (mats != null) { + for (mi in 0...mats.length) { + var mat = mats[mi]; + if (mat == null || mat.shader == null) continue; + for (sc in mat.shader.contexts) { + var sr = sc.raw; + if (sr != null && sr.name != null && sr.name.substr(0, 4) == "mesh" && + (sr.depth_write != true || sr.blend_source != null)) { + tbit = 32; + break; + } + } + if (tbit != 0) break; + } + } + matTranspCache.set(uid, tbit); + var nsnap = new haxe.ds.Vector(mats != null ? mats.length : 0); + for (mi in 0...nsnap.length) nsnap[mi] = mats[mi]; + matSnapshots.set(uid, nsnap); + } + var tbitCached = matTranspCache.get(uid); + if (tbitCached != null) flags |= tbitCached; + if (mesh.visible) flags |= 8; + if (radiusScale != 1.0) flags |= 16; + if (prevIdx != null) { + var poff = prevIdx * STRIDE; + if (prevData[poff + 0] != data[off + 0] || + prevData[poff + 1] != data[off + 1] || + prevData[poff + 2] != data[off + 2] || + prevData[poff + 8] != data[off + 8] || + prevData[poff + 9] != data[off + 9] || + prevData[poff + 10] != data[off + 10]) { + flags |= 2; + anyMoved = true; + } + } else { + flags |= 2; + anyMoved = true; + anyFlagsChanged = true; + } + if (prevIdx != null) { + var poff = prevIdx * STRIDE; + if (((flags ^ Std.int(prevData[poff + 7])) & ~2) != 0) anyFlagsChanged = true; + } + data[off + 7] = flags; + + var boff = i * BASIS_STRIDE; + basisData[boff + 0] = w._00; + basisData[boff + 1] = w._10; + basisData[boff + 2] = w._20; + basisData[boff + 3] = w._01; + basisData[boff + 4] = w._11; + basisData[boff + 5] = w._21; + basisData[boff + 6] = w._02; + basisData[boff + 7] = w._12; + basisData[boff + 8] = w._22; + basisData[boff + 9] = t.dim.x; + basisData[boff + 10] = t.dim.y; + basisData[boff + 11] = t.dim.z; + uidData[i] = uid; + } + + prevCount = count; + } + + public inline function getMinX(i: Int): Float { + return data[i * STRIDE + 0]; + } + public inline function getMinY(i: Int): Float { + return data[i * STRIDE + 1]; + } + public inline function getMinZ(i: Int): Float { + return data[i * STRIDE + 2]; + } + public inline function getRadius(i: Int): Float { + return data[i * STRIDE + 3]; + } + public inline function getCenterX(i: Int): Float { + return data[i * STRIDE + 4]; + } + public inline function getCenterY(i: Int): Float { + return data[i * STRIDE + 5]; + } + public inline function getCenterZ(i: Int): Float { + return data[i * STRIDE + 6]; + } + public inline function getFlags(i: Int): Int { + return Std.int(data[i * STRIDE + 7]); + } + public inline function isOccluder(i: Int): Bool { + return (getFlags(i) & 1) != 0; + } + public inline function isDynamic(i: Int): Bool { + return (getFlags(i) & 2) != 0; + } + public inline function isShadowCaster(i: Int): Bool { + return (getFlags(i) & 4) != 0; + } + public inline function isMeshVisible(i: Int): Bool { + return (getFlags(i) & 8) != 0; + } + public inline function getObjectID(i: Int): Int { + return uidData[i]; + } + + public inline function getMaxX(i: Int): Float { + return data[i * STRIDE + 8]; + } + public inline function getMaxY(i: Int): Float { + return data[i * STRIDE + 9]; + } + public inline function getMaxZ(i: Int): Float { + return data[i * STRIDE + 10]; + } + + public function clearVisible() { + visibleCount = 0; + if (visibleList.length > 0) visibleList.resize(0); + } + + public function addVisible(objectIndex: Int) { + visibleList.push(objectIndex); + visibleCount++; + } +} diff --git a/leenkx/Sources/iron/culling/CullSystem.hx b/leenkx/Sources/iron/culling/CullSystem.hx new file mode 100644 index 00000000..3b390902 --- /dev/null +++ b/leenkx/Sources/iron/culling/CullSystem.hx @@ -0,0 +1,773 @@ +package iron.culling; + +import iron.object.CameraObject; +import iron.object.FrustumPlane; +import iron.Scene; +import iron.RenderPath; +import iron.math.Mat4; + +class CullSystem { + + public static var active: CullSystem = null; + + public var tier: CullTier; + public var cullData: CullData; + public var rasterizer: SoftwareOcclusionRasterizer; + public var shadowRasterizer: SoftwareOcclusionRasterizer; + public var occluderProxies: Array; + public var bvh: ObjectBVH; + public var temporal: TemporalState; + + public var enabled: Bool = true; + public var occlusionEnabled: Bool = true; + public var bvhDirty: Bool = true; + public var proxiesDirty: Bool = true; + var dataSyncedFrame: Int = -1; + + var focalLengthPx: Float = 500.0; + var orthoPxPerUnit: Float = 0.0; + var screenW: Int = 1920; + var screenH: Int = 1080; + + public static var maxOccluders: Int = 48; + public static var maxOccluderTris: Int = 4000; + public static var maxTrisPerProxy: Int = 768; + public static var maxShadowOccluders: Int = 24; + public static var maxShadowOccluderTris: Int = 1536; + public static var boxOccluders: Bool = false; + public static var shadowBoxOccluders: Bool = false; + public static var shadowBoxShrink: Float = 0.7; + public static var shadowCullingEnabled: Bool = true; + public static var minOccluderPx: Float = 24.0; + public static var boxShrink: Float = 0.5; + public static var depthEps: Float = 1e-6; + + public static var shadowCullInterval: Int = 2; + + static inline var MAX_SHADOW_SLOTS = 8; + var shadowPasses: haxe.ds.ObjectMap> = new haxe.ds.ObjectMap(); + var shadowCallIdx: Map = new Map(); + var shadowCallFrame: Map = new Map(); + var lastShadowSlot: ShadowPassSlot = null; + var lastShadowFrame: Int = -1; + var lastShadowCallIdx: Int = -1; + #if lnx_csm + var shadowVps: haxe.ds.ObjectMap = new haxe.ds.ObjectMap(); + #end + var sceneRev: Int = 0; + var sceneRevFrame: Int = -1; + var frameCounter: Int = 0; + var bvhSyncedFrame: Int = -1; // Frame when BVH was built/refit + + var occlusionFrustumPlanes: Array = null; + var frustumPlanes: Array = null; + + var shadowFrustumPlanes: Array = null; + + var uidToProxy: haxe.ds.IntMap = new haxe.ds.IntMap(); + var uidScan: haxe.ds.IntMap = new haxe.ds.IntMap(); + + var prevVP: Mat4 = null; + #if lnx_taa + var unjitteredVP: Mat4 = null; + #end + public var shadowFlagsValid: Bool = false; + + var inFrustumList: Array = []; + var selIdx: Array = []; + var selScore: Array = []; + var selCount: Int = 0; + + public function new() { + cullData = new CullData(); + rasterizer = new SoftwareOcclusionRasterizer(128, 64); + shadowRasterizer = new SoftwareOcclusionRasterizer(128, 128); + occluderProxies = []; + temporal = new TemporalState(); + tier = detectTier(); + } + + static inline var MAT_EPS = 1e-7; + + static inline function matEquals(a: Mat4, b: Mat4): Bool { + return + Math.abs(a._00 - b._00) < MAT_EPS && Math.abs(a._10 - b._10) < MAT_EPS && + Math.abs(a._20 - b._20) < MAT_EPS && Math.abs(a._30 - b._30) < MAT_EPS && + Math.abs(a._01 - b._01) < MAT_EPS && Math.abs(a._11 - b._11) < MAT_EPS && + Math.abs(a._21 - b._21) < MAT_EPS && Math.abs(a._31 - b._31) < MAT_EPS && + Math.abs(a._02 - b._02) < MAT_EPS && Math.abs(a._12 - b._12) < MAT_EPS && + Math.abs(a._22 - b._22) < MAT_EPS && Math.abs(a._32 - b._32) < MAT_EPS && + Math.abs(a._03 - b._03) < MAT_EPS && Math.abs(a._13 - b._13) < MAT_EPS && + Math.abs(a._23 - b._23) < MAT_EPS && Math.abs(a._33 - b._33) < MAT_EPS; + } + + public static function detectTier(): CullTier { + #if (target.threaded) + return CPUMultiThread; + #else + return CPUSingleThread; + #end + } + + public function cull(camera: CameraObject) { + frameCounter = RenderPath.active != null ? RenderPath.active.frame : frameCounter + 1; + if (!enabled || Scene.active == null || Scene.active.meshes.length == 0) { + if (Scene.active != null) { + var meshes = Scene.active.meshes; + for (i in 0...meshes.length) { + meshes[i].occlusionVisible = true; + meshes[i].shadowOcclusionVisible = true; + } + } + return; + } + + var rp = RenderPath.active; + if (rp != null) { + screenW = rp.currentW; + screenH = rp.currentH; + } + + var targetW = Std.int(Math.max(128, Math.min(512, screenW >> 4))); + var targetH = Std.int(Math.max(64, Math.min(256, screenH >> 4))); + if (rasterizer.width < (targetW >> 1) || rasterizer.width > (targetW << 1) || + rasterizer.height < (targetH >> 1) || rasterizer.height > (targetH << 1)) { + rasterizer = new SoftwareOcclusionRasterizer(targetW, targetH); + } + + if (camera.data.raw.ortho == null) { + focalLengthPx = ContributionCull.computeFocalLengthPx( + camera.data.raw.fov, screenH + ); + orthoPxPerUnit = 0.0; + } else { + focalLengthPx = 0; + var o = camera.data.raw.ortho; + var halfH = (o[3] - o[2]) * 0.5; + orthoPxPerUnit = halfH > 1e-9 ? screenH * 0.5 / halfH : 0.0; + } + rasterizer.nearClip = camera.data.raw.ortho == null ? + camera.data.raw.near_plane : 0.0001; + + if (dataSyncedFrame != frameCounter) { + cullData.syncFromScene(); + dataSyncedFrame = frameCounter; + } + if (cullData.slotsRebuilt) temporal.clear(); + if (sceneRevFrame != frameCounter) { + sceneRevFrame = frameCounter; + if (cullData.anyMoved || cullData.anyFlagsChanged || cullData.orderChanged) sceneRev++; + } + var cd = cullData; + var meshes = cd.meshes; + + #if lnx_taa + if (unjitteredVP == null) unjitteredVP = Mat4.identity(); + unjitteredVP.multmats(camera.noJitterP, camera.V); + var cmpVP = unjitteredVP; + #else + var cmpVP = camera.VP; + #end + var vpSame = prevVP != null && matEquals(cmpVP, prevVP); + if (vpSame && !cd.anyMoved && !cd.anyFlagsChanged && !cd.orderChanged && !bvhDirty) { + return; + } + var needBvhRebuild = bvhDirty; + bvhDirty = false; + if (prevVP == null) prevVP = Mat4.identity(); + prevVP.setFrom(cmpVP); + + if (occlusionFrustumPlanes == null) { + occlusionFrustumPlanes = []; + for (i in 0...6) occlusionFrustumPlanes.push(new FrustumPlane()); + } + { + var vp = camera.VP; + var p = occlusionFrustumPlanes; + p[0].setComponents(vp._03 + vp._00, vp._13 + vp._10, vp._23 + vp._20, vp._33 + vp._30); + p[1].setComponents(vp._03 - vp._00, vp._13 - vp._10, vp._23 - vp._20, vp._33 - vp._30); + p[2].setComponents(vp._03 - vp._01, vp._13 - vp._11, vp._23 - vp._21, vp._33 - vp._31); + p[3].setComponents(vp._03 + vp._01, vp._13 + vp._11, vp._23 + vp._21, vp._33 + vp._31); + p[4].setComponents(vp._02, vp._12, vp._22, vp._32); + p[5].setComponents(vp._03 - vp._02, vp._13 - vp._12, vp._23 - vp._22, vp._33 - vp._32); + } + frustumPlanes = occlusionFrustumPlanes; + + var camX = camera.transform.worldx(); + var camY = camera.transform.worldy(); + var camZ = camera.transform.worldz(); + + if (bvh == null) bvh = new ObjectBVH(); + if (bvhSyncedFrame != frameCounter) { + if (needBvhRebuild || bvh.root < 0) { + bvh.build(cd); + } else if (cd.anyMoved || cd.orderChanged) { + bvh.refit(cd); + } + bvhSyncedFrame = frameCounter; + } + inFrustumList.resize(0); + var planes = frustumPlanes; + var fpx = focalLengthPx; + bvh.traverseFrontToBack(camX, camY, camZ, + function(node: BVHNode): Bool { + if (!ObjectBVH.aabbInFrustum(node, planes)) return true; + if (!ContributionCull.passesNode(node, camX, camY, camZ, fpx)) return true; + return false; + }, + function(objIdx: Int, _: Int) { + if ((cd.getFlags(objIdx) & 8) == 0) return; + if (!ContributionCull.passes( + cd.getCenterX(objIdx), cd.getCenterY(objIdx), cd.getCenterZ(objIdx), + cd.getRadius(objIdx), camX, camY, camZ, fpx + )) return; + inFrustumList.push(objIdx); + } + ); + cullData.clearVisible(); + + if (occlusionEnabled) { + selectOccluders(meshes, camX, camY, camZ); + + for (i in 0...meshes.length) { + meshes[i].occlusionVisible = true; + } + + rasterizer.clear(); + if (selCount > 0) rasterizeSelected(meshes, camera.VP); + var trisDone = rasterizer.trianglesRasterized; + if (trisDone > 0) rasterizer.buildHiZ(); + + if (trisDone > 0) { + testVisibilityBVH(camera.VP, camX, camY, camZ, planes); + } else { + for (k in 0...inFrustumList.length) { + var objIdx = inFrustumList[k]; + cd.addVisible(objIdx); + } + } + } else { + for (i in 0...meshes.length) { + meshes[i].occlusionVisible = true; + } + for (k in 0...inFrustumList.length) { + var objIdx = inFrustumList[k]; + cd.addVisible(objIdx); + } + } + + temporal.advanceFrame(); + } + + public function cullShadow(light: iron.object.LightObject) { + if (!occlusionEnabled || light == null || Scene.active == null) return; + if (!shadowCullingEnabled) { + var ms = Scene.active.meshes; + for (m in ms) m.shadowOcclusionVisible = true; + return; + } + + var frame = RenderPath.active != null ? RenderPath.active.frame : frameCounter; + var vp = light.VP; + if (vp == null) return; + + var cd = cullData; + + if (dataSyncedFrame != frame) { + cd.syncFromScene(); + dataSyncedFrame = frame; + } + if (sceneRevFrame != frame) { + sceneRevFrame = frame; + if (cd.anyMoved || cd.anyFlagsChanged || cd.orderChanged) sceneRev++; + } + var meshes = cd.meshes; + + var slots = shadowPasses.get(light); + if (slots == null) { + slots = []; + shadowPasses.set(light, slots); + } + var callIdx = 0; + { + var f = shadowCallFrame.get(light); + callIdx = (f == frame) ? shadowCallIdx.get(light) + 1 : 0; + shadowCallIdx.set(light, callIdx); + shadowCallFrame.set(light, frame); + } + var slotIdx = callIdx < MAX_SHADOW_SLOTS ? callIdx : MAX_SHADOW_SLOTS - 1; + while (slots.length <= slotIdx) slots.push(new ShadowPassSlot()); + var slot = slots[slotIdx]; + + if (slot != null && slot == lastShadowSlot && lastShadowFrame == frame && + lastShadowCallIdx == callIdx && slot.appliedRev == sceneRev) return; + + for (i in 0...meshes.length) { + meshes[i].shadowOcclusionVisible = true; + } + + #if lnx_csm + var isCsmLight = light.data.raw.type == "sun" && + iron.object.LightObject.cascadeCount > 1; + if (isCsmLight && callIdx == 0) { + var vp0 = shadowVps.get(light); + if (vp0 == null) { vp0 = Mat4.identity(); shadowVps.set(light, vp0); } + vp0.setFrom(vp); + } + #end + + if (shadowCullInterval > 1) { + if ((frame + slotIdx) % shadowCullInterval != 0) { + var applied = false; + #if lnx_csm + if (isCsmLight && callIdx > 0) { + var head = slots[0]; + var vp0 = shadowVps.get(light); + if (head.appliedRev == sceneRev && head.flagsLen > 0 && + head.vp != null && vp0 != null && matEquals(vp0, head.vp)) { + var hn = head.flagsLen < meshes.length ? head.flagsLen : meshes.length; + for (i in 0...hn) meshes[i].shadowOcclusionVisible = head.flags[i]; + } + applied = true; + } + #end + if (!applied && slot.appliedRev == sceneRev && slot.flagsLen > 0 && + slot.vp != null && matEquals(vp, slot.vp)) { + var n = slot.flagsLen < meshes.length ? slot.flagsLen : meshes.length; + for (i in 0...n) meshes[i].shadowOcclusionVisible = slot.flags[i]; + } + lastShadowSlot = slot; + lastShadowFrame = frame; + lastShadowCallIdx = callIdx; + return; + } + } + + if (bvh == null) bvh = new ObjectBVH(); + if (bvhSyncedFrame != frame) { + if (bvhDirty || bvh.root < 0) { + bvh.build(cd); + } else if (cd.anyMoved || cd.orderChanged) { + bvh.refit(cd); + } + bvhSyncedFrame = frame; + } + + if (shadowFrustumPlanes == null) { + shadowFrustumPlanes = []; + for (i in 0...6) shadowFrustumPlanes.push(new FrustumPlane()); + } + { + var p = shadowFrustumPlanes; + p[0].setComponents(vp._03 + vp._00, vp._13 + vp._10, vp._23 + vp._20, vp._33 + vp._30); + p[1].setComponents(vp._03 - vp._00, vp._13 - vp._10, vp._23 - vp._20, vp._33 - vp._30); + p[2].setComponents(vp._03 - vp._01, vp._13 - vp._11, vp._23 - vp._21, vp._33 - vp._31); + p[3].setComponents(vp._03 + vp._01, vp._13 + vp._11, vp._23 + vp._21, vp._33 + vp._31); + p[4].setComponents(vp._02, vp._12, vp._22, vp._32); + p[5].setComponents(vp._03 - vp._02, vp._13 - vp._12, vp._23 - vp._22, vp._33 - vp._32); + } + var lightPlanes = shadowFrustumPlanes; + + var lx = light.transform.worldx(); + var ly = light.transform.worldy(); + var lz = light.transform.worldz(); + + var raster = slot.rasterizer; + var vpSame = slot.vp != null && matEquals(vp, slot.vp); + if (slot.sceneRev == sceneRev && vpSame && raster != null) { + // skip + } else { + if (raster == null) { + raster = new SoftwareOcclusionRasterizer( + shadowRasterizer.width, shadowRasterizer.height); + slot.rasterizer = raster; + } + if (slot.vp == null) slot.vp = Mat4.identity(); + slot.vp.setFrom(vp); + + var perspective = light.data.raw.type != "sun"; + selCount = 0; + bvh.traverseFrontToBack(lx, ly, lz, + function(node: BVHNode): Bool { + if (!ObjectBVH.aabbInFrustum(node, lightPlanes)) return true; + return false; + }, + function(i: Int, _: Int) { + if ((cd.getFlags(i) & 8) == 0) return; + if ((cd.getFlags(i) & 16) != 0) return; + if ((cd.getFlags(i) & 4) == 0) return; + if ((cd.getFlags(i) & 32) != 0) return; + if (i >= meshes.length) return; + var r = cd.getRadius(i); + var score = r; + if (perspective) { + var dx = cd.getCenterX(i) - lx; + var dy = cd.getCenterY(i) - ly; + var dz = cd.getCenterZ(i) - lz; + var dist2 = dx * dx + dy * dy + dz * dz; + if (dist2 <= r * r) return; + score = r / Math.sqrt(dist2); + } + insertTopK(i, score, maxShadowOccluders); + } + ); + + raster = slot.rasterizer; + raster.clear(); + var triBudget = maxShadowOccluderTris; + for (s in 0...selCount) { + if (triBudget < 12) break; + var i = selIdx[s]; + var proxy = uidToProxy.get(cd.getObjectID(i)); + if (proxy != null && proxy.triangleCount > 0 && + proxy.triangleCount <= maxTrisPerProxy && proxy.triangleCount <= triBudget) { + proxy.rasterize(raster, meshes[i].transform.world, vp, + raster.width, raster.height); + triBudget -= proxy.triangleCount; + } else if (shadowBoxOccluders) { + rasterizeBoxOccluder(raster, i, vp, shadowBoxShrink); + triBudget -= 12; + } + } + if (raster.trianglesRasterized > 0) raster.buildHiZ(); + } + + if (raster.trianglesRasterized > 0) { + var sw = raster.width; + var sh = raster.height; + + bvh.traverseFrontToBack(lx, ly, lz, + function(node: BVHNode): Bool { + if (!ObjectBVH.aabbInFrustum(node, lightPlanes)) return true; + var anyVisible = false; + if (node.isLeaf) { + for (j in 0...node.objectCount) { + var idx = bvh.objectIndices[node.firstObject + j]; + if ((cd.getFlags(idx) & 8) != 0 && idx < meshes.length) { + anyVisible = true; break; + } + } + if (!anyVisible) return true; + } + if (raster.isOccluded( + node.minX, node.minY, node.minZ, + node.maxX, node.maxY, node.maxZ, + vp, sw, sh + )) { + bvh.forEachLeafInSubtree(node, function(idx: Int) { + if (idx < meshes.length && (cd.getFlags(idx) & 8) != 0) { + meshes[idx].shadowOcclusionVisible = false; + } + }); + return true; + } + return false; + }, + function(objIdx: Int, _: Int) { + if ((cd.getFlags(objIdx) & 8) == 0) return; + if (objIdx >= meshes.length) return; + meshes[objIdx].shadowOcclusionVisible = true; + if (raster.isOccluded( + cd.getMinX(objIdx), cd.getMinY(objIdx), cd.getMinZ(objIdx), + cd.getMaxX(objIdx), cd.getMaxY(objIdx), cd.getMaxZ(objIdx), + vp, sw, sh + )) { + meshes[objIdx].shadowOcclusionVisible = false; + } + } + ); + } + + #if lnx_csm + if (isCsmLight) { + for (c in 1...iron.object.LightObject.cascadeCount) { + if (c >= slots.length) break; + var s = slots[c]; + if (s == null || s == slot || s.vp == null || s.rasterizer == null) continue; + for (i in 0...meshes.length) { + if (meshes[i].shadowOcclusionVisible) continue; + if (!s.rasterizer.isOccluded( + cd.getMinX(i), cd.getMinY(i), cd.getMinZ(i), + cd.getMaxX(i), cd.getMaxY(i), cd.getMaxZ(i), + s.vp, s.rasterizer.width, s.rasterizer.height + )) { + meshes[i].shadowOcclusionVisible = true; + } + } + } + } + #end + + if (slot.flags.length < meshes.length) slot.flags.resize(meshes.length); + slot.flagsLen = meshes.length; + for (i in 0...meshes.length) slot.flags[i] = meshes[i].shadowOcclusionVisible; + + #if lnx_csm + if (isCsmLight && callIdx > 0) { + var head = slots[0]; + var vp0 = shadowVps.get(light); + if (head.appliedRev == sceneRev && head.flagsLen > 0 && + head.vp != null && vp0 != null && matEquals(vp0, head.vp)) { + var hn = head.flagsLen < meshes.length ? head.flagsLen : meshes.length; + for (i in 0...hn) meshes[i].shadowOcclusionVisible = head.flags[i]; + } else { + for (i in 0...meshes.length) meshes[i].shadowOcclusionVisible = true; + } + } + #end + + slot.sceneRev = sceneRev; + slot.appliedRev = sceneRev; + slot.lastUsed = frame; + lastShadowSlot = slot; + lastShadowFrame = frame; + lastShadowCallIdx = callIdx; + shadowFlagsValid = true; + } + + function selectOccluders( + meshes: Array, + camX: Float, camY: Float, camZ: Float + ) { + var cd = cullData; + selCount = 0; + var minPx = minOccluderPx; + var autoPx = autoOccluderMinScreenRatio * screenH; + for (k in 0...inFrustumList.length) { + var i = inFrustumList[k]; + if (i >= meshes.length) continue; + if (!meshes[i].occlusionVisible) continue; + var flags = cd.getFlags(i); + if ((flags & 16) != 0) continue; + if ((flags & 32) != 0) continue; + var r = cd.getRadius(i); + var px: Float; + var camInside = false; + if (focalLengthPx > 0) { + var dx = cd.getCenterX(i) - camX; + var dy = cd.getCenterY(i) - camY; + var dz = cd.getCenterZ(i) - camZ; + var dist2 = dx * dx + dy * dy + dz * dz; + if (dist2 <= r * r) camInside = true; + px = camInside ? 1e6 : focalLengthPx * r / Math.sqrt(dist2); + } else { + px = r * orthoPxPerUnit; + } + if (px < minPx) continue; + if ((flags & 1) == 0 && px < autoPx) continue; + if (camInside || !boxOccluders) { + var p = uidToProxy.get(cd.getObjectID(i)); + if (p == null || p.triangleCount <= 0 || p.triangleCount > maxTrisPerProxy) continue; + } + insertTopK(i, px, maxOccluders); + } + } + + function insertTopK(idx: Int, score: Float, k: Int) { + if (selIdx.length < k + 1) { + selIdx.resize(k + 1); + selScore.resize(k + 1); + } + var pos = selCount; + while (pos > 0 && selScore[pos - 1] < score) pos--; + if (pos >= k) return; + var last = selCount < k ? selCount : k - 1; + var j = last; + while (j > pos) { + selIdx[j] = selIdx[j - 1]; + selScore[j] = selScore[j - 1]; + j--; + } + selIdx[pos] = idx; + selScore[pos] = score; + if (selCount < k) selCount++; + } + + function rasterizeSelected(meshes: Array, vp: Mat4) { + var cd = cullData; + var triBudget = maxOccluderTris; + #if (target.threaded) + rasterizer.beginCollect(); + #end + for (s in 0...selCount) { + if (triBudget < 12) break; + var i = selIdx[s]; + var proxy = uidToProxy.get(cd.getObjectID(i)); + if (proxy != null && proxy.triangleCount > 0 && + proxy.triangleCount <= maxTrisPerProxy && proxy.triangleCount <= triBudget) { + var mesh = i < meshes.length ? meshes[i] : null; + if (mesh == null) continue; + proxy.rasterize(rasterizer, mesh.transform.world, vp, screenW, screenH); + triBudget -= proxy.triangleCount; + } else { + if (!boxOccluders) continue; + rasterizeBoxOccluder(rasterizer, i, vp); + triBudget -= 12; + } + } + #if (target.threaded) + rasterizer.flushCollected(); + #end + } + + function rasterizeBoxOccluder(raster: SoftwareOcclusionRasterizer, objectIndex: Int, vp: Mat4, shrinkOverride = -1.0) { + var cd = cullData; + var bx0 = cd.getMinX(objectIndex), by0 = cd.getMinY(objectIndex), bz0 = cd.getMinZ(objectIndex); + var bx1 = cd.getMaxX(objectIndex), by1 = cd.getMaxY(objectIndex), bz1 = cd.getMaxZ(objectIndex); + var cx = (bx0 + bx1) * 0.5; + var cy = (by0 + by1) * 0.5; + var cz = (bz0 + bz1) * 0.5; + var boxShrink = shrinkOverride >= 0.0 ? shrinkOverride : CullSystem.boxShrink; + var hx = (bx1 - bx0) * 0.5 * boxShrink; + var hy = (by1 - by0) * 0.5 * boxShrink; + var hz = (bz1 - bz0) * 0.5 * boxShrink; + raster.rasterizeBox( + cx - hx, cy - hy, cz - hz, + cx + hx, cy + hy, cz + hz, + vp, raster.width, raster.height + ); + } + + function testVisibilityBVH( + vp: Mat4, camX: Float, camY: Float, camZ: Float, + planes: Array + ) { + var cd = cullData; + var raster = rasterizer; + var sw = screenW; + var sh = screenH; + var meshes = cd.meshes; + var bvh = this.bvh; + var temp = temporal; + var retestInterval = 4; + + bvh.traverseFrontToBack(camX, camY, camZ, + function(node: BVHNode): Bool { + if (!ObjectBVH.aabbInFrustum(node, planes)) return true; + var anyVisible = false; + if (node.isLeaf) { + for (j in 0...node.objectCount) { + var idx = bvh.objectIndices[node.firstObject + j]; + if ((cd.getFlags(idx) & 8) != 0) { anyVisible = true; break; } + } + if (!anyVisible) return true; + } + if (raster.isOccluded( + node.minX, node.minY, node.minZ, + node.maxX, node.maxY, node.maxZ, + vp, sw, sh + )) { + bvh.forEachLeafInSubtree(node, function(idx: Int) { + if (idx >= meshes.length || (cd.getFlags(idx) & 8) == 0) return; + meshes[idx].occlusionVisible = false; + var uid = cd.getObjectID(idx); + temp.setVisible(uid, false); + temp.markTested(uid); + }); + return true; + } + return false; + }, + function(objIdx: Int, _: Int) { + if ((cd.getFlags(objIdx) & 8) == 0) return; + if (objIdx >= meshes.length) return; + var uid = cd.getObjectID(objIdx); + if (temp.wasVisibleLastFrame(uid) && !temp.needsVisibleReTest(uid, retestInterval)) { + meshes[objIdx].occlusionVisible = true; + cd.addVisible(objIdx); + return; + } + if (raster.isOccluded( + cd.getMinX(objIdx), cd.getMinY(objIdx), cd.getMinZ(objIdx), + cd.getMaxX(objIdx), cd.getMaxY(objIdx), cd.getMaxZ(objIdx), + vp, sw, sh + )) { + meshes[objIdx].occlusionVisible = false; + temp.setVisible(uid, false); + temp.markTested(uid); + } else { + meshes[objIdx].occlusionVisible = true; + cd.addVisible(objIdx); + temp.setVisible(uid, true); + temp.markTested(uid); + } + } + ); + } + + public static var autoOccluderMinScreenRatio: Float = 0.10; + + public inline function isVisible(objectIndex: Int): Bool { + var ms = cullData.meshes; + return objectIndex >= 0 && objectIndex < ms.length && ms[objectIndex].occlusionVisible; + } + + public inline function markDirty() { + bvhDirty = true; + proxiesDirty = true; + cullData.slotsDirty = true; + shadowFlagsValid = false; + } + + public function removeLight(light: iron.object.LightObject) { + shadowPasses.remove(light); + shadowCallIdx.remove(light); + shadowCallFrame.remove(light); + #if lnx_csm + shadowVps.remove(light); + #end + } + + public static function init() { + if (active == null) { + active = new CullSystem(); + } + } + + public function loadOccluderProxies() { + occluderProxies.resize(0); + if (Scene.active == null) { + uidToProxy.clear(); + return; + } + var meshes = Scene.active.meshes; + uidScan.clear(); + for (i in 0...meshes.length) { + var mesh = meshes[i]; + uidScan.set(mesh.uid, true); + if (uidToProxy.exists(mesh.uid)) continue; + if (mesh.data == null || mesh.data.raw == null) continue; + var raw = mesh.data.raw; + if (raw.occluder_proxy == null) continue; + var proxyRaw = raw.occluder_proxy; + if (proxyRaw.positions == null || proxyRaw.indices == null) continue; + var proxy = new OccluderProxy(); + proxy.setData(proxyRaw.positions, proxyRaw.indices, mesh.uid); + uidToProxy.set(mesh.uid, proxy); + } + var dead: Array = null; + for (uid in uidToProxy.keys()) { + if (!uidScan.exists(uid)) { + if (dead == null) dead = []; + dead.push(uid); + } + } + if (dead != null) for (uid in dead) uidToProxy.remove(uid); + for (proxy in uidToProxy.iterator()) occluderProxies.push(proxy); + } + + public static function shutdown() { + active = null; + } +} + +class ShadowPassSlot { + public var vp: Mat4 = null; + public var rasterizer: SoftwareOcclusionRasterizer = null; + public var sceneRev: Int = -1; + public var appliedRev: Int = -1; + public var lastUsed: Int = -1; + public var flags: Array = []; + public var flagsLen: Int = 0; + public function new() {} +} diff --git a/leenkx/Sources/iron/culling/CullTier.hx b/leenkx/Sources/iron/culling/CullTier.hx new file mode 100644 index 00000000..bd2b2706 --- /dev/null +++ b/leenkx/Sources/iron/culling/CullTier.hx @@ -0,0 +1,9 @@ +package iron.culling; + +enum CullTier { + CPUSingleThread; + CPUMultiThread; + GPUCompute; + GPUDriven; + MultiGPU; +} diff --git a/leenkx/Sources/iron/culling/HiZPyramid.hx b/leenkx/Sources/iron/culling/HiZPyramid.hx new file mode 100644 index 00000000..edbf10a7 --- /dev/null +++ b/leenkx/Sources/iron/culling/HiZPyramid.hx @@ -0,0 +1,171 @@ +package iron.culling; + +class HiZPyramid { + + public var width: Int; + public var height: Int; + public var mipLevels: Int; + public var mipBuffers: Array; + public var mipWidths: Array; + public var mipHeights: Array; + + public var depthEps: Float; + + var mipShifts: Array; + + public function new(width: Int, height: Int) { + this.width = width; + this.height = height; + mipBuffers = []; + mipWidths = []; + mipHeights = []; + depthEps = CullSystem.depthEps; + + var w = width; + var h = height; + mipLevels = 0; + while (w > 1 || h > 1) { + mipWidths.push(w); + mipHeights.push(h); + mipBuffers.push(new kha.arrays.Float32Array(w * h)); + mipLevels++; + w = (w + 1) >> 1; + h = (h + 1) >> 1; + } + mipWidths.push(1); + mipHeights.push(1); + mipBuffers.push(new kha.arrays.Float32Array(1)); + mipLevels++; + + mipShifts = [0, 1, 2, 3, 4, 5, 6]; + while (mipShifts.length < mipLevels) mipShifts.push(mipShifts.length); + } + + public function clear(farDepth: Float = 1e20) { + var buf = mipBuffers[0]; + for (i in 0...buf.length) buf[i] = farDepth; + } + + public inline function writeDepth(x: Int, y: Int, depth: Float) { + var idx = y * width + x; + if (x >= 0 && x < width && y >= 0 && y < height) { + if (depth < mipBuffers[0][idx]) { + mipBuffers[0][idx] = depth; + } + } + } + + public function buildPyramid() { + for (mip in 1...mipLevels) { + var prev = mipBuffers[mip - 1]; + var cur = mipBuffers[mip]; + var pw = mipWidths[mip - 1]; + var ph = mipHeights[mip - 1]; + var cw = mipWidths[mip]; + var ch = mipHeights[mip]; + + for (y in 0...ch) { + for (x in 0...cw) { + var sx = x * 2; + var sy = y * 2; + var d00 = prev[sy * pw + sx]; + var d10 = (sx + 1 < pw) ? prev[sy * pw + sx + 1] : d00; + var d01 = (sy + 1 < ph) ? prev[(sy + 1) * pw + sx] : d00; + var d11 = (sx + 1 < pw && sy + 1 < ph) ? prev[(sy + 1) * pw + sx + 1] : d00; + var maxD = d00; + if (d10 > maxD) maxD = d10; + if (d01 > maxD) maxD = d01; + if (d11 > maxD) maxD = d11; + cur[y * cw + x] = maxD; + } + } + } + } + + public inline function sample(x: Int, y: Int, mip: Int): Float { + var mw = mipWidths[mip]; + var mh = mipHeights[mip]; + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= mw) x = mw - 1; + if (y >= mh) y = mh - 1; + return mipBuffers[mip][y * mw + x]; + } + + public static function mipForSize(rectW: Float, rectH: Float, maxMip: Int): Int { + var maxDim = Math.max(rectW, rectH); + if (maxDim <= 2.0) return 0; + var mip = Std.int(Math.floor(Math.log(maxDim) / Math.log(2))) - 1; + if (mip < 0) mip = 0; + if (mip > maxMip) mip = maxMip; + return mip; + } + + public function isOccluded( + minX: Float, minY: Float, minZ: Float, + maxX: Float, maxY: Float, maxZ: Float, + vp: iron.math.Mat4, + _screenW: Int, _screenH: Int + ): Bool { + var minSx = 1e20, minSy = 1e20; + var maxSx = -1e20, maxSy = -1e20; + var minDepth = 1e20; + + var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30; + var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31; + var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32; + var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33; + + for (c in 0...8) { + var x = (c & 1) != 0 ? maxX : minX; + var y = (c & 2) != 0 ? maxY : minY; + var z = (c & 4) != 0 ? maxZ : minZ; + var w = m03 * x + m13 * y + m23 * z + m33; + if (w <= 0) return false; + var invW = SimdOps.reciprocal(w); + var ndcZ = (m02 * x + m12 * y + m22 * z + m32) * invW; + if (ndcZ < -1.0) ndcZ = -1.0; else if (ndcZ > 1.0) ndcZ = 1.0; + var sx = ((m00 * x + m10 * y + m20 * z + m30) * invW * 0.5 + 0.5) * width; + var sy = ((m01 * x + m11 * y + m21 * z + m31) * invW * 0.5 + 0.5) * height; + if (sx < minSx) minSx = sx; + if (sy < minSy) minSy = sy; + if (sx > maxSx) maxSx = sx; + if (sy > maxSy) maxSy = sy; + if (ndcZ < minDepth) minDepth = ndcZ; + } + + var ix0 = Std.int(Math.max(0, minSx)); + var iy0 = Std.int(Math.max(0, minSy)); + var ix1 = Std.int(Math.min(width - 1, maxSx)); + var iy1 = Std.int(Math.min(height - 1, maxSy)); + + if (ix0 > ix1 || iy0 > iy1) return false; + + if (ix0 > 0) ix0--; + if (iy0 > 0) iy0--; + if (ix1 < width - 1) ix1++; + if (iy1 < height - 1) iy1++; + + var rectW = ix1 - ix0 + 1; + var rectH = iy1 - iy0 + 1; + var mip = mipForSize(rectW, rectH, mipLevels - 1); + + var mw = mipWidths[mip]; + var mh = mipHeights[mip]; + var shift = mipShifts[mip]; + var tx0 = Std.int(Math.min(mw - 1, ix0 >> shift)); + var ty0 = Std.int(Math.min(mh - 1, iy0 >> shift)); + var tx1 = Std.int(Math.min(mw - 1, ix1 >> shift)); + var ty1 = Std.int(Math.min(mh - 1, iy1 >> shift)); + + var buf = mipBuffers[mip]; + var limit = minDepth - depthEps; + for (ty in ty0...ty1 + 1) { + var rowOff = ty * mw; + for (tx in tx0...tx1 + 1) { + if (buf[rowOff + tx] >= limit) return false; + } + } + return true; + } +} diff --git a/leenkx/Sources/iron/culling/JobSystem.hx b/leenkx/Sources/iron/culling/JobSystem.hx new file mode 100644 index 00000000..2c5e607c --- /dev/null +++ b/leenkx/Sources/iron/culling/JobSystem.hx @@ -0,0 +1,60 @@ +package iron.culling; + +#if (target.threaded) + +class JobSystem { + + public static var numWorkers(default, null): Int = 0; + + static var jobQueue: sys.thread.DequeVoid> = null; + static var doneLock: sys.thread.Lock = null; + static var jobError: Dynamic = null; + + public static function init(workers: Int = 3) { + if (jobQueue != null) return; + numWorkers = workers < 1 ? 1 : workers; + jobQueue = new sys.thread.Deque(); + doneLock = new sys.thread.Lock(); + for (_ in 0...numWorkers) { + sys.thread.Thread.create(workerLoop); + } + } + + static function workerLoop() { + while (true) { + var job = jobQueue.pop(true); + if (job == null) return; + try { + job(); + } catch (e: Dynamic) { + jobError = e; + } + doneLock.release(); + } + } + + public static function parallelFor(count: Int, fn: Int -> Int -> Void) { + init(); + var chunks = numWorkers + 1; + var chunkSize = Std.int((count + chunks - 1) / chunks); + if (chunkSize < 1) chunkSize = 1; + var jobs = 0; + var start = chunkSize; // Chunk 0 runs on the calling thread + while (start < count) { + var s = start; + var e = start + chunkSize > count ? count : start + chunkSize; + jobQueue.add(function() fn(s, e)); + jobs++; + start = e; + } + fn(0, chunkSize > count ? count : chunkSize); + for (_ in 0...jobs) doneLock.wait(); + if (jobError != null) { + var e = jobError; + jobError = null; + throw e; + } + } +} + +#end diff --git a/leenkx/Sources/iron/culling/ObjectBVH.hx b/leenkx/Sources/iron/culling/ObjectBVH.hx new file mode 100644 index 00000000..56df346f --- /dev/null +++ b/leenkx/Sources/iron/culling/ObjectBVH.hx @@ -0,0 +1,376 @@ +package iron.culling; + +class ObjectBVH { + + static inline var LEAF_THRESHOLD = 4; + static inline var NUM_BINS = 12; + static inline var TRAVERSAL_COST = 1.0; + static inline var INTERSECT_COST = 1.0; + + public var nodes: Array; + public var objectIndices: Array; + public var root: Int = -1; + public var objectCount: Int = 0; + + var stack: Array = []; + + static var binCounts: Array = [for (i in 0...NUM_BINS) 0]; + static var binMinX: Array = [for (i in 0...NUM_BINS) 0]; + static var binMinY: Array = [for (i in 0...NUM_BINS) 0]; + static var binMinZ: Array = [for (i in 0...NUM_BINS) 0]; + static var binMaxX: Array = [for (i in 0...NUM_BINS) 0]; + static var binMaxY: Array = [for (i in 0...NUM_BINS) 0]; + static var binMaxZ: Array = [for (i in 0...NUM_BINS) 0]; + + public function new() { + nodes = []; + objectIndices = []; + } + + public function build(cullData: CullData) { + objectCount = cullData.count; + nodes = []; + objectIndices = []; + + if (objectCount == 0) { + root = -1; + return; + } + + var indices = [for (i in 0...objectCount) i]; + root = buildRecursive(indices, cullData); + } + + static inline function surfaceArea(minX: Float, minY: Float, minZ: Float, + maxX: Float, maxY: Float, maxZ: Float): Float { + var wx = maxX - minX; + var wy = maxY - minY; + var wz = maxZ - minZ; + return 2.0 * (wx * wy + wx * wz + wy * wz); + } + + function buildRecursive(indices: Array, cullData: CullData): Int { + var nodeIdx = nodes.length; + var node = new BVHNode(); + nodes.push(node); + + var minX = 1e20, minY = 1e20, minZ = 1e20; + var maxX = -1e20, maxY = -1e20, maxZ = -1e20; + var centMinX = 1e20, centMinY = 1e20, centMinZ = 1e20; + var centMaxX = -1e20, centMaxY = -1e20, centMaxZ = -1e20; + var cx = 0.0, cy = 0.0, cz = 0.0; + + for (idx in indices) { + var x = cullData.getCenterX(idx); + var y = cullData.getCenterY(idx); + var z = cullData.getCenterZ(idx); + if (cullData.getMinX(idx) < minX) minX = cullData.getMinX(idx); + if (cullData.getMinY(idx) < minY) minY = cullData.getMinY(idx); + if (cullData.getMinZ(idx) < minZ) minZ = cullData.getMinZ(idx); + if (cullData.getMaxX(idx) > maxX) maxX = cullData.getMaxX(idx); + if (cullData.getMaxY(idx) > maxY) maxY = cullData.getMaxY(idx); + if (cullData.getMaxZ(idx) > maxZ) maxZ = cullData.getMaxZ(idx); + if (x < centMinX) centMinX = x; + if (y < centMinY) centMinY = y; + if (z < centMinZ) centMinZ = z; + if (x > centMaxX) centMaxX = x; + if (y > centMaxY) centMaxY = y; + if (z > centMaxZ) centMaxZ = z; + cx += x; + cy += y; + cz += z; + } + + var n = indices.length; + cx /= n; + cy /= n; + cz /= n; + + node.minX = minX; node.minY = minY; node.minZ = minZ; + node.maxX = maxX; node.maxY = maxY; node.maxZ = maxZ; + node.centerX = cx; + node.centerY = cy; + node.centerZ = cz; + + if (n <= LEAF_THRESHOLD) { + node.isLeaf = true; + node.firstObject = objectIndices.length; + for (idx in indices) objectIndices.push(idx); + node.objectCount = n; + return nodeIdx; + } + + var parentSA = surfaceArea(minX, minY, minZ, maxX, maxY, maxZ); + var leafCost = INTERSECT_COST * n; + + var bestCost = 1e20; + var bestAxis = -1; + var bestSplitBin = -1; + + var centBounds = [ + [centMinX, centMaxX], + [centMinY, centMaxY], + [centMinZ, centMaxZ] + ]; + + for (axis in 0...3) { + var lo = centBounds[axis][0]; + var hi = centBounds[axis][1]; + var range = hi - lo; + if (range < 1e-8) continue; + + var invRange = NUM_BINS / range; + + for (b in 0...NUM_BINS) { + binCounts[b] = 0; + binMinX[b] = 1e20; binMinY[b] = 1e20; binMinZ[b] = 1e20; + binMaxX[b] = -1e20; binMaxY[b] = -1e20; binMaxZ[b] = -1e20; + } + + for (idx in indices) { + var cv = axis == 0 ? cullData.getCenterX(idx) : + axis == 1 ? cullData.getCenterY(idx) : + cullData.getCenterZ(idx); + var b = Std.int((cv - lo) * invRange); + if (b < 0) b = 0; + if (b >= NUM_BINS) b = NUM_BINS - 1; + binCounts[b]++; + if (cullData.getMinX(idx) < binMinX[b]) binMinX[b] = cullData.getMinX(idx); + if (cullData.getMinY(idx) < binMinY[b]) binMinY[b] = cullData.getMinY(idx); + if (cullData.getMinZ(idx) < binMinZ[b]) binMinZ[b] = cullData.getMinZ(idx); + if (cullData.getMaxX(idx) > binMaxX[b]) binMaxX[b] = cullData.getMaxX(idx); + if (cullData.getMaxY(idx) > binMaxY[b]) binMaxY[b] = cullData.getMaxY(idx); + if (cullData.getMaxZ(idx) > binMaxZ[b]) binMaxZ[b] = cullData.getMaxZ(idx); + } + + var rightCount = [for (b in 0...NUM_BINS) 0]; + var rightMinX = [for (b in 0...NUM_BINS) 0.0]; + var rightMinY = [for (b in 0...NUM_BINS) 0.0]; + var rightMinZ = [for (b in 0...NUM_BINS) 0.0]; + var rightMaxX = [for (b in 0...NUM_BINS) 0.0]; + var rightMaxY = [for (b in 0...NUM_BINS) 0.0]; + var rightMaxZ = [for (b in 0...NUM_BINS) 0.0]; + + var cumCount = 0; + var cumMinX = 1e20, cumMinY = 1e20, cumMinZ = 1e20; + var cumMaxX = -1e20, cumMaxY = -1e20, cumMaxZ = -1e20; + + for (b in 0...NUM_BINS) { + var bi = NUM_BINS - 1 - b; + cumCount += binCounts[bi]; + if (binMinX[bi] < cumMinX) cumMinX = binMinX[bi]; + if (binMinY[bi] < cumMinY) cumMinY = binMinY[bi]; + if (binMinZ[bi] < cumMinZ) cumMinZ = binMinZ[bi]; + if (binMaxX[bi] > cumMaxX) cumMaxX = binMaxX[bi]; + if (binMaxY[bi] > cumMaxY) cumMaxY = binMaxY[bi]; + if (binMaxZ[bi] > cumMaxZ) cumMaxZ = binMaxZ[bi]; + rightCount[bi] = cumCount; + rightMinX[bi] = cumMinX; rightMinY[bi] = cumMinY; rightMinZ[bi] = cumMinZ; + rightMaxX[bi] = cumMaxX; rightMaxY[bi] = cumMaxY; rightMaxZ[bi] = cumMaxZ; + } + + var leftCount = 0; + var leftMinX = 1e20, leftMinY = 1e20, leftMinZ = 1e20; + var leftMaxX = -1e20, leftMaxY = -1e20, leftMaxZ = -1e20; + + for (b in 0...NUM_BINS - 1) { + if (binCounts[b] == 0) continue; + leftCount += binCounts[b]; + if (binMinX[b] < leftMinX) leftMinX = binMinX[b]; + if (binMinY[b] < leftMinY) leftMinY = binMinY[b]; + if (binMinZ[b] < leftMinZ) leftMinZ = binMinZ[b]; + if (binMaxX[b] > leftMaxX) leftMaxX = binMaxX[b]; + if (binMaxY[b] > leftMaxY) leftMaxY = binMaxY[b]; + if (binMaxZ[b] > leftMaxZ) leftMaxZ = binMaxZ[b]; + + var rc = rightCount[b + 1]; + if (rc == 0) continue; + + var leftSA = surfaceArea(leftMinX, leftMinY, leftMinZ, leftMaxX, leftMaxY, leftMaxZ); + var rightSA = surfaceArea(rightMinX[b + 1], rightMinY[b + 1], rightMinZ[b + 1], + rightMaxX[b + 1], rightMaxY[b + 1], rightMaxZ[b + 1]); + var cost = TRAVERSAL_COST + (leftSA * leftCount + rightSA * rc) / parentSA; + + if (cost < bestCost) { + bestCost = cost; + bestAxis = axis; + bestSplitBin = b; + } + } + } + + if (bestAxis < 0 || bestCost >= leafCost) { + node.isLeaf = true; + node.firstObject = objectIndices.length; + for (idx in indices) objectIndices.push(idx); + node.objectCount = n; + return nodeIdx; + } + + var lo = centBounds[bestAxis][0]; + var hi = centBounds[bestAxis][1]; + var invRange = NUM_BINS / (hi - lo); + var splitBin = bestSplitBin; + + var left = []; + var right = []; + for (idx in indices) { + var cv = bestAxis == 0 ? cullData.getCenterX(idx) : + bestAxis == 1 ? cullData.getCenterY(idx) : + cullData.getCenterZ(idx); + var b = Std.int((cv - lo) * invRange); + if (b < 0) b = 0; + if (b >= NUM_BINS) b = NUM_BINS - 1; + if (b <= splitBin) left.push(idx); + else right.push(idx); + } + + if (left.length == 0 || right.length == 0) { + indices.sort(function(a, b): Int { + var av = bestAxis == 0 ? cullData.getCenterX(a) : + bestAxis == 1 ? cullData.getCenterY(a) : + cullData.getCenterZ(a); + var bv = bestAxis == 0 ? cullData.getCenterX(b) : + bestAxis == 1 ? cullData.getCenterY(b) : + cullData.getCenterZ(b); + if (av < bv) return -1; + if (av > bv) return 1; + return 0; + }); + var mid = n >> 1; + left = indices.slice(0, mid); + right = indices.slice(mid); + } + + node.isLeaf = false; + node.leftChild = buildRecursive(left, cullData); + node.rightChild = buildRecursive(right, cullData); + + return nodeIdx; + } + + + public function refit(cullData: CullData) { + var i = nodes.length - 1; + while (i >= 0) { + var node = nodes[i]; + if (node.isLeaf) { + var minX = 1e20, minY = 1e20, minZ = 1e20; + var maxX = -1e20, maxY = -1e20, maxZ = -1e20; + for (j in 0...node.objectCount) { + var idx = objectIndices[node.firstObject + j]; + if (cullData.getMinX(idx) < minX) minX = cullData.getMinX(idx); + if (cullData.getMinY(idx) < minY) minY = cullData.getMinY(idx); + if (cullData.getMinZ(idx) < minZ) minZ = cullData.getMinZ(idx); + if (cullData.getMaxX(idx) > maxX) maxX = cullData.getMaxX(idx); + if (cullData.getMaxY(idx) > maxY) maxY = cullData.getMaxY(idx); + if (cullData.getMaxZ(idx) > maxZ) maxZ = cullData.getMaxZ(idx); + } + node.minX = minX; node.minY = minY; node.minZ = minZ; + node.maxX = maxX; node.maxY = maxY; node.maxZ = maxZ; + } else { + var l = nodes[node.leftChild]; + var r = nodes[node.rightChild]; + node.minX = l.minX < r.minX ? l.minX : r.minX; + node.minY = l.minY < r.minY ? l.minY : r.minY; + node.minZ = l.minZ < r.minZ ? l.minZ : r.minZ; + node.maxX = l.maxX > r.maxX ? l.maxX : r.maxX; + node.maxY = l.maxY > r.maxY ? l.maxY : r.maxY; + node.maxZ = l.maxZ > r.maxZ ? l.maxZ : r.maxZ; + } + node.centerX = (node.minX + node.maxX) * 0.5; + node.centerY = (node.minY + node.maxY) * 0.5; + node.centerZ = (node.minZ + node.maxZ) * 0.5; + i--; + } + } + + public function forEachLeafInSubtree(node: BVHNode, cb: (Int) -> Void) { + if (node.isLeaf) { + for (i in 0...node.objectCount) { + cb(objectIndices[node.firstObject + i]); + } + } else { + forEachLeafInSubtree(nodes[node.leftChild], cb); + forEachLeafInSubtree(nodes[node.rightChild], cb); + } + } + + public function traverseFrontToBack( + camX: Float, camY: Float, camZ: Float, + shouldSkip: (BVHNode) -> Bool, + onLeaf: (Int, Int) -> Void + ) { + if (root < 0) return; + + stack.resize(0); + stack.push(root); + + while (stack.length > 0) { + var nodeIdx = stack.pop(); + var node = nodes[nodeIdx]; + + if (shouldSkip(node)) continue; + + if (node.isLeaf) { + var start = node.firstObject; + for (i in 0...node.objectCount) { + onLeaf(objectIndices[start + i], i); + } + } else { + var leftNode = nodes[node.leftChild]; + var rightNode = nodes[node.rightChild]; + var leftDist = distSq(leftNode.centerX, leftNode.centerY, leftNode.centerZ, camX, camY, camZ); + var rightDist = distSq(rightNode.centerX, rightNode.centerY, rightNode.centerZ, camX, camY, camZ); + + if (leftDist > rightDist) { + stack.push(node.leftChild); + stack.push(node.rightChild); + } else { + stack.push(node.rightChild); + stack.push(node.leftChild); + } + } + } + } + + inline function distSq(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Float { + var dx = ax - bx; + var dy = ay - by; + var dz = az - bz; + return dx * dx + dy * dy + dz * dz; + } + + public static function aabbInFrustumRaw( + minX: Float, minY: Float, minZ: Float, + maxX: Float, maxY: Float, maxZ: Float, + planes: Array + ): Bool { + for (i in 0...planes.length) { + var plane = planes[i]; + var px = plane.normal.x >= 0 ? maxX : minX; + var py = plane.normal.y >= 0 ? maxY : minY; + var pz = plane.normal.z >= 0 ? maxZ : minZ; + if (SimdOps.dot3(px, py, pz, plane.normal.x, plane.normal.y, plane.normal.z) + plane.constant < 0) { + return false; + } + } + return true; + } + + public static function aabbInFrustum( + node: BVHNode, + planes: Array + ): Bool { + for (i in 0...planes.length) { + var plane = planes[i]; + var px = plane.normal.x >= 0 ? node.maxX : node.minX; + var py = plane.normal.y >= 0 ? node.maxY : node.minY; + var pz = plane.normal.z >= 0 ? node.maxZ : node.minZ; + if (SimdOps.dot3(px, py, pz, plane.normal.x, plane.normal.y, plane.normal.z) + plane.constant < 0) { + return false; + } + } + return true; + } +} diff --git a/leenkx/Sources/iron/culling/OccluderProxy.hx b/leenkx/Sources/iron/culling/OccluderProxy.hx new file mode 100644 index 00000000..eed0d1db --- /dev/null +++ b/leenkx/Sources/iron/culling/OccluderProxy.hx @@ -0,0 +1,71 @@ +package iron.culling; + +class OccluderProxy { + + public var positions: kha.arrays.Float32Array; + public var indices: kha.arrays.Uint32Array; + public var triangleCount: Int; + public var meshUid: Int; + + var worldPositions: kha.arrays.Float32Array; + var mvp: iron.math.Mat4; + + public function new() { + indices = new kha.arrays.Uint32Array(0); + triangleCount = 0; + } + + public function setData(pos: kha.arrays.Float32Array, idx: kha.arrays.Uint32Array, uid: Int) { + #if js + if (!Std.isOfType(pos, js.lib.DataView)) { + var srcF: Array = cast pos; + var dstF = new kha.arrays.Float32Array(srcF.length); + for (i in 0...srcF.length) dstF[i] = srcF[i]; + pos = dstF; + } + if (!Std.isOfType(idx, js.lib.DataView)) { + var srcI: Array = cast idx; + var dstI = new kha.arrays.Uint32Array(srcI.length); + for (i in 0...srcI.length) dstI[i] = srcI[i]; + idx = dstI; + } + #end + positions = pos; + indices = idx; + triangleCount = Std.int(idx.length / 3); + meshUid = uid; + } + + public function rasterize( + rasterizer: SoftwareOcclusionRasterizer, + world: iron.math.Mat4, + vp: iron.math.Mat4, + screenW: Int, screenH: Int + ) { + if (positions == null || indices == null || triangleCount == 0) return; + + var vertCount = Std.int(positions.length / 3); + + var clipSize = vertCount * 4; + if (worldPositions == null || worldPositions.length < clipSize) { + worldPositions = new kha.arrays.Float32Array(clipSize); + } + + if (mvp == null) mvp = iron.math.Mat4.identity(); + mvp.multmats(vp, world); + + for (i in 0...vertCount) { + var x = positions[i * 3]; + var y = positions[i * 3 + 1]; + var z = positions[i * 3 + 2]; + var off = i * 4; + worldPositions[off] = mvp._00 * x + mvp._10 * y + mvp._20 * z + mvp._30; + worldPositions[off + 1] = mvp._01 * x + mvp._11 * y + mvp._21 * z + mvp._31; + worldPositions[off + 2] = mvp._02 * x + mvp._12 * y + mvp._22 * z + mvp._32; + worldPositions[off + 3] = mvp._03 * x + mvp._13 * y + mvp._23 * z + mvp._33; + } + + rasterizer.rasterizeProxyTransformed(worldPositions, indices, screenW, screenH); + } + +} diff --git a/leenkx/Sources/iron/culling/SimdOps.hx b/leenkx/Sources/iron/culling/SimdOps.hx new file mode 100644 index 00000000..c89ecacb --- /dev/null +++ b/leenkx/Sources/iron/culling/SimdOps.hx @@ -0,0 +1,83 @@ +package iron.culling; + +#if cpp +@:include("smmintrin.h") +@:include("emmintrin.h") +@:include("xmmintrin.h") +#end +class SimdOps { + + public static inline function dot4( + a0: Float, a1: Float, a2: Float, a3: Float, + b0: Float, b1: Float, b2: Float, b3: Float + ): Float { + #if cpp + return untyped __cpp__("_mm_cvtss_f32(_mm_dp_ps(_mm_setr_ps({0}, {1}, {2}, {3}), _mm_setr_ps({4}, {5}, {6}, {7}), 0xF1))", + a0, a1, a2, a3, b0, b1, b2, b3); + #else + return a0 * b0 + a1 * b1 + a2 * b2 + a3 * b3; + #end + } + + public static inline function dot3( + a0: Float, a1: Float, a2: Float, + b0: Float, b1: Float, b2: Float + ): Float { + #if cpp + return untyped __cpp__("_mm_cvtss_f32(_mm_dp_ps(_mm_setr_ps({0}, {1}, {2}, 0.0f), _mm_setr_ps({3}, {4}, {5}, 0.0f), 0x71))", + a0, a1, a2, b0, b1, b2); + #else + return a0 * b0 + a1 * b1 + a2 * b2; + #end + } + + public static inline function reciprocal(x: Float): Float { + #if cpp + return untyped __cpp__("({ auto _r = _mm_rcp_ss(_mm_set_ss({0})); _mm_cvtss_f32(_mm_add_ss(_mm_mul_ss(_r, _mm_sub_ss(_mm_set_ss(2.0f), _mm_mul_ss(_r, _mm_set_ss({0})))), _r)); })", x); + #else + return 1.0 / x; + #end + } + + public static inline function max4f( + a: Float, b: Float, c: Float, d: Float + ): Float { + #if cpp + return untyped __cpp__("_mm_cvtss_f32(_mm_max_ss(_mm_max_ss(_mm_set_ss({0}), _mm_set_ss({1})), _mm_max_ss(_mm_set_ss({2}), _mm_set_ss({3}))))", + a, b, c, d); + #else + var m = a; + if (b > m) m = b; + if (c > m) m = c; + if (d > m) m = d; + return m; + #end + } + + public static inline function min4f( + a: Float, b: Float, c: Float, d: Float + ): Float { + #if cpp + return untyped __cpp__("_mm_cvtss_f32(_mm_min_ss(_mm_min_ss(_mm_set_ss({0}), _mm_set_ss({1})), _mm_min_ss(_mm_set_ss({2}), _mm_set_ss({3}))))", + a, b, c, d); + #else + var m = a; + if (b < m) m = b; + if (c < m) m = c; + if (d < m) m = d; + return m; + #end + } + + public static inline function frustumSphereTest6( + planeData: kha.arrays.Float32Array, + cx: Float, cy: Float, cz: Float, radius: Float + ): Bool { + for (i in 0...6) { + var base = i * 4; + var dist = dot3(cx, cy, cz, planeData[base], planeData[base + 1], planeData[base + 2]) + planeData[base + 3]; + if (dist + radius < 0) return false; + } + return true; + } +} diff --git a/leenkx/Sources/iron/culling/SoftwareOcclusionRasterizer.hx b/leenkx/Sources/iron/culling/SoftwareOcclusionRasterizer.hx new file mode 100644 index 00000000..43a7bc4c --- /dev/null +++ b/leenkx/Sources/iron/culling/SoftwareOcclusionRasterizer.hx @@ -0,0 +1,507 @@ +package iron.culling; + +class SoftwareOcclusionRasterizer { + + public var hiz: HiZPyramid; + public var width: Int; + public var height: Int; + + public var nearClip: Float = 0.0001; + + static inline var TILE_SIZE = 32; + + public var trianglesRasterized: Int = 0; + + public var collectMode = false; + var screenTris: Array = []; + var screenTriCount = 0; + + static final BOX_FACES = [ + [0, 1, 3, 2], // -Z + [4, 5, 7, 6], // +Z + [0, 1, 5, 4], // -Y + [2, 3, 7, 6], // +Y + [0, 2, 6, 4], // -X + [1, 3, 7, 5], // +X + ]; + + var inX: Array = [0,0,0]; + var inY: Array = [0,0,0]; + var inZ: Array = [0,0,0]; + var inW: Array = [0,0,0]; + var outX: Array = [0,0,0,0]; + var outY: Array = [0,0,0,0]; + var outZ: Array = [0,0,0,0]; + var outW: Array = [0,0,0,0]; + var projSx: Array = [0,0,0,0]; + var projSy: Array = [0,0,0,0]; + var projSz: Array = [0,0,0,0]; + + var boxClipX: Array = [0,0,0,0,0,0,0,0]; + var boxClipY: Array = [0,0,0,0,0,0,0,0]; + var boxClipZ: Array = [0,0,0,0,0,0,0,0]; + var boxClipW: Array = [0,0,0,0,0,0,0,0]; + + public function new(width: Int = 256, height: Int = 128) { + this.width = width; + this.height = height; + hiz = new HiZPyramid(width, height); + } + + public function clear() { + hiz.clear(1e20); + trianglesRasterized = 0; + } + + public function rasterizeTriangle( + v0x: Float, v0y: Float, v0z: Float, + v1x: Float, v1y: Float, v1z: Float, + v2x: Float, v2y: Float, v2z: Float + ) { + var e0a = v1y - v0y; var e0b = v0x - v1x; + var e1a = v2y - v1y; var e1b = v1x - v2x; + var e2a = v0y - v2y; var e2b = v2x - v0x; + var e0c = -e0a * v0x - e0b * v0y; + var e1c = -e1a * v1x - e1b * v1y; + var e2c = -e2a * v2x - e2b * v2y; + + var area = e0a * v2x + e0b * v2y + e0c; + if (area < 0) { + area = -area; + e0a = -e0a; e0b = -e0b; e0c = -e0c; + e1a = -e1a; e1b = -e1b; e1c = -e1c; + e2a = -e2a; e2b = -e2b; e2c = -e2c; + } + if (area < 1e-8) return; + + var minX = Std.int(Math.max(0, Math.floor(Math.min(v0x, Math.min(v1x, v2x))))); + var maxX = Std.int(Math.min(width - 1, Math.ceil(Math.max(v0x, Math.max(v1x, v2x))))); + var minY = Std.int(Math.max(0, Math.floor(Math.min(v0y, Math.min(v1y, v2y))))); + var maxY = Std.int(Math.min(height - 1, Math.ceil(Math.max(v0y, Math.max(v1y, v2y))))); + if (minX > maxX || minY > maxY) return; + + var d = (v1x - v0x) * (v2y - v0y) - (v2x - v0x) * (v1y - v0y); + var invD = 1.0 / d; + var dzdx = ((v1z - v0z) * (v2y - v0y) - (v2z - v0z) * (v1y - v0y)) * invD; + var dzdy = ((v2z - v0z) * (v1x - v0x) - (v1z - v0z) * (v2x - v0x)) * invD; + + var zPush = (Math.abs(dzdx) + Math.abs(dzdy)) * 0.5; + var zMaxTri = v0z; + if (v1z > zMaxTri) zMaxTri = v1z; + if (v2z > zMaxTri) zMaxTri = v2z; + + var cx0 = minX + 0.5; + var cy0 = minY + 0.5; + var e0row = e0a * cx0 + e0b * cy0 + e0c; + var e1row = e1a * cx0 + e1b * cy0 + e1c; + var e2row = e2a * cx0 + e2b * cy0 + e2c; + var zrow = v0z + dzdx * (cx0 - v0x) + dzdy * (cy0 - v0y); + + var buf = hiz.mipBuffers[0]; + var w = width; + for (py in minY...maxY + 1) { + var e0 = e0row; var e1 = e1row; var e2 = e2row; + var z = zrow; + var idx = py * w + minX; + for (px in minX...maxX + 1) { + if (e0 >= 0 && e1 >= 0 && e2 >= 0) { + var zPix = z + zPush; + if (zPix > zMaxTri) zPix = zMaxTri; + if (zPix < 0.0) zPix = 0.0; + if (zPix > 1.0) zPix = 1.0; + if (zPix < buf[idx]) buf[idx] = zPix; + } + e0 += e0a; e1 += e1a; e2 += e2a; + z += dzdx; + idx++; + } + e0row += e0b; e1row += e1b; e2row += e2b; + zrow += dzdy; + } + } + + function clipNearAndRasterize( + cx0: Float, cy0: Float, cz0: Float, cw0: Float, + cx1: Float, cy1: Float, cz1: Float, cw1: Float, + cx2: Float, cy2: Float, cz2: Float, cw2: Float + ) { + inX[0] = cx0; inY[0] = cy0; inZ[0] = cz0; inW[0] = cw0; + inX[1] = cx1; inY[1] = cy1; inZ[1] = cz1; inW[1] = cw1; + inX[2] = cx2; inY[2] = cy2; inZ[2] = cz2; inW[2] = cw2; + + var outCount = 0; + for (i in 0...3) { + var aIdx = (i + 2) % 3; + var bIdx = i; + var aInside = inW[aIdx] >= nearClip; + var bInside = inW[bIdx] >= nearClip; + + if (aInside) { + if (bInside) { + outX[outCount] = inX[bIdx]; outY[outCount] = inY[bIdx]; + outZ[outCount] = inZ[bIdx]; outW[outCount] = inW[bIdx]; + outCount++; + } else { + var t = (inW[aIdx] - nearClip) / (inW[aIdx] - inW[bIdx]); + outX[outCount] = inX[aIdx] + t * (inX[bIdx] - inX[aIdx]); + outY[outCount] = inY[aIdx] + t * (inY[bIdx] - inY[aIdx]); + outZ[outCount] = inZ[aIdx] + t * (inZ[bIdx] - inZ[aIdx]); + outW[outCount] = inW[aIdx] + t * (inW[bIdx] - inW[aIdx]); + outCount++; + } + } else { + if (bInside) { + var t = (inW[aIdx] - nearClip) / (inW[aIdx] - inW[bIdx]); + outX[outCount] = inX[aIdx] + t * (inX[bIdx] - inX[aIdx]); + outY[outCount] = inY[aIdx] + t * (inY[bIdx] - inY[aIdx]); + outZ[outCount] = inZ[aIdx] + t * (inZ[bIdx] - inZ[aIdx]); + outW[outCount] = inW[aIdx] + t * (inW[bIdx] - inW[aIdx]); + outCount++; + outX[outCount] = inX[bIdx]; outY[outCount] = inY[bIdx]; + outZ[outCount] = inZ[bIdx]; outW[outCount] = inW[bIdx]; + outCount++; + } + } + } + + if (outCount < 3) return; + + for (i in 0...outCount) { + var invW = SimdOps.reciprocal(outW[i]); + projSx[i] = (outX[i] * invW * 0.5 + 0.5) * width; + projSy[i] = (outY[i] * invW * 0.5 + 0.5) * height; + projSz[i] = outZ[i] * invW; + } + + var signedArea = (projSx[1] - projSx[0]) * (projSy[2] - projSy[0]) - + (projSy[1] - projSy[0]) * (projSx[2] - projSx[0]); + if (Math.abs(signedArea) < 1e-8) return; + + trianglesRasterized++; + + if (collectMode) { + screenTris[screenTriCount++] = projSx[0]; screenTris[screenTriCount++] = projSy[0]; screenTris[screenTriCount++] = projSz[0]; + screenTris[screenTriCount++] = projSx[1]; screenTris[screenTriCount++] = projSy[1]; screenTris[screenTriCount++] = projSz[1]; + screenTris[screenTriCount++] = projSx[2]; screenTris[screenTriCount++] = projSy[2]; screenTris[screenTriCount++] = projSz[2]; + if (outCount == 4) { + screenTris[screenTriCount++] = projSx[0]; screenTris[screenTriCount++] = projSy[0]; screenTris[screenTriCount++] = projSz[0]; + screenTris[screenTriCount++] = projSx[2]; screenTris[screenTriCount++] = projSy[2]; screenTris[screenTriCount++] = projSz[2]; + screenTris[screenTriCount++] = projSx[3]; screenTris[screenTriCount++] = projSy[3]; screenTris[screenTriCount++] = projSz[3]; + } + } else { + rasterizeTriangle( + projSx[0], projSy[0], projSz[0], + projSx[1], projSy[1], projSz[1], + projSx[2], projSy[2], projSz[2] + ); + if (outCount == 4) { + rasterizeTriangle( + projSx[0], projSy[0], projSz[0], + projSx[2], projSy[2], projSz[2], + projSx[3], projSy[3], projSz[3] + ); + } + } + } + + public function rasterizeProxy( + positions: kha.arrays.Float32Array, + indices: kha.arrays.Uint32Array, + vp: iron.math.Mat4, + screenW: Int, screenH: Int + ) { + var triCount = Std.int(indices.length / 3); + + var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30; + var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31; + var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32; + var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33; + + for (t in 0...triCount) { + var i0 = indices[t * 3] * 3; + var i1 = indices[t * 3 + 1] * 3; + var i2 = indices[t * 3 + 2] * 3; + + var px0 = positions[i0], py0 = positions[i0 + 1], pz0 = positions[i0 + 2]; + var px1 = positions[i1], py1 = positions[i1 + 1], pz1 = positions[i1 + 2]; + var px2 = positions[i2], py2 = positions[i2 + 1], pz2 = positions[i2 + 2]; + + var cw0 = m03 * px0 + m13 * py0 + m23 * pz0 + m33; + var cw1 = m03 * px1 + m13 * py1 + m23 * pz1 + m33; + var cw2 = m03 * px2 + m13 * py2 + m23 * pz2 + m33; + + if (cw0 < nearClip && cw1 < nearClip && cw2 < nearClip) continue; + + var cx0 = m00 * px0 + m10 * py0 + m20 * pz0 + m30; + var cy0 = m01 * px0 + m11 * py0 + m21 * pz0 + m31; + var cz0 = m02 * px0 + m12 * py0 + m22 * pz0 + m32; + + var cx1 = m00 * px1 + m10 * py1 + m20 * pz1 + m30; + var cy1 = m01 * px1 + m11 * py1 + m21 * pz1 + m31; + var cz1 = m02 * px1 + m12 * py1 + m22 * pz1 + m32; + + var cx2 = m00 * px2 + m10 * py2 + m20 * pz2 + m30; + var cy2 = m01 * px2 + m11 * py2 + m21 * pz2 + m31; + var cz2 = m02 * px2 + m12 * py2 + m22 * pz2 + m32; + + clipNearAndRasterize( + cx0, cy0, cz0, cw0, + cx1, cy1, cz1, cw1, + cx2, cy2, cz2, cw2 + ); + } + } + + public function rasterizeProxyTransformed( + clipPositions: kha.arrays.Float32Array, + indices: kha.arrays.Uint32Array, + screenW: Int, screenH: Int + ) { + var triCount = Std.int(indices.length / 3); + + for (t in 0...triCount) { + var i0 = indices[t * 3] * 4; + var i1 = indices[t * 3 + 1] * 4; + var i2 = indices[t * 3 + 2] * 4; + + var cx0 = clipPositions[i0], cy0 = clipPositions[i0 + 1], cz0 = clipPositions[i0 + 2], cw0 = clipPositions[i0 + 3]; + var cx1 = clipPositions[i1], cy1 = clipPositions[i1 + 1], cz1 = clipPositions[i1 + 2], cw1 = clipPositions[i1 + 3]; + var cx2 = clipPositions[i2], cy2 = clipPositions[i2 + 1], cz2 = clipPositions[i2 + 2], cw2 = clipPositions[i2 + 3]; + + if (cw0 < nearClip && cw1 < nearClip && cw2 < nearClip) continue; + + clipNearAndRasterize( + cx0, cy0, cz0, cw0, + cx1, cy1, cz1, cw1, + cx2, cy2, cz2, cw2 + ); + } + } + + public function rasterizeBox( + minX: Float, minY: Float, minZ: Float, + maxX: Float, maxY: Float, maxZ: Float, + vp: iron.math.Mat4, + screenW: Int, screenH: Int + ) { + var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30; + var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31; + var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32; + var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33; + + for (c in 0...8) { + var x = (c & 1) != 0 ? maxX : minX; + var y = (c & 2) != 0 ? maxY : minY; + var z = (c & 4) != 0 ? maxZ : minZ; + boxClipX[c] = m00 * x + m10 * y + m20 * z + m30; + boxClipY[c] = m01 * x + m11 * y + m21 * z + m31; + boxClipZ[c] = m02 * x + m12 * y + m22 * z + m32; + boxClipW[c] = m03 * x + m13 * y + m23 * z + m33; + } + + for (face in BOX_FACES) { + clipNearAndRasterize( + boxClipX[face[0]], boxClipY[face[0]], boxClipZ[face[0]], boxClipW[face[0]], + boxClipX[face[1]], boxClipY[face[1]], boxClipZ[face[1]], boxClipW[face[1]], + boxClipX[face[2]], boxClipY[face[2]], boxClipZ[face[2]], boxClipW[face[2]] + ); + clipNearAndRasterize( + boxClipX[face[0]], boxClipY[face[0]], boxClipZ[face[0]], boxClipW[face[0]], + boxClipX[face[2]], boxClipY[face[2]], boxClipZ[face[2]], boxClipW[face[2]], + boxClipX[face[3]], boxClipY[face[3]], boxClipZ[face[3]], boxClipW[face[3]] + ); + } + } + + public function buildHiZ() { + hiz.buildPyramid(); + } + + public function beginCollect() { + collectMode = true; + screenTriCount = 0; + } + + var tileTriCounts: Array = []; + var tileTriOffsets: Array = []; + var tileTriLists: Array> = []; + + public function flushCollected() { + collectMode = false; + var nTris = Std.int(screenTriCount / 9); + if (nTris == 0) return; + + var tilesX = Std.int(Math.ceil(width / TILE_SIZE)); + var tilesY = Std.int(Math.ceil(height / TILE_SIZE)); + var totalTiles = tilesX * tilesY; + + if (tileTriCounts.length < totalTiles) { + tileTriCounts.resize(totalTiles); + tileTriOffsets.resize(totalTiles); + tileTriLists.resize(totalTiles); + } + for (t in 0...totalTiles) tileTriCounts[t] = 0; + for (t in 0...nTris) { + var off = t * 9; + var tx0 = Std.int(Math.max(0, Math.floor(Math.min( + Math.min(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE))); + var tx1 = Std.int(Math.min(tilesX - 1, Math.ceil(Math.max( + Math.max(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE))); + var ty0 = Std.int(Math.max(0, Math.floor(Math.min( + Math.min(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE))); + var ty1 = Std.int(Math.min(tilesY - 1, Math.ceil(Math.max( + Math.max(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE))); + for (ty in ty0...ty1 + 1) { + for (tx in tx0...tx1 + 1) { + tileTriCounts[ty * tilesX + tx]++; + } + } + } + + for (t in 0...totalTiles) { + var l = tileTriLists[t]; + if (l == null) { l = []; tileTriLists[t] = l; } + if (l.length < tileTriCounts[t]) l.resize(tileTriCounts[t]); + tileTriOffsets[t] = 0; + } + for (t in 0...nTris) { + var off = t * 9; + var tx0 = Std.int(Math.max(0, Math.floor(Math.min( + Math.min(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE))); + var tx1 = Std.int(Math.min(tilesX - 1, Math.ceil(Math.max( + Math.max(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE))); + var ty0 = Std.int(Math.max(0, Math.floor(Math.min( + Math.min(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE))); + var ty1 = Std.int(Math.min(tilesY - 1, Math.ceil(Math.max( + Math.max(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE))); + for (ty in ty0...ty1 + 1) { + for (tx in tx0...tx1 + 1) { + var ti = ty * tilesX + tx; + tileTriLists[ti][tileTriOffsets[ti]++] = t; + } + } + } + + var buf = hiz.mipBuffers[0]; + var w = width; + var tris = screenTris; + + #if (target.threaded) + JobSystem.parallelFor(totalTiles, function(start, end) { + for (ti in start...end) { + var tx = ti % tilesX; + var ty = Std.int(ti / tilesX); + var px0 = tx * TILE_SIZE; + var py0 = ty * TILE_SIZE; + var px1 = Std.int(Math.min(w - 1, px0 + TILE_SIZE - 1)); + var py1 = Std.int(Math.min(height - 1, py0 + TILE_SIZE - 1)); + var list = tileTriLists[ti]; + var listLen = tileTriCounts[ti]; + for (li in 0...listLen) { + var off = list[li] * 9; + rasterizeTriangleBounded( + tris[off], tris[off + 1], tris[off + 2], + tris[off + 3], tris[off + 4], tris[off + 5], + tris[off + 6], tris[off + 7], tris[off + 8], + px0, py0, px1, py1, buf, w + ); + } + } + }); + #else + for (ti in 0...totalTiles) { + var tx = ti % tilesX; + var ty = Std.int(ti / tilesX); + var px0 = tx * TILE_SIZE; + var py0 = ty * TILE_SIZE; + var px1 = Std.int(Math.min(w - 1, px0 + TILE_SIZE - 1)); + var py1 = Std.int(Math.min(height - 1, py0 + TILE_SIZE - 1)); + var list = tileTriLists[ti]; + for (li in 0...list.length) { + var off = list[li] * 9; + rasterizeTriangleBounded( + tris[off], tris[off + 1], tris[off + 2], + tris[off + 3], tris[off + 4], tris[off + 5], + tris[off + 6], tris[off + 7], tris[off + 8], + px0, py0, px1, py1, buf, w + ); + } + } + #end + } + + function rasterizeTriangleBounded( + v0x: Float, v0y: Float, v0z: Float, + v1x: Float, v1y: Float, v1z: Float, + v2x: Float, v2y: Float, v2z: Float, + tileX0: Int, tileY0: Int, tileX1: Int, tileY1: Int, + buf: kha.arrays.Float32Array, w: Int + ) { + var e0a = v1y - v0y; var e0b = v0x - v1x; + var e1a = v2y - v1y; var e1b = v1x - v2x; + var e2a = v0y - v2y; var e2b = v2x - v0x; + var e0c = -e0a * v0x - e0b * v0y; + var e1c = -e1a * v1x - e1b * v1y; + var e2c = -e2a * v2x - e2b * v2y; + + var area = e0a * v2x + e0b * v2y + e0c; + if (area < 0) { + area = -area; + e0a = -e0a; e0b = -e0b; e0c = -e0c; + e1a = -e1a; e1b = -e1b; e1c = -e1c; + e2a = -e2a; e2b = -e2b; e2c = -e2c; + } + if (area < 1e-8) return; + + var minX = Std.int(Math.max(tileX0, Math.floor(Math.min(v0x, Math.min(v1x, v2x))))); + var maxX = Std.int(Math.min(tileX1, Math.ceil(Math.max(v0x, Math.max(v1x, v2x))))); + var minY = Std.int(Math.max(tileY0, Math.floor(Math.min(v0y, Math.min(v1y, v2y))))); + var maxY = Std.int(Math.min(tileY1, Math.ceil(Math.max(v0y, Math.max(v1y, v2y))))); + if (minX > maxX || minY > maxY) return; + + var d = (v1x - v0x) * (v2y - v0y) - (v2x - v0x) * (v1y - v0y); + var invD = 1.0 / d; + var dzdx = ((v1z - v0z) * (v2y - v0y) - (v2z - v0z) * (v1y - v0y)) * invD; + var dzdy = ((v2z - v0z) * (v1x - v0x) - (v1z - v0z) * (v2x - v0x)) * invD; + + var zPush = (Math.abs(dzdx) + Math.abs(dzdy)) * 0.5; + var zMaxTri = v0z; + if (v1z > zMaxTri) zMaxTri = v1z; + if (v2z > zMaxTri) zMaxTri = v2z; + + var cx0 = minX + 0.5; + var cy0 = minY + 0.5; + var e0row = e0a * cx0 + e0b * cy0 + e0c; + var e1row = e1a * cx0 + e1b * cy0 + e1c; + var e2row = e2a * cx0 + e2b * cy0 + e2c; + var zrow = v0z + dzdx * (cx0 - v0x) + dzdy * (cy0 - v0y); + + for (py in minY...maxY + 1) { + var e0 = e0row; var e1 = e1row; var e2 = e2row; + var z = zrow; + var idx = py * w + minX; + for (px in minX...maxX + 1) { + if (e0 >= 0 && e1 >= 0 && e2 >= 0) { + var zPix = z + zPush; + if (zPix > zMaxTri) zPix = zMaxTri; + if (zPix > 1.0) zPix = 1.0; + if (zPix < buf[idx]) { buf[idx] = zPix; } + } + e0 += e0a; e1 += e1a; e2 += e2a; + z += dzdx; + idx++; + } + e0row += e0b; e1row += e1b; e2row += e2b; + zrow += dzdy; + } + } + + public function isOccluded( + minX: Float, minY: Float, minZ: Float, + maxX: Float, maxY: Float, maxZ: Float, + vp: iron.math.Mat4, + screenW: Int, screenH: Int + ): Bool { + return hiz.isOccluded(minX, minY, minZ, maxX, maxY, maxZ, vp, screenW, screenH); + } +} diff --git a/leenkx/Sources/iron/culling/TemporalState.hx b/leenkx/Sources/iron/culling/TemporalState.hx new file mode 100644 index 00000000..9b7e060f --- /dev/null +++ b/leenkx/Sources/iron/culling/TemporalState.hx @@ -0,0 +1,79 @@ +package iron.culling; + +class TemporalState { + + public var visibleLastFrame: haxe.ds.IntMap; + public var visibleLastShadow: haxe.ds.IntMap; + public var frameLastTested: haxe.ds.IntMap; + public var occlusionConfidence: haxe.ds.IntMap; + public var capacity: Int = 0; + public var frameCount: Int = 0; + + public function new() { + visibleLastFrame = new haxe.ds.IntMap(); + visibleLastShadow = new haxe.ds.IntMap(); + frameLastTested = new haxe.ds.IntMap(); + occlusionConfidence = new haxe.ds.IntMap(); + } + + public function ensureCapacity(cap: Int) { + if (capacity < cap) capacity = cap; + } + + public inline function wasVisibleLastFrame(i: Int): Bool { + return visibleLastFrame.exists(i); + } + + public inline function wasVisibleShadowLastFrame(i: Int): Bool { + return visibleLastShadow.exists(i); + } + + public inline function needsTest(i: Int, maxSkipFrames: Int): Bool { + if (visibleLastFrame.exists(i)) return false; // assumed visible + var lastTested = frameLastTested.get(i); + if (lastTested == null) return true; + return frameCount - lastTested >= maxSkipFrames; + } + + public inline function needsVisibleReTest(i: Int, retestInterval: Int): Bool { + var lastTested = frameLastTested.get(i); + if (lastTested == null) return true; + return frameCount - lastTested >= retestInterval; + } + + public inline function setVisible(i: Int, visible: Bool) { + if (visible) { + visibleLastFrame.set(i, true); + occlusionConfidence.remove(i); + } else { + visibleLastFrame.remove(i); + var conf = occlusionConfidence.get(i); + occlusionConfidence.set(i, conf == null ? 1 : Std.int(Math.min(255, conf + 1))); + } + } + + public inline function markTested(i: Int) { + frameLastTested.set(i, frameCount); + } + + public inline function setVisibleShadow(i: Int, visible: Bool) { + if (visible) visibleLastShadow.set(i, true); + else visibleLastShadow.remove(i); + } + + public inline function advanceFrame() { + frameCount++; + } + + public function clear() { + visibleLastFrame = new haxe.ds.IntMap(); + visibleLastShadow = new haxe.ds.IntMap(); + frameLastTested = new haxe.ds.IntMap(); + occlusionConfidence = new haxe.ds.IntMap(); + } + + public inline function getConfidence(i: Int): Int { + var c = occlusionConfidence.get(i); + return c == null ? 0 : c; + } +} diff --git a/leenkx/Sources/iron/data/MeshBatch.hx b/leenkx/Sources/iron/data/MeshBatch.hx index da880c14..bb54aa35 100644 --- a/leenkx/Sources/iron/data/MeshBatch.hx +++ b/leenkx/Sources/iron/data/MeshBatch.hx @@ -82,20 +82,41 @@ class MeshBatch { Uniforms.setContextConstants(g, scontext, bindParams); - RenderPath.sortMeshesDistance(b.meshes); + var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent"; + if (!isShadowCtx) RenderPath.sortMeshesDistance(b.meshes); for (m in b.meshes) { if (!m.visible) continue; // Skip render if object is hidden + #if lnx_occlusion_culling + if (!m.occlusionVisible && (context == "mesh" || context == "depth" || context == "translucent")) continue; + if (!m.shadowOcclusionVisible && isShadowCtx) continue; + #end if (m.cullMesh(context, Scene.active.camera, RenderPath.active.light)) continue; // Get context - var materialContexts: Array = []; - var shaderContexts: Array = []; - m.getContexts(context, m.materials, materialContexts, shaderContexts); + var materialContexts: Array = null; + var shaderContexts: Array = null; + if (m.cachedMaterialContexts != null && MeshObject.matsEqual(m.cachedMats.get(context), m.materials)) { + materialContexts = m.cachedMaterialContexts.get(context); + shaderContexts = m.cachedShaderContexts.get(context); + } + if (materialContexts == null) { + materialContexts = []; + shaderContexts = []; + m.getContexts(context, m.materials, materialContexts, shaderContexts); + if (m.cachedMaterialContexts == null) { + m.cachedMats = new Map(); + m.cachedMaterialContexts = new Map(); + m.cachedShaderContexts = new Map(); + } + m.cachedMats.set(context, m.materials); + m.cachedMaterialContexts.set(context, materialContexts); + m.cachedShaderContexts.set(context, shaderContexts); + } Uniforms.posUnpack = m.data.scalePos; Uniforms.texUnpack = m.data.scaleTex; - m.transform.update(); + if (!isShadowCtx) m.transform.update(); // Render mesh Uniforms.setObjectConstants(g, scontext, m); diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index 669b76c5..eee72d67 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -60,6 +60,16 @@ typedef TMeshData = { @:optional public var scale_pos: Null; // Unpack pos from (-1,1) coords @:optional public var scale_tex: Null; // Unpack tex from (-1,1) coords @:optional public var morph_target: TMorphTarget; + @:optional public var occluder_proxy: TOccluderProxy; +} + +#if js +typedef TOccluderProxy = { +#else +@:structInit class TOccluderProxy { +#end + public var positions: Float32Array; + public var indices: Uint32Array; } #if js diff --git a/leenkx/Sources/iron/object/CameraObject.hx b/leenkx/Sources/iron/object/CameraObject.hx index 61c2ef45..2eaaedab 100644 --- a/leenkx/Sources/iron/object/CameraObject.hx +++ b/leenkx/Sources/iron/object/CameraObject.hx @@ -62,7 +62,20 @@ class CameraObject extends Object { public function buildProjection(screenAspect: Null = null) { if (data.raw.ortho != null) { - P = Mat4.ortho(data.raw.ortho[0], data.raw.ortho[1], data.raw.ortho[2], data.raw.ortho[3], data.raw.near_plane, data.raw.far_plane); + var left = data.raw.ortho[0]; + var right = data.raw.ortho[1]; + var bottom = data.raw.ortho[2]; + var top = data.raw.ortho[3]; + if (data.raw.aspect != null) { + var aspect: kha.FastFloat = data.raw.aspect; + if (screenAspect == null) screenAspect = iron.App.w() / iron.App.h(); + var scale = aspect / screenAspect; + var cx = (left + right) * 0.5; + var hw = (right - left) * 0.5 * scale; + left = cx - hw; + right = cx + hw; + } + P = Mat4.ortho(left, right, bottom, top, data.raw.near_plane, data.raw.far_plane); } else { if (screenAspect == null) screenAspect = iron.App.w() / iron.App.h(); @@ -247,25 +260,3 @@ class CameraObject extends Object { return new Vec4(-transform.world._20, -transform.world._21, -transform.world._22); } } - -class FrustumPlane { - public var normal = new Vec4(1.0, 0.0, 0.0); - public var constant = 0.0; - - public function new() {} - - public function normalize() { - var inverseNormalLength = 1.0 / normal.length(); - normal.mult(inverseNormalLength); - constant *= inverseNormalLength; - } - - public function distanceToSphere(sphereCenter: Vec4, sphereRadius: Float): Float { - return (normal.dot(sphereCenter) + constant) - sphereRadius; - } - - public inline function setComponents(x: Float, y: Float, z: Float, w: Float) { - normal.set(x, y, z); - constant = w; - } -} diff --git a/leenkx/Sources/iron/object/FrustumPlane.hx b/leenkx/Sources/iron/object/FrustumPlane.hx new file mode 100644 index 00000000..b725e7ac --- /dev/null +++ b/leenkx/Sources/iron/object/FrustumPlane.hx @@ -0,0 +1,25 @@ +package iron.object; + +import iron.math.Vec4; + +class FrustumPlane { + public var normal = new Vec4(1.0, 0.0, 0.0); + public var constant = 0.0; + + public function new() {} + + public function normalize() { + var inverseNormalLength = 1.0 / normal.length(); + normal.mult(inverseNormalLength); + constant *= inverseNormalLength; + } + + public function distanceToSphere(sphereCenter: Vec4, sphereRadius: Float): Float { + return (normal.dot(sphereCenter) + constant) - sphereRadius; + } + + public inline function setComponents(x: Float, y: Float, z: Float, w: Float) { + normal.set(x, y, z); + constant = w; + } +} diff --git a/leenkx/Sources/iron/object/LightObject.hx b/leenkx/Sources/iron/object/LightObject.hx index da6a8d79..a55d2682 100644 --- a/leenkx/Sources/iron/object/LightObject.hx +++ b/leenkx/Sources/iron/object/LightObject.hx @@ -34,7 +34,7 @@ class LightObject extends Object { // Cascades public static var cascadeCount = 1; public static var cascadeSplitFactor = 0.8; - public static var cascadeBounds = 1.0; + public static var cascadeBounds = 2.0; // occlusion culling shadows pop with 1.0 #if lnx_csm var cascadeData: Float32Array = null; var cascadeVP: Array; @@ -140,6 +140,9 @@ class LightObject extends Object { } #end #end + #if lnx_occlusion_culling + if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.removeLight(this); + #end super.remove(); } @@ -278,12 +281,13 @@ class LightObject extends Object { var worldPerTexelX = (maxx - minx) / smsize; var worldPerTexelY = (maxy - miny) / smsize; var worldPerTexelZ = (maxz - minz) / smsize; + var ex = maxx - minx, ey = maxy - miny, ez = maxz - minz; minx = Math.floor(minx / worldPerTexelX) * worldPerTexelX; miny = Math.floor(miny / worldPerTexelY) * worldPerTexelY; minz = Math.floor(minz / worldPerTexelZ) * worldPerTexelZ; - maxx = Math.floor(maxx / worldPerTexelX) * worldPerTexelX; - maxy = Math.floor(maxy / worldPerTexelY) * worldPerTexelY; - maxz = Math.floor(maxz / worldPerTexelZ) * worldPerTexelZ; + maxx = minx + ex; + maxy = miny + ey; + maxz = minz + ez; var hx = (maxx - minx) / 2; var hy = (maxy - miny) / 2; diff --git a/leenkx/Sources/iron/object/MeshObject.hx b/leenkx/Sources/iron/object/MeshObject.hx index 8a5a3435..86c79611 100644 --- a/leenkx/Sources/iron/object/MeshObject.hx +++ b/leenkx/Sources/iron/object/MeshObject.hx @@ -30,9 +30,21 @@ class MeshObject extends Object { public var frustumCulling = true; public var activeTilesheet: Tilesheet = null; public var tilesheets: Array = null; + #if lnx_occlusion_culling + public var occlusionVisible = true; + public var shadowOcclusionVisible = true; + #end + var shadowCameraCulled: Bool = false; + var shadowCameraCullFrame: Int = -1; + var shadowCameraCullCamera: CameraObject = null; + var shadowCameraCullLight: LightObject = null; public var skip_context: String = null; // Do not draw this context public var force_context: String = null; // Draw only this context static var lastPipeline: PipelineState = null; + + var cachedMats: Map> = null; + var cachedMaterialContexts: Map> = null; + var cachedShaderContexts: Map> = null; #if lnx_morph_target public var morphTarget: MorphTarget = null; #end @@ -48,6 +60,9 @@ class MeshObject extends Object { this.materials = materials; setData(data); Scene.active.meshes.push(this); + #if lnx_occlusion_culling + if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.markDirty(); + #end } public function setData(data: MeshData) { @@ -96,6 +111,9 @@ class MeshObject extends Object { if (activeTilesheet != null) activeTilesheet.remove(); if (tilesheets != null) { for (ts in tilesheets) { ts.remove(); } tilesheets = null; } if (Scene.active != null) Scene.active.meshes.remove(this); + #if lnx_occlusion_culling + if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.markDirty(); + #end #if (rp_renderer == "Deferred") if (Scene.active != null) Scene.active.markMaterialParamsDirty(); #end @@ -196,7 +214,7 @@ class MeshObject extends Object { var isShadow = context == "shadowmap"; if (camera == null) return false; - if (camera.data.raw.frustum_culling && frustumCulling) { + if (camera.data.raw.frustum_culling && frustumCulling && RenderPath.active != null && RenderPath.active.frustumCulling) { // Scale radius for skinned mesh and particle system // TODO: define skin & particle bounds var radiusScale = data.isSkinned ? 2.0 : 1.0; @@ -211,7 +229,8 @@ class MeshObject extends Object { var frustumPlanes = isShadow ? light.frustumPlanes : camera.frustumPlanes; if (isShadow && light.data.raw.type != "sun") { // Non-sun light bounds intersect camera frustum - light.transform.radius = light.data.raw.far_plane; + var farPlane: kha.FastFloat = light.data.raw.far_plane; + light.transform.radius = farPlane * 1.25; if (!CameraObject.sphereInFrustum(camera.frustumPlanes, light.transform)) { return setCulled(isShadow, true); } @@ -220,6 +239,37 @@ class MeshObject extends Object { if (!CameraObject.sphereInFrustum(frustumPlanes, transform, radiusScale)) { return setCulled(isShadow, true); } + + if (isShadow && light.data.raw.type == "sun") { + var frame = RenderPath.active.frame; + if (shadowCameraCullFrame == frame && shadowCameraCullCamera == camera && + shadowCameraCullLight == light) { + if (shadowCameraCulled) return setCulled(isShadow, true); + } else { + shadowCameraCullFrame = frame; + shadowCameraCullCamera = camera; + shadowCameraCullLight = light; + shadowCameraCulled = false; + var ldx = -light.V._02; + var ldy = -light.V._12; + var ldz = -light.V._22; + var radius = transform.radius * radiusScale; + var cx = transform.worldx(); + var cy = transform.worldy(); + var cz = transform.worldz(); + var range = camera.data.raw.far_plane * (LightObject.cascadeBounds * 4.0); + var margin = radius * 0.1; + for (plane in camera.frustumPlanes) { + var reach = plane.normal.x * cx + plane.normal.y * cy + plane.normal.z * cz + plane.constant + radius + margin; + var dot = plane.normal.x * ldx + plane.normal.y * ldy + plane.normal.z * ldz; + if (dot > 0) reach += dot * range; + if (reach < 0) { + shadowCameraCulled = true; + return setCulled(isShadow, true); + } + } + } + } } //culled = false; @@ -253,6 +303,12 @@ class MeshObject extends Object { } } + static function matsEqual(a: Vector, b: Vector): Bool { + if (a == null || b == null || a.length != b.length) return false; + for (i in 0...a.length) if (a[i] != b[i]) return false; + return true; + } + public function render(g: Graphics, context: String, bindParams: Array) { if (data == null || !data.geom.ready) return; // Data not yet streamed if (!visible) return; // Skip render if object is hidden @@ -293,6 +349,7 @@ class MeshObject extends Object { // Get lod var mats = materials; var lod = this; + var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent"; if (raw != null && raw.lods != null && raw.lods.length > 0) { computeScreenSize(Scene.active.camera); initLods(); @@ -317,13 +374,29 @@ class MeshObject extends Object { if (isLodMaterial() && !validContext(mats, context)) return; // Get context - var materialContexts: Array = []; - var shaderContexts: Array = []; - getContexts(context, mats, materialContexts, shaderContexts); + var materialContexts: Array = null; + var shaderContexts: Array = null; + if (cachedMaterialContexts != null && matsEqual(cachedMats.get(context), mats)) { + materialContexts = cachedMaterialContexts.get(context); + shaderContexts = cachedShaderContexts.get(context); + } + if (materialContexts == null) { + materialContexts = []; + shaderContexts = []; + getContexts(context, mats, materialContexts, shaderContexts); + if (cachedMaterialContexts == null) { + cachedMats = new Map(); + cachedMaterialContexts = new Map(); + cachedShaderContexts = new Map(); + } + cachedMats.set(context, mats); + cachedMaterialContexts.set(context, materialContexts); + cachedShaderContexts.set(context, shaderContexts); + } Uniforms.posUnpack = data.scalePos; Uniforms.texUnpack = data.scaleTex; - transform.update(); + if (!isShadowCtx) transform.update(); // Render mesh var ldata = lod.data; diff --git a/leenkx/Sources/iron/system/LnxPack.hx b/leenkx/Sources/iron/system/LnxPack.hx index 533ccec6..6b4ba98e 100644 --- a/leenkx/Sources/iron/system/LnxPack.hx +++ b/leenkx/Sources/iron/system/LnxPack.hx @@ -166,6 +166,7 @@ class LnxPack { case "anim": TAnimation; case "tracks": TTrack; case "morph_target": TMorphTarget; + case "occluder_proxy": TOccluderProxy; case "vertex_groups": TVertex_groups; case "tilesheet": TTilesheetData; case "events": TTilesheetEvent; diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx index 55df7bbf..546214b8 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx @@ -933,26 +933,6 @@ class RenderPathDeferred { } #end - #if rp_water - { - #if (!kha_opengl) - path.setDepthFrom("tex", "gbuffer1"); - #end - - path.setTarget("buf"); - path.bindTarget("tex", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - path.setTarget("tex"); - path.bindTarget("_main", "gbufferD"); - path.bindTarget("buf", "tex"); - path.drawShader("shader_datas/water_pass/water_pass"); - - #if (!kha_opengl) - path.setDepthFrom("tex", "gbuffer0"); - #end - } - #end - #if rp_ssrefr { if (leenkx.data.Config.raw.rp_ssrefr != false) @@ -1027,6 +1007,26 @@ class RenderPathDeferred { } #end + #if rp_water + { + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer1"); + #end + + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/water_pass/water_pass"); + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); + #end + } + #end + #if rp_ssr { if (leenkx.data.Config.raw.rp_ssr != false) { diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index e00908c7..1a1c3515 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -1133,6 +1133,11 @@ class LeenkxExporter: } out_object['properties'].append(out_property) + if bobject.lnx_is_occluder: + if 'properties' not in out_object: + out_object['properties'] = [] + out_object['properties'].append({'name': 'occluder', 'value': True}) + # Export the object reference and material references objref = bobject.data @@ -2125,7 +2130,8 @@ class LeenkxExporter: fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled) assets.add(fp) # No export necessary - if bobject.data.lnx_cached and os.path.exists(fp) or fp in self.build_cache.exported_mesh_files: + fp_json = fp.split('.lnx')[0] + '.json' + if bobject.data.lnx_cached and (os.path.exists(fp) or os.path.exists(fp_json)) or fp in self.build_cache.exported_mesh_files: return # Mesh users have different modifier stack @@ -2202,6 +2208,22 @@ class LeenkxExporter: if armature: self.export_skin(bobject, armature, export_mesh, out_mesh) + rpdat = lnx.utils.get_rp() + is_occluder = bobject.lnx_is_occluder + if not is_occluder: + for prop in bobject.lnx_propertylist: + if prop.name_prop == 'occluder' and getattr(prop, prop.type_prop + '_prop') == True: + is_occluder = True + break + use_global = False + if not is_occluder and rpdat is not None and rpdat.lnx_occlusion_all_objects: + is_occluder = rpdat.lnx_occlusion_min_size <= 0.0 or \ + max(bobject.dimensions) >= rpdat.lnx_occlusion_min_size + use_global = is_occluder + if is_occluder and rpdat is not None and rpdat.lnx_occlusion_culling: + self.export_occluder_proxy(export_mesh, out_mesh, bobject, use_global) + + # Restore the morph state after mesh export if shape_keys: bobject.active_shape_key_index = active_shape_key_index @@ -2311,6 +2333,139 @@ class LeenkxExporter: + def export_occluder_proxy(self, export_mesh: bpy.types.Mesh, out_mesh, bobject, use_global=False): + rpdat = lnx.utils.get_rp() + mode = bobject.lnx_occluder_mode + shrink = bobject.lnx_occluder_shrink + if use_global or mode == 'default': + mode = rpdat.lnx_occlusion_proxy_mode + if use_global or shrink < 0.0: + shrink = rpdat.lnx_occlusion_proxy_shrink + + if mode == 'box': + proxy_positions, proxy_indices = self._box_proxy(export_mesh) + self._apply_inward_offset(proxy_positions, shrink) + elif mode == 'convex': + proxy_positions, proxy_indices = self._convex_proxy(export_mesh) + self._apply_inward_offset(proxy_positions, shrink) + else: + proxy_positions, proxy_indices = self._apply_decimate_to_mesh( + export_mesh, rpdat, shrink) + + if len(proxy_indices) == 0: + return + + out_mesh['occluder_proxy'] = { + 'positions': proxy_positions, + 'indices': proxy_indices + } + + def _box_proxy(self, export_mesh: bpy.types.Mesh): + """12-triangle axis-aligned bounding box proxy in local space.""" + if len(export_mesh.vertices) == 0: + return [], [] + xs = [v.co.x for v in export_mesh.vertices] + ys = [v.co.y for v in export_mesh.vertices] + zs = [v.co.z for v in export_mesh.vertices] + lo = (min(xs), min(ys), min(zs)) + hi = (max(xs), max(ys), max(zs)) + proxy_positions = [ + lo[0], lo[1], lo[2], hi[0], lo[1], lo[2], + hi[0], hi[1], lo[2], lo[0], hi[1], lo[2], + lo[0], lo[1], hi[2], hi[0], lo[1], hi[2], + hi[0], hi[1], hi[2], lo[0], hi[1], hi[2]] + proxy_indices = [ + 0, 2, 1, 0, 3, 2, + 4, 5, 6, 4, 6, 7, + 0, 1, 5, 0, 5, 4, + 2, 3, 7, 2, 7, 6, + 1, 2, 6, 1, 6, 5, + 0, 4, 7, 0, 7, 3] + return proxy_positions, proxy_indices + + def _convex_proxy(self, export_mesh: bpy.types.Mesh): + proxy_positions = [] + proxy_indices = [] + bm = bmesh.new() + try: + bm.from_mesh(export_mesh) + bmesh.ops.convex_hull(bm, input=bm.verts) + remap = {} + for f in bm.faces: + fv = f.verts + for i in range(1, len(fv) - 1): + for v in (fv[0], fv[i], fv[i + 1]): + idx = remap.get(v.index) + if idx is None: + idx = len(remap) + remap[v.index] = idx + co = v.co + proxy_positions.append(co.x) + proxy_positions.append(co.y) + proxy_positions.append(co.z) + proxy_indices.append(idx) + finally: + bm.free() + return proxy_positions, proxy_indices + + def _apply_inward_offset(self, positions, shrink_pct): + n = len(positions) // 3 + if n == 0 or shrink_pct <= 0.0: + return + factor = 1.0 - shrink_pct * 0.01 + cx = sum(positions[0::3]) / n + cy = sum(positions[1::3]) / n + cz = sum(positions[2::3]) / n + for i in range(n): + positions[i * 3] = cx + (positions[i * 3] - cx) * factor + positions[i * 3 + 1] = cy + (positions[i * 3 + 1] - cy) * factor + positions[i * 3 + 2] = cz + (positions[i * 3 + 2] - cz) * factor + + def _apply_decimate_to_mesh(self, export_mesh: bpy.types.Mesh, rpdat, shrink): + temp_mesh = export_mesh.copy() + temp_obj = bpy.data.objects.new('lnx_decimate_temp', temp_mesh) + try: + self.scene.collection.objects.link(temp_obj) + + mod = temp_obj.modifiers.new(name='lnx_decimate', type='DECIMATE') + mod.decimate_type = rpdat.lnx_occlusion_decimate_mode + if rpdat.lnx_occlusion_decimate_mode == 'COLLAPSE': + mod.ratio = rpdat.lnx_occlusion_decimate_ratio + elif rpdat.lnx_occlusion_decimate_mode == 'DISSOLVE': + mod.angle_limit = math.radians( + rpdat.lnx_occlusion_decimate_ratio * 180) + elif rpdat.lnx_occlusion_decimate_mode == 'UNSUBDIV': + mod.iterations = max(1, int(rpdat.lnx_occlusion_decimate_ratio * 10)) + + self.depsgraph.update() + eval_obj = temp_obj.evaluated_get(self.depsgraph) + eval_mesh = eval_obj.to_mesh( + preserve_all_data_layers=False, depsgraph=self.depsgraph) + try: + eval_mesh.calc_loop_triangles() + + proxy_positions = [] + for v in eval_mesh.vertices: + co = v.co + proxy_positions.append(co.x) + proxy_positions.append(co.y) + proxy_positions.append(co.z) + + proxy_indices = [] + for tri in eval_mesh.loop_triangles: + proxy_indices.append(tri.vertices[0]) + proxy_indices.append(tri.vertices[1]) + proxy_indices.append(tri.vertices[2]) + + self._apply_inward_offset(proxy_positions, shrink) + finally: + eval_obj.to_mesh_clear() + finally: + bpy.data.objects.remove(temp_obj, do_unlink=True) + bpy.data.meshes.remove(temp_mesh, do_unlink=True) + + return proxy_positions, proxy_indices + def export_light(self, object_ref): """Exports a single light object.""" rpdat = lnx.utils.get_rp() @@ -2496,23 +2651,57 @@ class LeenkxExporter: b = proj[1][1] c = proj[2][2] d = proj[2][3] - k = (c - 1.0) / (c + 1.0) o['fov'] = 2.0 * math.atan(1.0 / b) + if abs(a) > 1e-6: + o['aspect'] = b / a if with_planes: - o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) - o['far_plane'] = k * o['near_plane'] + denom = c + 1.0 + k = (c - 1.0) / denom if abs(denom) >= 1e-6 else 0.0 + if abs(k) < 1e-6: + o['near_plane'] = 0.1 + o['far_plane'] = 1000.0 + else: + near = (d * (1.0 - k)) / (2.0 * k) + far = k * near + o['near_plane'] = max(0.001, min(near, 100000.0)) + o['far_plane'] = max(0.001, min(far, 100000.0)) @staticmethod def extract_ortho(o, proj): + a = proj[0][0] + b = proj[1][1] # left, right, bottom, top - o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \ - (1 - proj[3][0]) / proj[0][0], \ - -(1 + proj[3][1]) / proj[1][1], \ - (1 - proj[3][1]) / proj[1][1]] + o['ortho'] = [-(1 + proj[3][0]) / a, \ + (1 - proj[3][0]) / a, \ + -(1 + proj[3][1]) / b, \ + (1 - proj[3][1]) / b] o['near_plane'] = (1 + proj[3][2]) / proj[2][2] o['far_plane'] = -(1 - proj[3][2]) / proj[2][2] o['near_plane'] *= 2 o['far_plane'] *= 2 + if abs(a) > 1e-6: + o['aspect'] = b / a + + @staticmethod + def compute_effective_par(rpdat, render): + if rpdat is None: + return render.pixel_aspect_x, render.pixel_aspect_y + mode = rpdat.lnx_aspect_mode + if mode == 'blender': + return render.pixel_aspect_x, render.pixel_aspect_y + elif mode == 'square': + return 1.0, 1.0 + elif mode == 'ntsc_43': + return 0.889, 1.0 + elif mode == 'pal_43': + return 1.067, 1.0 + elif mode == 'ntsc_169': + return 1.212, 1.0 + elif mode == 'pal_169': + return 1.459, 1.0 + elif mode == 'custom': + return rpdat.lnx_aspect_custom, 1.0 + return render.pixel_aspect_x, render.pixel_aspect_y def export_camera(self, objectRef): o = {} @@ -2521,12 +2710,16 @@ class LeenkxExporter: camera = objectRef[1]["objectTable"][0] render = self.scene.render + rpdat = lnx.utils.get_rp() + + par_x, par_y = self.compute_effective_par(rpdat, render) + proj = camera.calc_matrix_camera( self.depsgraph, x=render.resolution_x, y=render.resolution_y, - scale_x=render.pixel_aspect_x, - scale_y=render.pixel_aspect_y) + scale_x=par_x, + scale_y=par_y) if objref.type == 'PERSP': self.extract_projection(o, proj) else: @@ -2594,7 +2787,7 @@ class LeenkxExporter: self.output['material_datas'].append(o) bpy.data.materials.remove(mat) rpdat = lnx.utils.get_rp() - if not rpdat.lnx_culling: + if not rpdat.lnx_backface_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' @@ -2673,7 +2866,7 @@ class LeenkxExporter: o['skip_context'] = material.lnx_skip_context rpdat = lnx.utils.get_rp() - if material.lnx_two_sided or not rpdat.lnx_culling: + if material.lnx_two_sided or not rpdat.lnx_backface_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' elif material.lnx_cull_mode != 'clockwise': @@ -3190,6 +3383,12 @@ class LeenkxExporter: 'clear_color': self.get_camera_clear_color() } + render = self.scene.render + rpdat = lnx.utils.get_rp() + par_x, par_y = self.compute_effective_par(rpdat, render) + res_aspect = render.resolution_x / render.resolution_y if render.resolution_y > 0 else 1.0 + effective_aspect = res_aspect * par_x / par_y + # Set viewport camera projection proj, is_persp = self.get_viewport_projection_matrix() if proj is not None: @@ -3197,6 +3396,9 @@ class LeenkxExporter: self.extract_projection(out_camera, proj, with_planes=False) else: self.extract_ortho(out_camera, proj) + + out_camera['aspect'] = effective_aspect + self.output['camera_datas'].append(out_camera) out_object = { diff --git a/leenkx/blender/lnx/live_patch.py b/leenkx/blender/lnx/live_patch.py index c59e4f65..500cb39c 100644 --- a/leenkx/blender/lnx/live_patch.py +++ b/leenkx/blender/lnx/live_patch.py @@ -390,12 +390,14 @@ def send_event(event_id: str, opt_data: Any = None): elif event_id == 'cam_props': cam: bpy.types.Camera = obj_data.data render = bpy.context.scene.render + rpdat = lnx.utils.get_rp() + par_x, par_y = LeenkxExporter.compute_effective_par(rpdat, render) projection = obj_data.calc_matrix_camera( bpy.context.evaluated_depsgraph_get(), x=render.resolution_x, y=render.resolution_y, - scale_x=render.pixel_aspect_x, - scale_y=render.pixel_aspect_y) + scale_x=par_x, + scale_y=par_y) focal_y = projection[1][1] depth_z = projection[2][2] depth_w = projection[2][3] @@ -403,8 +405,10 @@ def send_event(event_id: str, opt_data: Any = None): fov = 2.0 * math.atan(1.0 / focal_y) near = (depth_w * (1.0 - far_near_ratio)) / (2.0 * far_near_ratio) far = far_near_ratio * near + res_aspect = render.resolution_x / render.resolution_y if render.resolution_y > 0 else 1.0 + effective_aspect = res_aspect * par_x / par_y cam_obj = f'iron.Scene.active.getCamera("{cam.name}")' - js = f'var c = {cam_obj}; var cRaw = c.data.raw; cRaw.fov={fov}; cRaw.near_plane={near}; cRaw.far_plane={far}; cRaw.frustum_culling={"true" if cam.lnx_frustum_culling else "false"}; c.buildProjection();' + js = f'var c = {cam_obj}; var cRaw = c.data.raw; cRaw.fov={fov}; cRaw.near_plane={near}; cRaw.far_plane={far}; cRaw.frustum_culling={"true" if cam.lnx_frustum_culling else "false"}; cRaw.aspect={effective_aspect}; c.buildProjection();' write_patch(js) elif event_id == 'obj_visibility': diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index 1eaa2879..7d6471ea 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -190,6 +190,9 @@ def build(): rpdat = lnx.utils.get_rp() project_path = lnx.utils.get_fp() + if rpdat.lnx_frustum_culling: + assets.add_khafile_def('lnx_frustum_culling') + if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpath'] != None: lnx.api.drivers[rpdat.rp_driver]['make_rpath']() return @@ -210,6 +213,9 @@ def build(): if rpdat.rp_hdr: assets.add_khafile_def('rp_hdr') + if rpdat.lnx_occlusion_culling: + assets.add_khafile_def('lnx_occlusion_culling') + assets.add_khafile_def('rp_renderer={0}'.format(rpdat.rp_renderer)) if rpdat.rp_depthprepass: assets.add_khafile_def('rp_depthprepass') diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index af32d39f..f96f4eca 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -425,6 +425,16 @@ def init_properties(): bpy.types.Object.lnx_sorting_index = compatible_prop(IntProperty, name="Sorting Index", description="Sorting index for the Render's Draw Order", default=0, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_spawn = compatible_prop(BoolProperty, name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_mobile = compatible_prop(BoolProperty, name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_is_occluder = compatible_prop(BoolProperty, name="Is Occluder", description="Export simplified occluder geometry for software occlusion culling", default=False, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_occluder_mode = compatible_prop(EnumProperty, + items=[('convex', 'Convex Hull', 'Convex hull of the mesh - tight on convex shapes, over-covers concave ones'), + ('box', 'Box', 'Axis-aligned bounding box - fastest, 12 triangles'), + ('decimate', 'Decimate', 'Decimated mesh - closest fit, slowest export')], + name="Occluder Mode", + description="Proxy geometry used for occlusion culling", + default='convex', + override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_occluder_shrink = compatible_prop(FloatProperty, name="Occluder Shrink %", description="Shrink proxy toward its center: 0 = full size, 50 = half size (default), 100 = point", default=50.0, min=0.0, max=100.0, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_visible = compatible_prop(BoolProperty, name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_visible_shadow = compatible_prop(BoolProperty, name="Lighting", description="Object contributes to the lighting even if invisible", default=True, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_soft_body_margin = FloatProperty(name="Soft Body Margin", description="Collision margin", default=0.04) diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index cdf18a2b..49618fee 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -232,6 +232,15 @@ def update_renderpath(self, context): assets.invalidate_shader_cache(self, context) bpy.data.worlds['Lnx'].lnx_recompile = True +def update_occlusion_culling(self, context): + for m in bpy.data.meshes: + m.lnx_cached = False + for c in bpy.data.curves: + c.lnx_cached = False + for c in bpy.data.metaballs: + c.lnx_cached = False + update_renderpath(self, context) + def udpate_shadowmap_cascades(self, context): bpy.data.worlds['Lnx'].lnx_recompile = True update_renderpath(self, context) @@ -461,6 +470,7 @@ class LnxRPListItem(bpy.types.PropertyGroup): ('Distance', 'Distance', 'Distance'), ('Index', 'Index', 'Index')], name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data) + lnx_radix: BoolProperty(name="Radix Sort", description="Sort draw calls with the radix library instead of the comparison sort", default=True, update=update_renderpath) rp_depth_texture: BoolProperty(name="Depth Texture", description="Current render-path state", default=False) rp_depth_texture_state: EnumProperty( items=[('On', 'On', 'On'), @@ -697,8 +707,48 @@ class LnxRPListItem(bpy.types.PropertyGroup): ('CPU', 'CPU', 'CPU'), ('Off', 'Off', 'Off')], name='Particles', description='Enable particle simulation', default='GPU', update=assets.invalidate_shader_cache) - # Material override flags - lnx_culling: BoolProperty(name="Culling", default=True) + # Culling flags + lnx_backface_culling: BoolProperty(name="Backface Culling", description="Cull back-facing triangles (winding-based)", default=True) + lnx_frustum_culling: BoolProperty(name="Frustum Culling", description="Cull objects outside camera frustum", default=True) + lnx_occlusion_culling: BoolProperty(name="Object Occlusion Culling (Experimental)", description="Software occlusion culling using Hi-Z buffer", default=False, update=update_occlusion_culling) + lnx_occlusion_all_objects: BoolProperty(name="All Objects Occlude", description="Every mesh can occlude - ignores the per-object 'Is Occluder' flag", default=False, update=update_occlusion_culling) + lnx_occlusion_min_size: FloatProperty(name="Min Occluder Size", description="Objects whose largest bounding-box side is smaller than this (meters) never export as occluders", default=0.5, min=0.0, update=update_occlusion_culling) + lnx_occlusion_proxy_mode: EnumProperty( + name="Proxy Mode", + description="Occluder proxy geometry type", + items=[ + ('box', 'Box', 'Axis-aligned bounding box - fastest, 12 triangles'), + ('convex', 'Convex Hull', 'Convex hull of the mesh - tight on convex shapes, over-covers concave ones'), + ('decimate', 'Decimate', 'Decimated mesh - closest fit, slowest export'), + ], + default='convex', update=update_occlusion_culling) + lnx_occlusion_proxy_shrink: FloatProperty(name="Proxy Shrink %", description="Shrink proxy toward its center: 0 = full size, 50 = half size (default), 100 = point - keeps occluders conservative", default=50.0, min=0.0, max=100.0, precision=1, update=update_occlusion_culling) + lnx_occlusion_box_shrink: IntProperty(name="Box Shrink %", description="0 = full AABB (aggressive), 50 = half-size (default, balanced), 100 = point (no occlusion)", default=50, min=0, max=100, update=update_renderpath) + lnx_occlusion_depth_eps: IntProperty(name="Depth Epsilon", description="6 = 1e-6 (default, tight), higher = more tolerant (less aggressive culling), lower = stricter", default=6, min=1, max=10, update=update_renderpath) + lnx_occlusion_decimate_ratio: FloatProperty(name="Ratio", description="Proportion of edges to collapse (COLLAPSE) or angle threshold in degrees (DISSOLVE) or iterations (UNSUBDIV)", default=0.5, min=0.01, max=1.0, precision=3, update=update_occlusion_culling) + lnx_occlusion_decimate_mode: EnumProperty( + name="Decimate Mode", + description="Decimate method applied to occluder proxy geometry", + items=[ + ('COLLAPSE', 'Collapse', 'Collapse edges to reduce triangle count by ratio'), + ('DISSOLVE', 'Dissolve', 'Dissolve vertices with angle below threshold (ratio = degrees)'), + ('UNSUBDIV', 'Unsubdivide', 'Reverse subdivision (ratio = iterations, 0.1 = 1 iter, 1.0 = 10 iter)'), + ], + default='COLLAPSE', update=update_occlusion_culling) + lnx_aspect_mode: EnumProperty( + name="Aspect Ratio", + description="Pixel aspect ratio override for camera projection. 'Blender' uses the scene's resolution and pixel aspect settings", + items=[ + ('blender', 'Blender', 'Use Blender\'s Output Properties resolution and pixel aspect ratio'), + ('square', 'Square (1.0)', 'Square pixels (1:1)'), + ('ntsc_43', 'NTSC 4:3', 'NTSC pixel aspect ratio (0.889)'), + ('pal_43', 'PAL 4:3', 'PAL pixel aspect ratio (1.067)'), + ('ntsc_169', 'NTSC 16:9', 'NTSC widescreen pixel aspect ratio (1.212)'), + ('pal_169', 'PAL 16:9', 'PAL widescreen pixel aspect ratio (1.459)'), + ('custom', 'Custom', 'Custom pixel aspect ratio'), + ], + default='blender') + lnx_aspect_custom: FloatProperty(name="Custom PAR", description="Custom pixel aspect ratio (width/height of a single pixel)", default=1.0, min=0.01, max=10.0, precision=3) lnx_two_sided_area_light: BoolProperty(name="Two-Sided Area Light", description="Emit light from both faces of area plane", default=False, update=assets.invalidate_shader_cache) @staticmethod diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 4e58a95b..fe6efe87 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -112,6 +112,10 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel): sub.prop(obj, 'lnx_visible_shadow') if obj.type == 'MESH': + layout.prop(obj, 'lnx_is_occluder') + if obj.lnx_is_occluder: + layout.prop(obj, 'lnx_occluder_mode') + layout.prop(obj, 'lnx_occluder_shrink') layout.prop(obj, 'lnx_instanced') if 'Rot' in obj.lnx_instanced: layout.prop(obj, 'lnx_instanced_rot_type') @@ -1843,6 +1847,7 @@ class LNX_PT_RenderPathRendererPanel(bpy.types.Panel): layout.prop(rpdat, 'rp_blending_state') layout.prop(rpdat, 'rp_depth_texture_state') layout.prop(rpdat, 'rp_draw_order') + layout.prop(rpdat, 'lnx_radix') layout.prop(rpdat, 'lnx_samples_per_pixel') layout.prop(rpdat, 'lnx_texture_filter') layout.prop(rpdat, 'rp_sss_state') @@ -1878,7 +1883,23 @@ class LNX_PT_RenderPathRendererPanel(bpy.types.Panel): col = layout.column() col.prop(rpdat, "rp_hdr") col.prop(rpdat, "rp_stereo") - col.prop(rpdat, 'lnx_culling') + col.prop(rpdat, 'lnx_backface_culling') + col.prop(rpdat, 'lnx_frustum_culling') + col.prop(rpdat, 'lnx_occlusion_culling') + if rpdat.lnx_occlusion_culling: + col.prop(rpdat, 'lnx_occlusion_all_objects') + if rpdat.lnx_occlusion_all_objects: + col.prop(rpdat, 'lnx_occlusion_min_size') + col.prop(rpdat, 'lnx_occlusion_proxy_mode') + if rpdat.lnx_occlusion_proxy_mode == 'decimate': + col.prop(rpdat, 'lnx_occlusion_decimate_mode') + col.prop(rpdat, 'lnx_occlusion_decimate_ratio') + col.prop(rpdat, 'lnx_occlusion_proxy_shrink') + col.prop(rpdat, 'lnx_occlusion_box_shrink') + col.prop(rpdat, 'lnx_occlusion_depth_eps') + col.prop(rpdat, 'lnx_aspect_mode') + if rpdat.lnx_aspect_mode == 'custom': + col.prop(rpdat, 'lnx_aspect_custom') col.prop(rpdat, 'rp_pp') diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 35455ab3..3d471782 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -249,6 +249,9 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + if not os.path.exists(os.path.join('Libraries', 'lib/hscript')): khafile.write(add_leenkx_library(sdk_path, 'lib/hscript', rel_path=do_relpath_sdk)) khafile.write("project.addDefine('" + 'hscript' + "');\n") + if lnx.utils.get_rp().lnx_radix: + if not os.path.exists(os.path.join('Libraries', 'lib/radix')): + khafile.write(add_leenkx_library(sdk_path, 'lib/radix', rel_path=do_relpath_sdk)) # Project libraries if os.path.exists('Libraries'): @@ -522,6 +525,9 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + if rpdat.rp_draw_order == 'Index': assets.add_khafile_def('lnx_draworder_index') + if rpdat.lnx_radix: + assets.add_khafile_def('lnx_radix') + if lnx.utils.get_viewport_controls() == 'azerty': assets.add_khafile_def('lnx_azerty') @@ -721,6 +727,11 @@ class Main { f.write("\n" + content_indent + "iron.object.LightObject.cascadeSplitFactor = " + str(rpdat.lnx_shadowmap_split) + ";") if rpdat.lnx_shadowmap_bounds != 1.0: f.write("\n" + content_indent + "iron.object.LightObject.cascadeBounds = " + str(rpdat.lnx_shadowmap_bounds) + ";") + + if rpdat.lnx_occlusion_culling: + depth_eps = max(10.0 ** -int(rpdat.lnx_occlusion_depth_eps), 1e-6) + f.write("\n" + content_indent + "iron.culling.CullSystem.boxShrink = " + str(rpdat.lnx_occlusion_box_shrink / 100) + ";") + f.write("\n" + content_indent + "iron.culling.CullSystem.depthEps = " + str(depth_eps) + ";") asset_references = list(set(assets.assets)) diff --git a/lib/radix/LICENSE_1_0.txt b/lib/radix/LICENSE_1_0.txt new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/lib/radix/LICENSE_1_0.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/lib/radix/README.md b/lib/radix/README.md new file mode 100644 index 00000000..fe3653da --- /dev/null +++ b/lib/radix/README.md @@ -0,0 +1,23 @@ +# Radix +Haxe port: in-place MSD radix sort (American flag sort) with an insertion sort fallback for small sub-ranges implemented in the Leenkx SDK by Riccardo Mazzucco + +Original reference source Malte Skarupke's [ska_sort](https://github.com/skarupke/ska_sort) - https://github.com/skarupke/ska_sort + +## Usage + +```haxe +import radix.Radix; + +// Stable sort: counting-sort scatter between `a` and `buffer`. +// Equal keys keep input order - matches haxe Array.sort semantics. +Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf); + +// In-place MSD sort on the same (element, keys) arrays. +// No scratch buffer needed, but NOT stable - equal keys may reorder. +Radix.sortKeyed(meshes, keys); + +// Or let the sorter build keys from an extractor +Radix.sort(meshes, function(m) { + return (m.depthRead ? 0x80000000 : 0) | (FPHelper.floatToI32(m.cameraDistance) >>> 1); +}); +``` diff --git a/lib/radix/Sources/radix/Radix.hx b/lib/radix/Sources/radix/Radix.hx new file mode 100644 index 00000000..41905a17 --- /dev/null +++ b/lib/radix/Sources/radix/Radix.hx @@ -0,0 +1,350 @@ +package radix; + +import haxe.ds.Vector; +import haxe.io.FPHelper; + +/** + In-place MSD radix sort - Haxe port of Malte Skarupke's ska_sort + (ref/ska_sort.hpp, Boost Software License 1.0). Original: + https://github.com/skarupke/ska_sort +**/ + +class Radix { + + static inline var STD_SORT_THRESHOLD = 128; + static inline var AMERICAN_FLAG_SORT_THRESHOLD = 1024; + + public static inline function intToUnsigned(i:Int):Int { + return i ^ 0x80000000; + } + + public static inline function floatToUnsigned(f:Float):Int { + var u = FPHelper.floatToI32(f); + var signBit = u >> 31; + return u ^ (signBit | 0x80000000); + } + + public static inline function uLess(a:Int, b:Int):Bool { + return (a ^ 0x80000000) < (b ^ 0x80000000); + } + + public static function sort(a:Array, extractKey:T->Int, ?keys:Array):Void { + if (keys == null) keys = new Array(); + for (i in 0...a.length) keys[i] = extractKey(a[i]); + sortKeyed(a, keys); + } + + public static function sortFloat(a:Array, extractKey:T->Float, ?keys:Array):Void { + if (keys == null) keys = new Array(); + for (i in 0...a.length) keys[i] = floatToUnsigned(extractKey(a[i])); + sortKeyed(a, keys); + } + + public static function sortKeyed(a:Array, keys:Array):Void { + var n = a.length; + if (stdSortIfLessThanThreshold(a, keys, 0, n)) return; + msdSort(a, keys, 0, n, 24, new RadixScratch()); + } + + static function msdSort(a:Array, keys:Array, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void { + var numElements = end - begin; + if (numElements < AMERICAN_FLAG_SORT_THRESHOLD) + americanFlagSort(a, keys, begin, end, shift, scratch); + else + skaByteSort(a, keys, begin, end, shift, scratch); + } + + static function stdSortIfLessThanThreshold(a:Array, keys:Array, begin:Int, end:Int):Bool { + var numElements = end - begin; + if (numElements <= 1) return true; + if (numElements >= STD_SORT_THRESHOLD) return false; + insertionSort(a, keys, begin, end); + return true; + } + + static function insertionSort(a:Array, keys:Array, begin:Int, end:Int):Void { + var i = begin + 1; + while (i < end) { + var key = keys[i]; + var v = a[i]; + var j = i - 1; + while (j >= begin && uLess(key, keys[j])) { + keys[j + 1] = keys[j]; + a[j + 1] = a[j]; + j--; + } + keys[j + 1] = key; + a[j + 1] = v; + i++; + } + } + + static function americanFlagSort(a:Array, keys:Array, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void { + var frame = scratch.frame(3 - (shift >> 3)); + var offset = frame.offset; + var nextOffset = frame.nextOffset; + var remaining = frame.remaining; + zeroTable(offset); + for (i in begin...end) { + offset[(keys[i] >>> shift) & 0xff]++; + } + var numPartitions = 0; + var total = 0; + for (i in 0...256) { + var count = offset[i]; + if (count == 0) continue; + offset[i] = total; + total += count; + nextOffset[i] = total; + remaining[numPartitions] = i; + numPartitions++; + } + if (numPartitions > 1) { + var curIdx = 0; + var cur = remaining[0]; + var lastIdx = numPartitions - 1; + var it = begin; + var blockEnd = begin + nextOffset[cur]; + var lastElement = end - 1; + var done = false; + while (true) { + var block = (keys[it] >>> shift) & 0xff; + if (block == cur) { + it++; + if (it == lastElement) break; + if (it == blockEnd) { + while (true) { + curIdx++; + if (curIdx == lastIdx) { + done = true; + break; + } + cur = remaining[curIdx]; + if (offset[cur] != nextOffset[cur]) break; + } + if (done) break; + it = begin + offset[cur]; + blockEnd = begin + nextOffset[cur]; + } + } + else { + var t = offset[block]; + offset[block] = t + 1; + swapAt(a, keys, it, begin + t); + } + } + } + if (shift > 0) { + var startOffset = 0; + var partitionBegin = begin; + for (i in 0...numPartitions) { + var endOffset = nextOffset[remaining[i]]; + var partitionEnd = begin + endOffset; + if (!stdSortIfLessThanThreshold(a, keys, partitionBegin, partitionEnd)) { + msdSort(a, keys, partitionBegin, partitionEnd, shift - 8, scratch); + } + startOffset = endOffset; + partitionBegin = partitionEnd; + } + } + } + + static function skaByteSort(a:Array, keys:Array, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void { + var frame = scratch.frame(3 - (shift >> 3)); + var offset = frame.offset; + var nextOffset = frame.nextOffset; + var remaining = frame.remaining; + zeroTable(offset); + for (i in begin...end) { + offset[(keys[i] >>> shift) & 0xff]++; + } + var numPartitions = 0; + var total = 0; + for (i in 0...256) { + var count = offset[i]; + if (count != 0) { + offset[i] = total; + total += count; + remaining[numPartitions] = i; + numPartitions++; + } + nextOffset[i] = total; + } + var lastRemaining = numPartitions; + while (lastRemaining > 1) { + var b = 0; + while (b < lastRemaining && processPending(a, keys, offset, nextOffset, remaining[b], begin, shift)) b++; + var it = b + 1; + while (it < lastRemaining) { + if (processPending(a, keys, offset, nextOffset, remaining[it], begin, shift)) { + var t = remaining[b]; + remaining[b] = remaining[it]; + remaining[it] = t; + b++; + } + it++; + } + lastRemaining = b; + } + if (shift > 0) { + var i = numPartitions; + while (i > 0) { + i--; + var partition = remaining[i]; + var start = partition == 0 ? 0 : nextOffset[partition - 1]; + var endOff = nextOffset[partition]; + if (!stdSortIfLessThanThreshold(a, keys, begin + start, begin + endOff)) { + msdSort(a, keys, begin + start, begin + endOff, shift - 8, scratch); + } + } + } + } + + static inline function newTable():Vector { + var v = new Vector(256); + #if !(cpp || hl || java || cs || flash) + for (i in 0...256) v[i] = 0; + #end + return v; + } + + static function processPending(a:Array, keys:Array, offset:Vector, nextOffset:Vector, partition:Int, begin:Int, shift:Int):Bool { + var endOff = nextOffset[partition]; + if (offset[partition] == endOff) return false; + var it = begin + offset[partition]; + var count = endOff - offset[partition]; + while (count >= 4) { + placeAt(a, keys, offset, it, begin, shift); it++; + placeAt(a, keys, offset, it, begin, shift); it++; + placeAt(a, keys, offset, it, begin, shift); it++; + placeAt(a, keys, offset, it, begin, shift); it++; + count -= 4; + } + while (count > 0) { + placeAt(a, keys, offset, it, begin, shift); + it++; + count--; + } + return offset[partition] != endOff; + } + + static inline function placeAt(a:Array, keys:Array, offset:Vector, it:Int, begin:Int, shift:Int):Void { + var p = (keys[it] >>> shift) & 0xff; + var t = offset[p]; + offset[p] = t + 1; + swapAt(a, keys, it, begin + t); + } + + static inline function zeroTable(v:Vector):Void { + for (i in 0...256) v[i] = 0; + } + + static inline function swapAt(a:Array, keys:Array, i:Int, j:Int):Void { + var ta = a[i]; + a[i] = a[j]; + a[j] = ta; + var tk = keys[i]; + keys[i] = keys[j]; + keys[j] = tk; + } + + public static function sortCopy(a:Array, buffer:Array, extractKey:T->Int):Bool { + var numElements = a.length; + if (numElements < STD_SORT_THRESHOLD) { + sort(a, extractKey); + return false; + } + var keys = new Array(); + var bufKeys = new Array(); + for (i in 0...numElements) keys[i] = extractKey(a[i]); + sortCopyKeyed(a, keys, buffer, bufKeys); + return false; + } + + public static function sortCopyKeyed(a:Array, keys:Array, buffer:Array, bufKeys:Array):Void { + var n = a.length; + var counts0 = newTable(); + var counts1 = newTable(); + var counts2 = newTable(); + var counts3 = newTable(); + for (i in 0...n) { + var key = keys[i]; + counts0[key & 0xff]++; + counts1[(key >>> 8) & 0xff]++; + counts2[(key >>> 16) & 0xff]++; + counts3[(key >>> 24) & 0xff]++; + } + var total0 = 0, total1 = 0, total2 = 0, total3 = 0; + for (i in 0...256) { + var old0 = counts0[i]; + var old1 = counts1[i]; + var old2 = counts2[i]; + var old3 = counts3[i]; + counts0[i] = total0; + counts1[i] = total1; + counts2[i] = total2; + counts3[i] = total3; + total0 += old0; + total1 += old1; + total2 += old2; + total3 += old3; + } + for (i in 0...n) { + var b = keys[i] & 0xff; + var t = counts0[b]; + counts0[b] = t + 1; + buffer[t] = a[i]; + bufKeys[t] = keys[i]; + } + for (i in 0...n) { + var b = (bufKeys[i] >>> 8) & 0xff; + var t = counts1[b]; + counts1[b] = t + 1; + a[t] = buffer[i]; + keys[t] = bufKeys[i]; + } + for (i in 0...n) { + var b = (keys[i] >>> 16) & 0xff; + var t = counts2[b]; + counts2[b] = t + 1; + buffer[t] = a[i]; + bufKeys[t] = keys[i]; + } + for (i in 0...n) { + var b = (bufKeys[i] >>> 24) & 0xff; + var t = counts3[b]; + counts3[b] = t + 1; + a[t] = buffer[i]; + keys[t] = bufKeys[i]; + } + } +} + +private class RadixFrame { + public var offset:Vector; + public var nextOffset:Vector; + public var remaining:Vector; + + public function new() { + offset = new Vector(256); + nextOffset = new Vector(256); + remaining = new Vector(256); + } +} + +private class RadixScratch { + public var frames:Array; + + public function new() { + frames = []; + } + + public inline function frame(level:Int):RadixFrame { + var f = frames[level]; + if (f == null) { + f = new RadixFrame(); + frames[level] = f; + } + return f; + } +} diff --git a/lib/radix/haxelib.json b/lib/radix/haxelib.json new file mode 100644 index 00000000..a0379dca --- /dev/null +++ b/lib/radix/haxelib.json @@ -0,0 +1,11 @@ +{ + "name": "radix", + "url": "https://github.com/skarupke/ska_sort", + "license": "BSL-1.0", + "tags": ["sort", "radix", "msd", "in-place"], + "description": "Haxe port : in-place MSD radix sort (American flag sort) for Leenkx SDK from Malte Skarupke's ska_sort with insertion sort fallback and stable LSD copy path", + "version": "1.0.0", + "classPath": "Sources/", + "releasenote": "Leenkx HAXE port", + "contributors": ["Riccardo Mazzucco"] +} From ee04bb87ddad42ce4a624590fc31f96eff775dcf Mon Sep 17 00:00:00 2001 From: Onek8 Date: Thu, 15 Oct 2026 02:52:58 -0700 Subject: [PATCH 7/7] Indent Fix --- leenkx/blender/lnx/make_renderpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index 7d6471ea..8a99faf1 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -190,7 +190,7 @@ def build(): rpdat = lnx.utils.get_rp() project_path = lnx.utils.get_fp() - if rpdat.lnx_frustum_culling: + if rpdat.lnx_frustum_culling: assets.add_khafile_def('lnx_frustum_culling') if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpath'] != None: