Merge pull request 'main' (#148) from Onek8/LNXSDK:main into main

Reviewed-on: #148
This commit is contained in:
2026-10-15 09:56:20 +00:00
38 changed files with 3826 additions and 144 deletions

View File

@ -81,11 +81,11 @@ void main() {
vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0);
float ior = unpackIOR(gr.x);
float transmittance = gr.y;
float surfaceDepth = gr.z;
float surfaceDepth = textureLod(gbufferD, texCoord, 0.0).r;
float d = surfaceDepth * 2.0 - 1.0;
vec4 sceneSample = textureLod(tex, texCoord, 0.0);
if (surfaceDepth == 0.0 || surfaceDepth == 1.0) {
if (gr.z == 0.0 || surfaceDepth == 1.0) {
fragColor = sceneSample;
return;
}

View File

@ -96,41 +96,81 @@ vec3 sampleWaterNormals(vec2 hitXY, float speed, out vec2 tcnor0, out vec2 tcnor
return normalize(n0 + n1 - 1.0);
}
float waveHeight(vec2 xy, float speed) {
return (sin(xy.x * 10.0 / waterDisplace + speed) * cos(xy.y * 10.0 / waterDisplace + speed) + sin(xy.x * 20.0 / waterDisplace + speed * 1.3) * cos(xy.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) / 50.0 * waterDisplace;
}
void main() {
float gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0;
vec3 vray = normalize(viewRay);
float speed = time * 2.0 * waterSpeed;
bool isSky = (gdepth == 1.0);
float farPlane = cameraProj.y / (1.0 - cameraProj.x);
float cosAng = dot(eyeLook, vray);
float domeR = farPlane * 0.94;
float tDome = -3.5 * vray.z + sqrt(max(domeR * domeR - 12.25 * (1.0 - vray.z * vray.z), 0.0));
float domeDepth01 = cameraProj.x + cameraProj.y / (tDome * cosAng);
bool isSky = (gdepth * 0.5 + 0.5) >= domeDepth01;
// Ray-plane intersection with water surface (z = waterLevel)
float denom = dot(vray, vec3(0.0, 0.0, 1.0));
float denom = vray.z;
float tWater = (waterLevel - eye.z) / denom;
bool hasWaterHit = (abs(denom) > 0.0001) && (tWater > 0.0);
if (eye.z < waterLevel) {
vec2 tc = texCoord;
float fogFactor;
if (hasWaterHit && denom > 0.0) {
// Looking up at water surface - apply normal distortion
vec3 hit = eye + tWater * vray;
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(hit.xy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
fogFactor = clamp(tWater * waterDensity, 0.0, 0.95);
} else {
// Looking forward/down - distort via water surface above fragment
vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj);
vec2 wxy = isSky ? (eye.xy + vray.xy * 50.0) : p.xy;
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float waterDist = isSky ? 50.0 : length(p - eye);
fogFactor = clamp(waterDist * waterDensity, 0.0, 0.95);
float depthThick = 1.0 + (waterLevel - eye.z);
vec3 p = vec3(0.0);
float geomDist = farPlane;
if (!isSky) {
p = getPos(eye, eyeLook, vray, gdepth, cameraProj);
geomDist = length(p - eye);
}
bool upRay = hasWaterHit && denom > 0.0;
float waterDist = upRay ? min(tWater, geomDist) : geomDist;
vec2 wxy;
if (upRay && tWater < geomDist) {
wxy = (eye + tWater * vray).xy;
} else if (!isSky) {
wxy = p.xy;
} else {
wxy = eye.xy + vray.xy * 50.0;
}
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float ddepth2 = textureLod(gbufferD, tc, 0.0).r * 2.0 - 1.0;
float dist2 = cameraProj.y / ((ddepth2 * 0.5 + 0.5) - cameraProj.x) / cosAng;
if (dist2 > geomDist + 1.0) tc = texCoord;
vec3 refracted = textureLod(tex, tc, 0.0).rgb;
fragColor.rgb = mix(refracted, waterColor, fogFactor);
vec3 outCol = refracted;
if (upRay && tWater < geomDist) {
outCol *= max(1.0 - length(n2.xy) * 1.5, 0.0);
vec3 h2 = normalize(-vray + ld);
outCol += pow(max(dot(n2, h2), 0.0), 80.0) * 0.6;
}
if (!isSky) {
float fdw = abs(p.z - (waterLevel + waveHeight(p.xy, speed)));
float bandw = 0.1 + waterDist * 0.003;
if (fdw < bandw) {
float foamLod = clamp(waterDist / 15.0, 0.0, 5.0);
vec3 fm0 = textureLod(sfoam, tc0 * 3.0 + vec2(speed / 30.0, speed / 50.0), foamLod).rgb;
vec3 fm1 = textureLod(sfoam, tc1 * 4.0 + vec2(-speed / 35.0, speed / 45.0), foamLod).rgb;
float fstr = clamp(1.0 - fm0.r * 0.5 - fm1.b * 0.5, 0.0, 1.0);
float ffac = clamp(1.0 - fdw / bandw, 0.0, 1.0);
float foamFade = clamp(1.0 - (waterLevel - eye.z) * 0.4, 0.0, 1.0);
vec3 foamCol = mix(waterColor, vec3(1.0), 0.6);
outCol = mix(outCol, foamCol, ffac * fstr * 0.5 * foamFade);
}
}
float fogFactor = 1.0 - exp(-waterDist * waterDensity * 0.006 * depthThick);
outCol = mix(outCol, waterColor, clamp(fogFactor, 0.0, 1.0));
fragColor.rgb = outCol;
fragColor.a = 1.0;
return;
}
@ -144,21 +184,19 @@ void main() {
if (isSky) tWater = min(tWater, 100.0); // Clamp to prevent aliasing at horizon
vec3 p = isSky ? (eye + tWater * vray) : getPos(eye, eyeLook, vray, gdepth, cameraProj);
float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0);
if (!isSky && p.z > waterLevel) {
fragColor = vec4(0.0);
if (!isSky && p.z > waterLevel - 0.01) {
fragColor = textureLod(tex, texCoord, 0.0);
return;
}
float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0);
// Displace surface
float geomZ = p.z; // undisplaced depth for foam/fog tests
p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed)
+ sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5)
/ 50.0 * waterDisplace;
p.z += waveHeight(p.xy, speed);
// Hit plane to determine uvs
vec3 v = normalize(eye - p);
float t = (waterLevel - eye.z) / dot(v, vec3(0.0, 0.0, 1.0));
float t = (waterLevel - eye.z) / v.z;
vec3 hit = eye + t * v;
// Sample normal maps
@ -171,8 +209,8 @@ void main() {
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
} else {
float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0;
vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj);
tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float dist2 = cameraProj.y / ((ddepth * 0.5 + 0.5) - cameraProj.x) / cosAng;
tc = (eye.z + vray.z * dist2) > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
}
// Light
@ -222,26 +260,25 @@ void main() {
#else
fragColor.rgb = mix(refracted, reflectedEnv, waterReflect * fresnel);
#endif
// Water color tint - blend rather than multiply to preserve brightness
float colorMix = isSky ? 0.7 : 0.5;
fragColor.rgb = mix(fragColor.rgb, fragColor.rgb * waterColor, colorMix * horizonFactor);
fragColor.rgb = mix(fragColor.rgb, waterColor, colorMix);
// Blinn-Phong specular using half-vector, faded at horizon
vec3 h = normalize(v + ld);
float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0);
fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0);
// Depth fog - blend toward waterColor with depth, faded at horizon
float depthFog = clamp(-(geomZ - waterLevel) * waterDensity, 0.0, 0.9);
fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor);
fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0) * horizonFactor;
float waterColumn = isSky ? 1.0 : max(length(p - eye) - tWater, 0.0);
float depthFog = 1.0 - exp(-waterColumn * waterDensity * 0.006);
vec3 surfaceCol = mix(waterColor, reflectedEnv, waterReflect * fresnel);
fragColor.rgb = mix(fragColor.rgb, surfaceCol, min(depthFog, 0.9));
// Alpha fades smoothly at horizon instead of hard cut
// Skydome sits 3.5 below the camera (_skydomeMatrix), so its horizon
// appears where the ray z reaches -3.5 / domeRadius
float farPlane = cameraProj.y / (1.0 - cameraProj.x);
float horizonFade = clamp((-3.5 / (farPlane * 0.95) - vray.z) * 20.0, 0.0, 1.0);
fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0, 0.0, 1.0);
// Foam - based on actual geometry depth below water surface
float fd = isSky ? 1.0 : abs(geomZ - waterLevel);
if (fd < 0.1) {
fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0 + 0.1, 0.0, 1.0);
float waveH = p.z - geomZ; // displacement already applied above
float band = 0.1 + tWater * 0.003;
float fd = isSky ? 1.0 : abs(geomZ - (waterLevel + waveH));
if (fd < band) {
// Based on foam by Owen Deery
// http://fire-face.com/personal/water
// Distance-based LOD blurs foam at range to reduce noise
@ -250,8 +287,10 @@ void main() {
vec2 foamUV1 = tcnor1 * 4.0 + vec2(-speed / 35.0, speed / 45.0);
vec3 foamMask0 = textureLod(sfoam, foamUV0, foamLod).rgb;
vec3 foamMask1 = textureLod(sfoam, foamUV1, foamLod).rgb;
float foamStrength = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0);
float fac = (1.0 - (fd * (1.0 / 0.1))) * horizonFactor;
fragColor.rgb = mix(fragColor.rgb, mix(fragColor.rgb, waterColor + 0.2, foamStrength), clamp(fac, 0.0, 1.0) * 0.5);
float foamMask = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0);
float fac = clamp(1.0 - fd / band, 0.0, 1.0);
vec3 foamCol = mix(waterColor, vec3(1.0), 0.6);
fragColor.rgb = mix(fragColor.rgb, foamCol, fac * foamMask * 0.8);
fragColor.a = max(fragColor.a, fac * foamMask);
}
}

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

