Object Occlusion Culling

This commit is contained in:
2026-10-15 02:41:03 -07:00
parent 3273054766
commit 612a3d0a09
33 changed files with 3629 additions and 84 deletions

View File

@ -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<String>;
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<Int>();
static var sortDistanceMeshesBuf = new Array<MeshObject>();
static var sortDistanceKeysBuf = new Array<Int>();
static var sortIndexKeys = new Array<Int>();
static var sortIndexMeshesBuf = new Array<MeshObject>();
static var sortIndexKeysBuf = new Array<Int>();
public static function sortMeshesDistance(meshes: Array<MeshObject>) {
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<MeshObject>) {
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<String>, _meshes: Array<MeshObject>) {
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) {

View File

@ -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() {}
}

View File

@ -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));
}
}

View File

@ -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<MeshObject> = [];
public var slotsDirty: Bool = true;
public var slotsRebuilt: Bool = false;
public var visibleList: Array<Int>;
public var visibleCount: Int = 0;
var staticFlagsCache: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
var matTranspCache: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
var matSnapshots: haxe.ds.IntMap<haxe.ds.Vector<iron.data.MaterialData>> = new haxe.ds.IntMap();
var prevUidMap: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
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<Int> = 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<iron.data.MaterialData>(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++;
}
}

View File

@ -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<OccluderProxy>;
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<iron.object.LightObject, Array<ShadowPassSlot>> = new haxe.ds.ObjectMap();
var shadowCallIdx: Map<iron.object.LightObject, Int> = new Map();
var shadowCallFrame: Map<iron.object.LightObject, Int> = new Map();
var lastShadowSlot: ShadowPassSlot = null;
var lastShadowFrame: Int = -1;
var lastShadowCallIdx: Int = -1;
#if lnx_csm
var shadowVps: haxe.ds.ObjectMap<iron.object.LightObject, Mat4> = 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<FrustumPlane> = null;
var frustumPlanes: Array<FrustumPlane> = null;
var shadowFrustumPlanes: Array<FrustumPlane> = null;
var uidToProxy: haxe.ds.IntMap<OccluderProxy> = new haxe.ds.IntMap<OccluderProxy>();
var uidScan: haxe.ds.IntMap<Bool> = new haxe.ds.IntMap<Bool>();
var prevVP: Mat4 = null;
#if lnx_taa
var unjitteredVP: Mat4 = null;
#end
public var shadowFlagsValid: Bool = false;
var inFrustumList: Array<Int> = [];
var selIdx: Array<Int> = [];
var selScore: Array<Float> = [];
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<iron.object.MeshObject>,
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<iron.object.MeshObject>, 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<FrustumPlane>
) {
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<Int> = 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<Bool> = [];
public var flagsLen: Int = 0;
public function new() {}
}

View File

@ -0,0 +1,9 @@
package iron.culling;
enum CullTier {
CPUSingleThread;
CPUMultiThread;
GPUCompute;
GPUDriven;
MultiGPU;
}

View File

@ -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<kha.arrays.Float32Array>;
public var mipWidths: Array<Int>;
public var mipHeights: Array<Int>;
public var depthEps: Float;
var mipShifts: Array<Int>;
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;
}
}

View File

@ -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.Deque<Void->Void> = 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

View File

@ -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<BVHNode>;
public var objectIndices: Array<Int>;
public var root: Int = -1;
public var objectCount: Int = 0;
var stack: Array<Int> = [];
static var binCounts: Array<Int> = [for (i in 0...NUM_BINS) 0];
static var binMinX: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMinY: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMinZ: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxX: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxY: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxZ: Array<Float> = [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<Int>, 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<iron.object.FrustumPlane>
): 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<iron.object.FrustumPlane>
): 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;
}
}

View File

@ -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<Float> = 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<Int> = 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);
}
}

View File

@ -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;
}
}

View File

@ -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<Float> = [];
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<Float> = [0,0,0];
var inY: Array<Float> = [0,0,0];
var inZ: Array<Float> = [0,0,0];
var inW: Array<Float> = [0,0,0];
var outX: Array<Float> = [0,0,0,0];
var outY: Array<Float> = [0,0,0,0];
var outZ: Array<Float> = [0,0,0,0];
var outW: Array<Float> = [0,0,0,0];
var projSx: Array<Float> = [0,0,0,0];
var projSy: Array<Float> = [0,0,0,0];
var projSz: Array<Float> = [0,0,0,0];
var boxClipX: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipY: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipZ: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipW: Array<Float> = [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<Int> = [];
var tileTriOffsets: Array<Int> = [];
var tileTriLists: Array<Array<Int>> = [];
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);
}
}

View File

@ -0,0 +1,79 @@
package iron.culling;
class TemporalState {
public var visibleLastFrame: haxe.ds.IntMap<Bool>;
public var visibleLastShadow: haxe.ds.IntMap<Bool>;
public var frameLastTested: haxe.ds.IntMap<Int>;
public var occlusionConfidence: haxe.ds.IntMap<Int>;
public var capacity: Int = 0;
public var frameCount: Int = 0;
public function new() {
visibleLastFrame = new haxe.ds.IntMap<Bool>();
visibleLastShadow = new haxe.ds.IntMap<Bool>();
frameLastTested = new haxe.ds.IntMap<Int>();
occlusionConfidence = new haxe.ds.IntMap<Int>();
}
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<Bool>();
visibleLastShadow = new haxe.ds.IntMap<Bool>();
frameLastTested = new haxe.ds.IntMap<Int>();
occlusionConfidence = new haxe.ds.IntMap<Int>();
}
public inline function getConfidence(i: Int): Int {
var c = occlusionConfidence.get(i);
return c == null ? 0 : c;
}
}

View File

@ -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<MaterialContext> = [];
var shaderContexts: Array<ShaderContext> = [];
m.getContexts(context, m.materials, materialContexts, shaderContexts);
var materialContexts: Array<MaterialContext> = null;
var shaderContexts: Array<ShaderContext> = 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);

View File

@ -60,6 +60,16 @@ typedef TMeshData = {
@:optional public var scale_pos: Null<FastFloat>; // Unpack pos from (-1,1) coords
@:optional public var scale_tex: Null<FastFloat>; // 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

View File

@ -62,7 +62,20 @@ class CameraObject extends Object {
public function buildProjection(screenAspect: Null<Float> = 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;
}
}

View File

@ -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;
}
}

View File

@ -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<Mat4>;
@ -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;

View File

@ -30,9 +30,21 @@ class MeshObject extends Object {
public var frustumCulling = true;
public var activeTilesheet: Tilesheet = null;
public var tilesheets: Array<Tilesheet> = 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<String, Vector<MaterialData>> = null;
var cachedMaterialContexts: Map<String, Array<MaterialContext>> = null;
var cachedShaderContexts: Map<String, Array<ShaderContext>> = 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<MaterialData>, b: Vector<MaterialData>): 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<String>) {
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<MaterialContext> = [];
var shaderContexts: Array<ShaderContext> = [];
getContexts(context, mats, materialContexts, shaderContexts);
var materialContexts: Array<MaterialContext> = null;
var shaderContexts: Array<ShaderContext> = 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;

View File

@ -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;

View File

@ -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) {