@ -14,6 +14,7 @@ import iron.math.Vec4;
import iron.math.Mat4;
import iron.data.SceneFormat;
import iron.data.MeshData;
import iron.data.ShaderData;
class Geometry {
#if lnx_deinterleaved
@ -208,7 +209,7 @@ class Geometry {
for (v in 0...vertexBuffers.length)
if (vertexBuffers[v].name == e.name) {
vbs.push(vertexBuffers[v].buffer);
continue;
break;
}
if (e.name == "ipos" && instancedVB != null) {
vbs.push(instancedVB);
@ -264,7 +265,7 @@ class Geometry {
for (i in 0...vaLength)
vertexBuffers[i] = {
name: vertexArrays[i].attrib,
buffer: makeDeinterleavedVB(vertexArrays[i].values, vertexArrays[i].attrib, vertexArrays[i].size)
buffer: makeDeinterleavedVB(vertexArrays[i])
};
#else
@ -295,14 +296,20 @@ class Geometry {
}
#if lnx_deinterleaved
function makeDeinterleavedVB(data: ByteArray, name: String, structLength: Int): VertexBuffer {
function makeDeinterleavedVB(va: TVertexArray): VertexBuffer {
var struct = new VertexStructure();
struct.add(name, structLength == 2 ? VertexData.Short2Norm : VertexData.Short4Norm);
struct.add(va.attrib, ShaderContext.parseData(va.data));
var vertexBuffer = new VertexBuffer(Std.int(data.byteLength / 2 / structLength), struct, usage);
var vcount = verticesCount(va);
var vertexBuffer = new VertexBuffer(vcount, struct, usage);
var stride = Std.int(struct.byteSize() / 2);
var vertices = vertexBuffer.lock();
for (i in 0...Std.int(vertices.byteLength / 2)) vertices.setInt16(i * 2, data.getInt16(i * 2));
var di = -1;
for (i in 0...vcount) {
for (o in 0...va.size) vertices.setInt16(++di * 2, va.values[i * va.size + o]);
for (o in va.size...stride) vertices.setInt16(++di * 2, 0);
}
vertexBuffer.unlock();
return vertexBuffer;

View File

@ -71,30 +71,52 @@ class MeshBatch {
if (b.meshes.length > 0 && b.meshes[0].cullMaterial(context)) continue;
var scontext = b.shader.getContext(context);
if (scontext == null) continue;
g.setPipeline(scontext.pipeState);
// #if lnx_deinterleaved // TODO
// g.setVertexBuffers(b.vertexBuffers);
// #else
#if lnx_deinterleaved
g.setVertexBuffers(b.getVertexBuffers(scontext.raw.vertex_elements));
#else
g.setVertexBuffer(b.getVertexBuffer(scontext.raw.vertex_elements));
// #end
#end
g.setIndexBuffer(b.indexBuffer);
Uniforms.setContextConstants(g, scontext, bindParams);
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);
@ -126,8 +148,12 @@ class Bucket {
public var batched = false;
public var shader: ShaderData;
#if lnx_deinterleaved
public var vertexBuffers: Array<InterleavedVertexBuffer> = [];
#else
var vertexBuffer: VertexBuffer;
var vertexBufferMap: Map<String, VertexBuffer> = new Map();
#end
public var indexBuffer: IndexBuffer;
public var meshes: Array<MeshObject> = [];
@ -137,8 +163,12 @@ class Bucket {
public function remove() {
indexBuffer.delete();
#if lnx_deinterleaved
for (buf in vertexBuffers) buf.buffer.delete();
#else
// this.vertexBuffer is in the map, so it's also deleted here
for (buf in vertexBufferMap) buf.delete();
#end
meshes = [];
}
@ -150,6 +180,20 @@ class Bucket {
meshes.remove(m);
}
#if lnx_deinterleaved
public function getVertexBuffers(elems: Array<TVertexElement>): Array<VertexBuffer> {
var vbs: Array<VertexBuffer> = [];
for (e in elems) {
for (v in vertexBuffers) {
if (v.name == e.name) {
vbs.push(v.buffer);
break;
}
}
}
return vbs;
}
#else
function copyAttribute(attribSize: Int, count: Int, to: ByteArray, toStride: Int, toOffset: Int, from: ByteArray, fromStride: Int, fromOffset: Int) {
for (i in 0...count) {
for (j in 0...attribSize) {
@ -195,6 +239,7 @@ class Bucket {
}
return vb;
}
#end
function vertexCount(g: Geometry, hasUVs: Bool): Int {
var vcount = g.getVerticesLength();
@ -239,6 +284,50 @@ class Bucket {
if (mdatas.length == 0) return;
#if lnx_deinterleaved
var attrNames: Array<String> = [];
var attrReps = new Map<String, TVertexArray>();
for (md in mdatas) {
for (va in md.geom.vertexArrays) {
if (attrReps.get(va.attrib) == null) {
attrReps.set(va.attrib, va);
attrNames.push(va.attrib);
}
}
}
for (name in attrNames) {
var rep = attrReps.get(name);
var attrStruct = new VertexStructure();
attrStruct.add(name, ShaderContext.parseData(rep.data));
var stride = Std.int(attrStruct.byteSize() / 2);
var vtotal = 0;
for (md in mdatas) vtotal += md.geom.getVerticesCount();
var vb = new VertexBuffer(vtotal, attrStruct, Usage.StaticUsage);
var vertices = vb.lock();
var ai = -1;
for (md in mdatas) {
var va: TVertexArray = null;
for (v in md.geom.vertexArrays) {
if (v.attrib == name) {
va = v;
break;
}
}
if (va != null && va.values != null) {
var nverts = md.geom.getVerticesCount();
for (i in 0...nverts) {
for (o in 0...va.size) vertices.setInt16(++ai * 2, va.values[i * va.size + o]);
for (o in va.size...stride) vertices.setInt16(++ai * 2, 0);
}
}
else {
for (i in 0...md.geom.getVerticesCount() * stride) vertices.setInt16(++ai * 2, 0);
}
}
vb.unlock();
vertexBuffers.push({name: name, buffer: vb});
}
#else
// Pick UVs if present
var vs = mdatas[0].geom.struct;
for (md in mdatas) if (md.geom.struct.size() > vs.size()) vs = md.geom.struct;
@ -256,6 +345,7 @@ class Bucket {
var s = "";
for (e in vs.elements) s += e.name;
vertexBufferMap.set(s, vertexBuffer);
#end
indexBuffer = new IndexBuffer(icount, Usage.StaticUsage);
var indices = indexBuffer.lock();

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

@ -102,8 +102,9 @@ class ShaderContext {
constants = [];
textureUnits = [];
var instStruct: VertexStructure = null;
if (instancingType > 0) { // Instancing
var instStruct = new VertexStructure();
instStruct = new VertexStructure();
instStruct.add("ipos", VertexData.Float3);
if (instancingType == 2 || instancingType == 4) {
instStruct.add("irot", VertexData.Float3);
@ -117,11 +118,19 @@ class ShaderContext {
this.instanceStride = Std.int(instStruct.byteSize() / 4);
instStruct.instanced = true;
pipeState.inputLayout = [structure, instStruct];
}
else { // Regular
pipeState.inputLayout = [structure];
#if lnx_deinterleaved
var layouts: Array<VertexStructure> = [];
for (e in structure.elements) {
var s = new VertexStructure();
s.add(e.name, e.data);
layouts.push(s);
}
#else
var layouts = [structure];
#end
if (instStruct != null) layouts.push(instStruct);
pipeState.inputLayout = layouts;
// Depth
pipeState.depthWrite = raw.depth_write;

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

View File

@ -1133,6 +1133,11 @@ class LeenkxExporter:
}
out_object['properties'].append(out_property)
if bobject.lnx_is_occluder:
if 'properties' not in out_object:
out_object['properties'] = []
out_object['properties'].append({'name': 'occluder', 'value': True})
# Export the object reference and material references
objref = bobject.data
@ -2125,7 +2130,8 @@ class LeenkxExporter:
fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled)
assets.add(fp)
# No export necessary
if bobject.data.lnx_cached and os.path.exists(fp) or fp in self.build_cache.exported_mesh_files:
fp_json = fp.split('.lnx')[0] + '.json'
if bobject.data.lnx_cached and (os.path.exists(fp) or os.path.exists(fp_json)) or fp in self.build_cache.exported_mesh_files:
return
# Mesh users have different modifier stack
@ -2202,6 +2208,22 @@ class LeenkxExporter:
if armature:
self.export_skin(bobject, armature, export_mesh, out_mesh)
rpdat = lnx.utils.get_rp()
is_occluder = bobject.lnx_is_occluder
if not is_occluder:
for prop in bobject.lnx_propertylist:
if prop.name_prop == 'occluder' and getattr(prop, prop.type_prop + '_prop') == True:
is_occluder = True
break
use_global = False
if not is_occluder and rpdat is not None and rpdat.lnx_occlusion_all_objects:
is_occluder = rpdat.lnx_occlusion_min_size <= 0.0 or \
max(bobject.dimensions) >= rpdat.lnx_occlusion_min_size
use_global = is_occluder
if is_occluder and rpdat is not None and rpdat.lnx_occlusion_culling:
self.export_occluder_proxy(export_mesh, out_mesh, bobject, use_global)
# Restore the morph state after mesh export
if shape_keys:
bobject.active_shape_key_index = active_shape_key_index
@ -2311,6 +2333,139 @@ class LeenkxExporter:
def export_occluder_proxy(self, export_mesh: bpy.types.Mesh, out_mesh, bobject, use_global=False):
rpdat = lnx.utils.get_rp()
mode = bobject.lnx_occluder_mode
shrink = bobject.lnx_occluder_shrink
if use_global or mode == 'default':
mode = rpdat.lnx_occlusion_proxy_mode
if use_global or shrink < 0.0:
shrink = rpdat.lnx_occlusion_proxy_shrink
if mode == 'box':
proxy_positions, proxy_indices = self._box_proxy(export_mesh)
self._apply_inward_offset(proxy_positions, shrink)
elif mode == 'convex':
proxy_positions, proxy_indices = self._convex_proxy(export_mesh)
self._apply_inward_offset(proxy_positions, shrink)
else:
proxy_positions, proxy_indices = self._apply_decimate_to_mesh(
export_mesh, rpdat, shrink)
if len(proxy_indices) == 0:
return
out_mesh['occluder_proxy'] = {
'positions': proxy_positions,
'indices': proxy_indices
}
def _box_proxy(self, export_mesh: bpy.types.Mesh):
"""12-triangle axis-aligned bounding box proxy in local space."""
if len(export_mesh.vertices) == 0:
return [], []
xs = [v.co.x for v in export_mesh.vertices]
ys = [v.co.y for v in export_mesh.vertices]
zs = [v.co.z for v in export_mesh.vertices]
lo = (min(xs), min(ys), min(zs))
hi = (max(xs), max(ys), max(zs))
proxy_positions = [
lo[0], lo[1], lo[2], hi[0], lo[1], lo[2],
hi[0], hi[1], lo[2], lo[0], hi[1], lo[2],
lo[0], lo[1], hi[2], hi[0], lo[1], hi[2],
hi[0], hi[1], hi[2], lo[0], hi[1], hi[2]]
proxy_indices = [
0, 2, 1, 0, 3, 2,
4, 5, 6, 4, 6, 7,
0, 1, 5, 0, 5, 4,
2, 3, 7, 2, 7, 6,
1, 2, 6, 1, 6, 5,
0, 4, 7, 0, 7, 3]
return proxy_positions, proxy_indices
def _convex_proxy(self, export_mesh: bpy.types.Mesh):
proxy_positions = []
proxy_indices = []
bm = bmesh.new()
try:
bm.from_mesh(export_mesh)
bmesh.ops.convex_hull(bm, input=bm.verts)
remap = {}
for f in bm.faces:
fv = f.verts
for i in range(1, len(fv) - 1):
for v in (fv[0], fv[i], fv[i + 1]):
idx = remap.get(v.index)
if idx is None:
idx = len(remap)
remap[v.index] = idx
co = v.co
proxy_positions.append(co.x)
proxy_positions.append(co.y)
proxy_positions.append(co.z)
proxy_indices.append(idx)
finally:
bm.free()
return proxy_positions, proxy_indices
def _apply_inward_offset(self, positions, shrink_pct):
n = len(positions) // 3
if n == 0 or shrink_pct <= 0.0:
return
factor = 1.0 - shrink_pct * 0.01
cx = sum(positions[0::3]) / n
cy = sum(positions[1::3]) / n
cz = sum(positions[2::3]) / n
for i in range(n):
positions[i * 3] = cx + (positions[i * 3] - cx) * factor
positions[i * 3 + 1] = cy + (positions[i * 3 + 1] - cy) * factor
positions[i * 3 + 2] = cz + (positions[i * 3 + 2] - cz) * factor
def _apply_decimate_to_mesh(self, export_mesh: bpy.types.Mesh, rpdat, shrink):
temp_mesh = export_mesh.copy()
temp_obj = bpy.data.objects.new('lnx_decimate_temp', temp_mesh)
try:
self.scene.collection.objects.link(temp_obj)
mod = temp_obj.modifiers.new(name='lnx_decimate', type='DECIMATE')
mod.decimate_type = rpdat.lnx_occlusion_decimate_mode
if rpdat.lnx_occlusion_decimate_mode == 'COLLAPSE':
mod.ratio = rpdat.lnx_occlusion_decimate_ratio
elif rpdat.lnx_occlusion_decimate_mode == 'DISSOLVE':
mod.angle_limit = math.radians(
rpdat.lnx_occlusion_decimate_ratio * 180)
elif rpdat.lnx_occlusion_decimate_mode == 'UNSUBDIV':
mod.iterations = max(1, int(rpdat.lnx_occlusion_decimate_ratio * 10))
self.depsgraph.update()
eval_obj = temp_obj.evaluated_get(self.depsgraph)
eval_mesh = eval_obj.to_mesh(
preserve_all_data_layers=False, depsgraph=self.depsgraph)
try:
eval_mesh.calc_loop_triangles()
proxy_positions = []
for v in eval_mesh.vertices:
co = v.co
proxy_positions.append(co.x)
proxy_positions.append(co.y)
proxy_positions.append(co.z)
proxy_indices = []
for tri in eval_mesh.loop_triangles:
proxy_indices.append(tri.vertices[0])
proxy_indices.append(tri.vertices[1])
proxy_indices.append(tri.vertices[2])
self._apply_inward_offset(proxy_positions, shrink)
finally:
eval_obj.to_mesh_clear()
finally:
bpy.data.objects.remove(temp_obj, do_unlink=True)
bpy.data.meshes.remove(temp_mesh, do_unlink=True)
return proxy_positions, proxy_indices
def export_light(self, object_ref):
"""Exports a single light object."""
rpdat = lnx.utils.get_rp()
@ -2496,23 +2651,57 @@ class LeenkxExporter:
b = proj[1][1]
c = proj[2][2]
d = proj[2][3]
k = (c - 1.0) / (c + 1.0)
o['fov'] = 2.0 * math.atan(1.0 / b)
if abs(a) > 1e-6:
o['aspect'] = b / a
if with_planes:
o['near_plane'] = (d * (1.0 - k)) / (2.0 * k)
o['far_plane'] = k * o['near_plane']
denom = c + 1.0
k = (c - 1.0) / denom if abs(denom) >= 1e-6 else 0.0
if abs(k) < 1e-6:
o['near_plane'] = 0.1
o['far_plane'] = 1000.0
else:
near = (d * (1.0 - k)) / (2.0 * k)
far = k * near
o['near_plane'] = max(0.001, min(near, 100000.0))
o['far_plane'] = max(0.001, min(far, 100000.0))
@staticmethod
def extract_ortho(o, proj):
a = proj[0][0]
b = proj[1][1]
# left, right, bottom, top
o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \
(1 - proj[3][0]) / proj[0][0], \
-(1 + proj[3][1]) / proj[1][1], \
(1 - proj[3][1]) / proj[1][1]]
o['ortho'] = [-(1 + proj[3][0]) / a, \
(1 - proj[3][0]) / a, \
-(1 + proj[3][1]) / b, \
(1 - proj[3][1]) / b]
o['near_plane'] = (1 + proj[3][2]) / proj[2][2]
o['far_plane'] = -(1 - proj[3][2]) / proj[2][2]
o['near_plane'] *= 2
o['far_plane'] *= 2
if abs(a) > 1e-6:
o['aspect'] = b / a
@staticmethod
def compute_effective_par(rpdat, render):
if rpdat is None:
return render.pixel_aspect_x, render.pixel_aspect_y
mode = rpdat.lnx_aspect_mode
if mode == 'blender':
return render.pixel_aspect_x, render.pixel_aspect_y
elif mode == 'square':
return 1.0, 1.0
elif mode == 'ntsc_43':
return 0.889, 1.0
elif mode == 'pal_43':
return 1.067, 1.0
elif mode == 'ntsc_169':
return 1.212, 1.0
elif mode == 'pal_169':
return 1.459, 1.0
elif mode == 'custom':
return rpdat.lnx_aspect_custom, 1.0
return render.pixel_aspect_x, render.pixel_aspect_y
def export_camera(self, objectRef):
o = {}
@ -2521,12 +2710,16 @@ class LeenkxExporter:
camera = objectRef[1]["objectTable"][0]
render = self.scene.render
rpdat = lnx.utils.get_rp()
par_x, par_y = self.compute_effective_par(rpdat, render)
proj = camera.calc_matrix_camera(
self.depsgraph,
x=render.resolution_x,
y=render.resolution_y,
scale_x=render.pixel_aspect_x,
scale_y=render.pixel_aspect_y)
scale_x=par_x,
scale_y=par_y)
if objref.type == 'PERSP':
self.extract_projection(o, proj)
else:
@ -2594,7 +2787,7 @@ class LeenkxExporter:
self.output['material_datas'].append(o)
bpy.data.materials.remove(mat)
rpdat = lnx.utils.get_rp()
if not rpdat.lnx_culling:
if not rpdat.lnx_backface_culling:
o['override_context'] = {}
o['override_context']['cull_mode'] = 'none'
@ -2673,7 +2866,7 @@ class LeenkxExporter:
o['skip_context'] = material.lnx_skip_context
rpdat = lnx.utils.get_rp()
if material.lnx_two_sided or not rpdat.lnx_culling:
if material.lnx_two_sided or not rpdat.lnx_backface_culling:
o['override_context'] = {}
o['override_context']['cull_mode'] = 'none'
elif material.lnx_cull_mode != 'clockwise':
@ -3190,6 +3383,12 @@ class LeenkxExporter:
'clear_color': self.get_camera_clear_color()
}
render = self.scene.render
rpdat = lnx.utils.get_rp()
par_x, par_y = self.compute_effective_par(rpdat, render)
res_aspect = render.resolution_x / render.resolution_y if render.resolution_y > 0 else 1.0
effective_aspect = res_aspect * par_x / par_y
# Set viewport camera projection
proj, is_persp = self.get_viewport_projection_matrix()
if proj is not None:
@ -3197,6 +3396,9 @@ class LeenkxExporter:
self.extract_projection(out_camera, proj, with_planes=False)
else:
self.extract_ortho(out_camera, proj)
out_camera['aspect'] = effective_aspect
self.output['camera_datas'].append(out_camera)
out_object = {

View File

@ -390,12 +390,14 @@ def send_event(event_id: str, opt_data: Any = None):
elif event_id == 'cam_props':
cam: bpy.types.Camera = obj_data.data
render = bpy.context.scene.render
rpdat = lnx.utils.get_rp()
par_x, par_y = LeenkxExporter.compute_effective_par(rpdat, render)
projection = obj_data.calc_matrix_camera(
bpy.context.evaluated_depsgraph_get(),
x=render.resolution_x,
y=render.resolution_y,
scale_x=render.pixel_aspect_x,
scale_y=render.pixel_aspect_y)
scale_x=par_x,
scale_y=par_y)
focal_y = projection[1][1]
depth_z = projection[2][2]
depth_w = projection[2][3]
@ -403,8 +405,10 @@ def send_event(event_id: str, opt_data: Any = None):
fov = 2.0 * math.atan(1.0 / focal_y)
near = (depth_w * (1.0 - far_near_ratio)) / (2.0 * far_near_ratio)
far = far_near_ratio * near
res_aspect = render.resolution_x / render.resolution_y if render.resolution_y > 0 else 1.0
effective_aspect = res_aspect * par_x / par_y
cam_obj = f'iron.Scene.active.getCamera("{cam.name}")'
js = f'var c = {cam_obj}; var cRaw = c.data.raw; cRaw.fov={fov}; cRaw.near_plane={near}; cRaw.far_plane={far}; cRaw.frustum_culling={"true" if cam.lnx_frustum_culling else "false"}; c.buildProjection();'
js = f'var c = {cam_obj}; var cRaw = c.data.raw; cRaw.fov={fov}; cRaw.near_plane={near}; cRaw.far_plane={far}; cRaw.frustum_culling={"true" if cam.lnx_frustum_culling else "false"}; cRaw.aspect={effective_aspect}; c.buildProjection();'
write_patch(js)
elif event_id == 'obj_visibility':

View File

@ -190,6 +190,9 @@ def build():
rpdat = lnx.utils.get_rp()
project_path = lnx.utils.get_fp()
if rpdat.lnx_frustum_culling:
assets.add_khafile_def('lnx_frustum_culling')
if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpath'] != None:
lnx.api.drivers[rpdat.rp_driver]['make_rpath']()
return
@ -210,6 +213,9 @@ def build():
if rpdat.rp_hdr:
assets.add_khafile_def('rp_hdr')
if rpdat.lnx_occlusion_culling:
assets.add_khafile_def('lnx_occlusion_culling')
assets.add_khafile_def('rp_renderer={0}'.format(rpdat.rp_renderer))
if rpdat.rp_depthprepass:
assets.add_khafile_def('rp_depthprepass')

View File

@ -386,6 +386,10 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo
elif node.sky_type == 'MULTIPLE_SCATTERING':
return parse_sky_multiple_scattering(node, state)
elif node.sky_type == '':
state.curshader.add_uniform('vec3 sunDir', link='_sunPosition')
return c.to_vec3([0.0, 0.0, 0.0])
else:
log.error(f'Unsupported sky model: {node.sky_type}!')
return c.to_vec3([0.0, 0.0, 0.0])

View File

@ -425,6 +425,16 @@ def init_properties():
bpy.types.Object.lnx_sorting_index = compatible_prop(IntProperty, name="Sorting Index", description="Sorting index for the Render's Draw Order", default=0, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_spawn = compatible_prop(BoolProperty, name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_mobile = compatible_prop(BoolProperty, name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_is_occluder = compatible_prop(BoolProperty, name="Is Occluder", description="Export simplified occluder geometry for software occlusion culling", default=False, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_occluder_mode = compatible_prop(EnumProperty,
items=[('convex', 'Convex Hull', 'Convex hull of the mesh - tight on convex shapes, over-covers concave ones'),
('box', 'Box', 'Axis-aligned bounding box - fastest, 12 triangles'),
('decimate', 'Decimate', 'Decimated mesh - closest fit, slowest export')],
name="Occluder Mode",
description="Proxy geometry used for occlusion culling",
default='convex',
override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_occluder_shrink = compatible_prop(FloatProperty, name="Occluder Shrink %", description="Shrink proxy toward its center: 0 = full size, 50 = half size (default), 100 = point", default=50.0, min=0.0, max=100.0, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_visible = compatible_prop(BoolProperty, name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_visible_shadow = compatible_prop(BoolProperty, name="Lighting", description="Object contributes to the lighting even if invisible", default=True, override={'LIBRARY_OVERRIDABLE'})
bpy.types.Object.lnx_soft_body_margin = FloatProperty(name="Soft Body Margin", description="Collision margin", default=0.04)

View File

@ -232,6 +232,15 @@ def update_renderpath(self, context):
assets.invalidate_shader_cache(self, context)
bpy.data.worlds['Lnx'].lnx_recompile = True
def update_occlusion_culling(self, context):
for m in bpy.data.meshes:
m.lnx_cached = False
for c in bpy.data.curves:
c.lnx_cached = False
for c in bpy.data.metaballs:
c.lnx_cached = False
update_renderpath(self, context)
def udpate_shadowmap_cascades(self, context):
bpy.data.worlds['Lnx'].lnx_recompile = True
update_renderpath(self, context)
@ -461,6 +470,7 @@ class LnxRPListItem(bpy.types.PropertyGroup):
('Distance', 'Distance', 'Distance'),
('Index', 'Index', 'Index')],
name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data)
lnx_radix: BoolProperty(name="Radix Sort", description="Sort draw calls with the radix library instead of the comparison sort", default=True, update=update_renderpath)
rp_depth_texture: BoolProperty(name="Depth Texture", description="Current render-path state", default=False)
rp_depth_texture_state: EnumProperty(
items=[('On', 'On', 'On'),
@ -697,8 +707,48 @@ class LnxRPListItem(bpy.types.PropertyGroup):
('CPU', 'CPU', 'CPU'),
('Off', 'Off', 'Off')],
name='Particles', description='Enable particle simulation', default='GPU', update=assets.invalidate_shader_cache)
# Material override flags
lnx_culling: BoolProperty(name="Culling", default=True)
# Culling flags
lnx_backface_culling: BoolProperty(name="Backface Culling", description="Cull back-facing triangles (winding-based)", default=True)
lnx_frustum_culling: BoolProperty(name="Frustum Culling", description="Cull objects outside camera frustum", default=True)
lnx_occlusion_culling: BoolProperty(name="Object Occlusion Culling (Experimental)", description="Software occlusion culling using Hi-Z buffer", default=False, update=update_occlusion_culling)
lnx_occlusion_all_objects: BoolProperty(name="All Objects Occlude", description="Every mesh can occlude - ignores the per-object 'Is Occluder' flag", default=False, update=update_occlusion_culling)
lnx_occlusion_min_size: FloatProperty(name="Min Occluder Size", description="Objects whose largest bounding-box side is smaller than this (meters) never export as occluders", default=0.5, min=0.0, update=update_occlusion_culling)
lnx_occlusion_proxy_mode: EnumProperty(
name="Proxy Mode",
description="Occluder proxy geometry type",
items=[
('box', 'Box', 'Axis-aligned bounding box - fastest, 12 triangles'),
('convex', 'Convex Hull', 'Convex hull of the mesh - tight on convex shapes, over-covers concave ones'),
('decimate', 'Decimate', 'Decimated mesh - closest fit, slowest export'),
],
default='convex', update=update_occlusion_culling)
lnx_occlusion_proxy_shrink: FloatProperty(name="Proxy Shrink %", description="Shrink proxy toward its center: 0 = full size, 50 = half size (default), 100 = point - keeps occluders conservative", default=50.0, min=0.0, max=100.0, precision=1, update=update_occlusion_culling)
lnx_occlusion_box_shrink: IntProperty(name="Box Shrink %", description="0 = full AABB (aggressive), 50 = half-size (default, balanced), 100 = point (no occlusion)", default=50, min=0, max=100, update=update_renderpath)
lnx_occlusion_depth_eps: IntProperty(name="Depth Epsilon", description="6 = 1e-6 (default, tight), higher = more tolerant (less aggressive culling), lower = stricter", default=6, min=1, max=10, update=update_renderpath)
lnx_occlusion_decimate_ratio: FloatProperty(name="Ratio", description="Proportion of edges to collapse (COLLAPSE) or angle threshold in degrees (DISSOLVE) or iterations (UNSUBDIV)", default=0.5, min=0.01, max=1.0, precision=3, update=update_occlusion_culling)
lnx_occlusion_decimate_mode: EnumProperty(
name="Decimate Mode",
description="Decimate method applied to occluder proxy geometry",
items=[
('COLLAPSE', 'Collapse', 'Collapse edges to reduce triangle count by ratio'),
('DISSOLVE', 'Dissolve', 'Dissolve vertices with angle below threshold (ratio = degrees)'),
('UNSUBDIV', 'Unsubdivide', 'Reverse subdivision (ratio = iterations, 0.1 = 1 iter, 1.0 = 10 iter)'),
],
default='COLLAPSE', update=update_occlusion_culling)
lnx_aspect_mode: EnumProperty(
name="Aspect Ratio",
description="Pixel aspect ratio override for camera projection. 'Blender' uses the scene's resolution and pixel aspect settings",
items=[
('blender', 'Blender', 'Use Blender\'s Output Properties resolution and pixel aspect ratio'),
('square', 'Square (1.0)', 'Square pixels (1:1)'),
('ntsc_43', 'NTSC 4:3', 'NTSC pixel aspect ratio (0.889)'),
('pal_43', 'PAL 4:3', 'PAL pixel aspect ratio (1.067)'),
('ntsc_169', 'NTSC 16:9', 'NTSC widescreen pixel aspect ratio (1.212)'),
('pal_169', 'PAL 16:9', 'PAL widescreen pixel aspect ratio (1.459)'),
('custom', 'Custom', 'Custom pixel aspect ratio'),
],
default='blender')
lnx_aspect_custom: FloatProperty(name="Custom PAR", description="Custom pixel aspect ratio (width/height of a single pixel)", default=1.0, min=0.01, max=10.0, precision=3)
lnx_two_sided_area_light: BoolProperty(name="Two-Sided Area Light", description="Emit light from both faces of area plane", default=False, update=assets.invalidate_shader_cache)
@staticmethod

View File

@ -112,6 +112,10 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel):
sub.prop(obj, 'lnx_visible_shadow')
if obj.type == 'MESH':
layout.prop(obj, 'lnx_is_occluder')
if obj.lnx_is_occluder:
layout.prop(obj, 'lnx_occluder_mode')
layout.prop(obj, 'lnx_occluder_shrink')
layout.prop(obj, 'lnx_instanced')
if 'Rot' in obj.lnx_instanced:
layout.prop(obj, 'lnx_instanced_rot_type')
@ -1843,6 +1847,7 @@ class LNX_PT_RenderPathRendererPanel(bpy.types.Panel):
layout.prop(rpdat, 'rp_blending_state')
layout.prop(rpdat, 'rp_depth_texture_state')
layout.prop(rpdat, 'rp_draw_order')
layout.prop(rpdat, 'lnx_radix')
layout.prop(rpdat, 'lnx_samples_per_pixel')
layout.prop(rpdat, 'lnx_texture_filter')
layout.prop(rpdat, 'rp_sss_state')
@ -1878,7 +1883,23 @@ class LNX_PT_RenderPathRendererPanel(bpy.types.Panel):
col = layout.column()
col.prop(rpdat, "rp_hdr")
col.prop(rpdat, "rp_stereo")
col.prop(rpdat, 'lnx_culling')
col.prop(rpdat, 'lnx_backface_culling')
col.prop(rpdat, 'lnx_frustum_culling')
col.prop(rpdat, 'lnx_occlusion_culling')
if rpdat.lnx_occlusion_culling:
col.prop(rpdat, 'lnx_occlusion_all_objects')
if rpdat.lnx_occlusion_all_objects:
col.prop(rpdat, 'lnx_occlusion_min_size')
col.prop(rpdat, 'lnx_occlusion_proxy_mode')
if rpdat.lnx_occlusion_proxy_mode == 'decimate':
col.prop(rpdat, 'lnx_occlusion_decimate_mode')
col.prop(rpdat, 'lnx_occlusion_decimate_ratio')
col.prop(rpdat, 'lnx_occlusion_proxy_shrink')
col.prop(rpdat, 'lnx_occlusion_box_shrink')
col.prop(rpdat, 'lnx_occlusion_depth_eps')
col.prop(rpdat, 'lnx_aspect_mode')
if rpdat.lnx_aspect_mode == 'custom':
col.prop(rpdat, 'lnx_aspect_custom')
col.prop(rpdat, 'rp_pp')

View File

@ -249,6 +249,9 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' +
if not os.path.exists(os.path.join('Libraries', 'lib/hscript')):
khafile.write(add_leenkx_library(sdk_path, 'lib/hscript', rel_path=do_relpath_sdk))
khafile.write("project.addDefine('" + 'hscript' + "');\n")
if lnx.utils.get_rp().lnx_radix:
if not os.path.exists(os.path.join('Libraries', 'lib/radix')):
khafile.write(add_leenkx_library(sdk_path, 'lib/radix', rel_path=do_relpath_sdk))
# Project libraries
if os.path.exists('Libraries'):
@ -522,6 +525,9 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' +
if rpdat.rp_draw_order == 'Index':
assets.add_khafile_def('lnx_draworder_index')
if rpdat.lnx_radix:
assets.add_khafile_def('lnx_radix')
if lnx.utils.get_viewport_controls() == 'azerty':
assets.add_khafile_def('lnx_azerty')
@ -721,6 +727,11 @@ class Main {
f.write("\n" + content_indent + "iron.object.LightObject.cascadeSplitFactor = " + str(rpdat.lnx_shadowmap_split) + ";")
if rpdat.lnx_shadowmap_bounds != 1.0:
f.write("\n" + content_indent + "iron.object.LightObject.cascadeBounds = " + str(rpdat.lnx_shadowmap_bounds) + ";")
if rpdat.lnx_occlusion_culling:
depth_eps = max(10.0 ** -int(rpdat.lnx_occlusion_depth_eps), 1e-6)
f.write("\n" + content_indent + "iron.culling.CullSystem.boxShrink = " + str(rpdat.lnx_occlusion_box_shrink / 100) + ";")
f.write("\n" + content_indent + "iron.culling.CullSystem.depthEps = " + str(depth_eps) + ";")
asset_references = list(set(assets.assets))

View File

@ -123,7 +123,16 @@ def render_envmap(target_dir: str, world: bpy.types.World) -> str:
render_path = os.path.join(target_dir, image_name)
scene.render.filepath = render_path
bpy.ops.render.render(write_still=True, scene=scene.name)
try:
bpy.ops.render.render(write_still=True, scene=scene.name)
except RuntimeError as e:
log.warn(f'Environment map render failed ({e}), writing a grey image instead')
img = bpy.data.images.new('_lnx_noworld', 64, 32)
img.pixels = [0.05] * len(img.pixels)
img.filepath_raw = render_path
img.file_format = ENVMAP_FORMAT
img.save()
bpy.data.images.remove(img)
return image_name

23
lib/radix/LICENSE_1_0.txt Normal file
View File

@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

23
lib/radix/README.md Normal file
View File

@ -0,0 +1,23 @@
# Radix
Haxe port: in-place MSD radix sort (American flag sort) with an insertion sort fallback for small sub-ranges implemented in the Leenkx SDK by Riccardo Mazzucco
Original reference source Malte Skarupke's [ska_sort](https://github.com/skarupke/ska_sort) - https://github.com/skarupke/ska_sort
## Usage
```haxe
import radix.Radix;
// Stable sort: counting-sort scatter between `a` and `buffer`.
// Equal keys keep input order - matches haxe Array.sort semantics.
Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf);
// In-place MSD sort on the same (element, keys) arrays.
// No scratch buffer needed, but NOT stable - equal keys may reorder.
Radix.sortKeyed(meshes, keys);
// Or let the sorter build keys from an extractor
Radix.sort(meshes, function(m) {
return (m.depthRead ? 0x80000000 : 0) | (FPHelper.floatToI32(m.cameraDistance) >>> 1);
});
```

View File

@ -0,0 +1,350 @@
package radix;
import haxe.ds.Vector;
import haxe.io.FPHelper;
/**
In-place MSD radix sort - Haxe port of Malte Skarupke's ska_sort
(ref/ska_sort.hpp, Boost Software License 1.0). Original:
https://github.com/skarupke/ska_sort
**/
class Radix {
static inline var STD_SORT_THRESHOLD = 128;
static inline var AMERICAN_FLAG_SORT_THRESHOLD = 1024;
public static inline function intToUnsigned(i:Int):Int {
return i ^ 0x80000000;
}
public static inline function floatToUnsigned(f:Float):Int {
var u = FPHelper.floatToI32(f);
var signBit = u >> 31;
return u ^ (signBit | 0x80000000);
}
public static inline function uLess(a:Int, b:Int):Bool {
return (a ^ 0x80000000) < (b ^ 0x80000000);
}
public static function sort<T>(a:Array<T>, extractKey:T->Int, ?keys:Array<Int>):Void {
if (keys == null) keys = new Array<Int>();
for (i in 0...a.length) keys[i] = extractKey(a[i]);
sortKeyed(a, keys);
}
public static function sortFloat<T>(a:Array<T>, extractKey:T->Float, ?keys:Array<Int>):Void {
if (keys == null) keys = new Array<Int>();
for (i in 0...a.length) keys[i] = floatToUnsigned(extractKey(a[i]));
sortKeyed(a, keys);
}
public static function sortKeyed<T>(a:Array<T>, keys:Array<Int>):Void {
var n = a.length;
if (stdSortIfLessThanThreshold(a, keys, 0, n)) return;
msdSort(a, keys, 0, n, 24, new RadixScratch());
}
static function msdSort<T>(a:Array<T>, keys:Array<Int>, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void {
var numElements = end - begin;
if (numElements < AMERICAN_FLAG_SORT_THRESHOLD)
americanFlagSort(a, keys, begin, end, shift, scratch);
else
skaByteSort(a, keys, begin, end, shift, scratch);
}
static function stdSortIfLessThanThreshold<T>(a:Array<T>, keys:Array<Int>, begin:Int, end:Int):Bool {
var numElements = end - begin;
if (numElements <= 1) return true;
if (numElements >= STD_SORT_THRESHOLD) return false;
insertionSort(a, keys, begin, end);
return true;
}
static function insertionSort<T>(a:Array<T>, keys:Array<Int>, begin:Int, end:Int):Void {
var i = begin + 1;
while (i < end) {
var key = keys[i];
var v = a[i];
var j = i - 1;
while (j >= begin && uLess(key, keys[j])) {
keys[j + 1] = keys[j];
a[j + 1] = a[j];
j--;
}
keys[j + 1] = key;
a[j + 1] = v;
i++;
}
}
static function americanFlagSort<T>(a:Array<T>, keys:Array<Int>, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void {
var frame = scratch.frame(3 - (shift >> 3));
var offset = frame.offset;
var nextOffset = frame.nextOffset;
var remaining = frame.remaining;
zeroTable(offset);
for (i in begin...end) {
offset[(keys[i] >>> shift) & 0xff]++;
}
var numPartitions = 0;
var total = 0;
for (i in 0...256) {
var count = offset[i];
if (count == 0) continue;
offset[i] = total;
total += count;
nextOffset[i] = total;
remaining[numPartitions] = i;
numPartitions++;
}
if (numPartitions > 1) {
var curIdx = 0;
var cur = remaining[0];
var lastIdx = numPartitions - 1;
var it = begin;
var blockEnd = begin + nextOffset[cur];
var lastElement = end - 1;
var done = false;
while (true) {
var block = (keys[it] >>> shift) & 0xff;
if (block == cur) {
it++;
if (it == lastElement) break;
if (it == blockEnd) {
while (true) {
curIdx++;
if (curIdx == lastIdx) {
done = true;
break;
}
cur = remaining[curIdx];
if (offset[cur] != nextOffset[cur]) break;
}
if (done) break;
it = begin + offset[cur];
blockEnd = begin + nextOffset[cur];
}
}
else {
var t = offset[block];
offset[block] = t + 1;
swapAt(a, keys, it, begin + t);
}
}
}
if (shift > 0) {
var startOffset = 0;
var partitionBegin = begin;
for (i in 0...numPartitions) {
var endOffset = nextOffset[remaining[i]];
var partitionEnd = begin + endOffset;
if (!stdSortIfLessThanThreshold(a, keys, partitionBegin, partitionEnd)) {
msdSort(a, keys, partitionBegin, partitionEnd, shift - 8, scratch);
}
startOffset = endOffset;
partitionBegin = partitionEnd;
}
}
}
static function skaByteSort<T>(a:Array<T>, keys:Array<Int>, begin:Int, end:Int, shift:Int, scratch:RadixScratch):Void {
var frame = scratch.frame(3 - (shift >> 3));
var offset = frame.offset;
var nextOffset = frame.nextOffset;
var remaining = frame.remaining;
zeroTable(offset);
for (i in begin...end) {
offset[(keys[i] >>> shift) & 0xff]++;
}
var numPartitions = 0;
var total = 0;
for (i in 0...256) {
var count = offset[i];
if (count != 0) {
offset[i] = total;
total += count;
remaining[numPartitions] = i;
numPartitions++;
}
nextOffset[i] = total;
}
var lastRemaining = numPartitions;
while (lastRemaining > 1) {
var b = 0;
while (b < lastRemaining && processPending(a, keys, offset, nextOffset, remaining[b], begin, shift)) b++;
var it = b + 1;
while (it < lastRemaining) {
if (processPending(a, keys, offset, nextOffset, remaining[it], begin, shift)) {
var t = remaining[b];
remaining[b] = remaining[it];
remaining[it] = t;
b++;
}
it++;
}
lastRemaining = b;
}
if (shift > 0) {
var i = numPartitions;
while (i > 0) {
i--;
var partition = remaining[i];
var start = partition == 0 ? 0 : nextOffset[partition - 1];
var endOff = nextOffset[partition];
if (!stdSortIfLessThanThreshold(a, keys, begin + start, begin + endOff)) {
msdSort(a, keys, begin + start, begin + endOff, shift - 8, scratch);
}
}
}
}
static inline function newTable():Vector<Int> {
var v = new Vector<Int>(256);
#if !(cpp || hl || java || cs || flash)
for (i in 0...256) v[i] = 0;
#end
return v;
}
static function processPending<T>(a:Array<T>, keys:Array<Int>, offset:Vector<Int>, nextOffset:Vector<Int>, partition:Int, begin:Int, shift:Int):Bool {
var endOff = nextOffset[partition];
if (offset[partition] == endOff) return false;
var it = begin + offset[partition];
var count = endOff - offset[partition];
while (count >= 4) {
placeAt(a, keys, offset, it, begin, shift); it++;
placeAt(a, keys, offset, it, begin, shift); it++;
placeAt(a, keys, offset, it, begin, shift); it++;
placeAt(a, keys, offset, it, begin, shift); it++;
count -= 4;
}
while (count > 0) {
placeAt(a, keys, offset, it, begin, shift);
it++;
count--;
}
return offset[partition] != endOff;
}
static inline function placeAt<T>(a:Array<T>, keys:Array<Int>, offset:Vector<Int>, it:Int, begin:Int, shift:Int):Void {
var p = (keys[it] >>> shift) & 0xff;
var t = offset[p];
offset[p] = t + 1;
swapAt(a, keys, it, begin + t);
}
static inline function zeroTable(v:Vector<Int>):Void {
for (i in 0...256) v[i] = 0;
}
static inline function swapAt<T>(a:Array<T>, keys:Array<Int>, i:Int, j:Int):Void {
var ta = a[i];
a[i] = a[j];
a[j] = ta;
var tk = keys[i];
keys[i] = keys[j];
keys[j] = tk;
}
public static function sortCopy<T>(a:Array<T>, buffer:Array<T>, extractKey:T->Int):Bool {
var numElements = a.length;
if (numElements < STD_SORT_THRESHOLD) {
sort(a, extractKey);
return false;
}
var keys = new Array<Int>();
var bufKeys = new Array<Int>();
for (i in 0...numElements) keys[i] = extractKey(a[i]);
sortCopyKeyed(a, keys, buffer, bufKeys);
return false;
}
public static function sortCopyKeyed<T>(a:Array<T>, keys:Array<Int>, buffer:Array<T>, bufKeys:Array<Int>):Void {
var n = a.length;
var counts0 = newTable();
var counts1 = newTable();
var counts2 = newTable();
var counts3 = newTable();
for (i in 0...n) {
var key = keys[i];
counts0[key & 0xff]++;
counts1[(key >>> 8) & 0xff]++;
counts2[(key >>> 16) & 0xff]++;
counts3[(key >>> 24) & 0xff]++;
}
var total0 = 0, total1 = 0, total2 = 0, total3 = 0;
for (i in 0...256) {
var old0 = counts0[i];
var old1 = counts1[i];
var old2 = counts2[i];
var old3 = counts3[i];
counts0[i] = total0;
counts1[i] = total1;
counts2[i] = total2;
counts3[i] = total3;
total0 += old0;
total1 += old1;
total2 += old2;
total3 += old3;
}
for (i in 0...n) {
var b = keys[i] & 0xff;
var t = counts0[b];
counts0[b] = t + 1;
buffer[t] = a[i];
bufKeys[t] = keys[i];
}
for (i in 0...n) {
var b = (bufKeys[i] >>> 8) & 0xff;
var t = counts1[b];
counts1[b] = t + 1;
a[t] = buffer[i];
keys[t] = bufKeys[i];
}
for (i in 0...n) {
var b = (keys[i] >>> 16) & 0xff;
var t = counts2[b];
counts2[b] = t + 1;
buffer[t] = a[i];
bufKeys[t] = keys[i];
}
for (i in 0...n) {
var b = (bufKeys[i] >>> 24) & 0xff;
var t = counts3[b];
counts3[b] = t + 1;
a[t] = buffer[i];
keys[t] = bufKeys[i];
}
}
}
private class RadixFrame {
public var offset:Vector<Int>;
public var nextOffset:Vector<Int>;
public var remaining:Vector<Int>;
public function new() {
offset = new Vector<Int>(256);
nextOffset = new Vector<Int>(256);
remaining = new Vector<Int>(256);
}
}
private class RadixScratch {
public var frames:Array<RadixFrame>;
public function new() {
frames = [];
}
public inline function frame(level:Int):RadixFrame {
var f = frames[level];
if (f == null) {
f = new RadixFrame();
frames[level] = f;
}
return f;
}
}

11
lib/radix/haxelib.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "radix",
"url": "https://github.com/skarupke/ska_sort",
"license": "BSL-1.0",
"tags": ["sort", "radix", "msd", "in-place"],
"description": "Haxe port : in-place MSD radix sort (American flag sort) for Leenkx SDK from Malte Skarupke's ska_sort with insertion sort fallback and stable LSD copy path",
"version": "1.0.0",
"classPath": "Sources/",
"releasenote": "Leenkx HAXE port",
"contributors": ["Riccardo Mazzucco"]
}