import nape.util.BitmapDebug; import nape.geom.Vec2; class Perlin3D { public static inline function noise(x:Float, y:Float = 0.0, z:Float = 0.0) { var X:Int = untyped __int__(x); x -= X; X &= 0xff; var Y:Int = untyped __int__(y); y -= Y; Y &= 0xff; var Z:Int = untyped __int__(z); z -= Z; Z &= 0xff; var u = fade(x); var v = fade(y); var w = fade(z); var A = p(X) +Y; var AA = p(A)+Z; var AB = p(A+1)+Z; var B = p(X+1)+Y; var BA = p(B)+Z; var BB = p(B+1)+Z; return lerp(w, lerp(v, lerp(u, grad(p(AA ), x , y , z ), grad(p(BA ), x-1, y , z )), lerp(u, grad(p(AB ), x , y-1, z ), grad(p(BB ), x-1, y-1, z ))), lerp(v, lerp(u, grad(p(AA+1), x , y , z-1 ), grad(p(BA+1), x-1, y , z-1 )), lerp(u, grad(p(AB+1), x , y-1, z-1 ), grad(p(BB+1), x-1, y-1, z-1 )))); } static inline function fade(t:Float) return t*t*t*(t*(t*6-15)+10); static inline function lerp(t:Float, a:Float, b:Float) return a + t*(b-a); static inline function grad(hash:Int, x:Float, y:Float, z:Float) { var h = hash&15; var u = h<8 ? x : y; var v = h<4 ? y : h==12||h==14 ? x : z; return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v); } static inline function p(i:Int) return perm[i]; static var perm: haxe.ds.Vector; public static function initNoise() { perm = new haxe.ds.Vector(512); var p = [151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; for(i in 0...256) { perm[i] = p[i]; perm[256+i] = p[i]; } } } typedef Plane = Array; typedef AABB = Array; typedef Occluder = { aabb: AABB, distance: Float }; enum Tree { ACell(aabb:AABB, l0: Null, l1: Null, l2: Null, l3: Null); ALeaf(aabb:AABB, v:Occluder); } class Main { static function main() { Perlin3D.initNoise(); var perlinScale = 0.01; var threshold = 0.0; var dim = 16; var sw = 1024; var sh = 768; var w = Math.floor(sw / dim); var h = Math.floor(sh / dim); var chunks = [for (y in 0...h) for (x in 0...w) { var data = [for (j in 0...dim) for (i in 0...dim) { Perlin3D.noise((x * dim + i) * perlinScale, (y * dim + j) * perlinScale, 1.5) < threshold; }]; var opaque = [true, true, true, true]; // x+ x-, y+, y- for (i in 0...dim) { opaque[0] = opaque[0] && data[i * dim + (dim - 1)]; opaque[1] = opaque[1] && data[i * dim]; opaque[2] = opaque[2] && data[i + (dim - 1) * dim]; opaque[3] = opaque[3] && data[i]; } { opaque: opaque, data: data }; }]; var map = new flash.display.BitmapData(sw, sh, false, 0xffffff); for (y in 0...sh) for (x in 0...sw) { var chunkX = Std.int(x / dim); var chunkY = Std.int(y / dim); var blockX = x % dim; var blockY = y % dim; var chunk = chunks[chunkY * w + chunkX]; if (chunk.data[blockY * dim + blockX]) map.setPixel(x, y, (chunkX + chunkY) % 2 == 0 ? 0xa0a0a0 : 0xb0b0b0); else map.setPixel(x, y, 0xffffff); if (blockX == (dim - 1) && chunk.opaque[0]) map.setPixel(x, y, 0xff8888); if (blockX == 0 && chunk.opaque[1]) map.setPixel(x, y, 0x88ffff); if (blockY == (dim - 1) && chunk.opaque[2]) map.setPixel(x, y, 0x88ff88); if (blockY == 0 && chunk.opaque[3]) map.setPixel(x, y, 0xff88ff); } var bit; flash.Lib.current.addChild(bit = new flash.display.Bitmap(map)); bit.alpha = 0.5; var debug = new BitmapDebug(sw, sh, 0xffffff, true); flash.Lib.current.addChild(debug.display); function plane(u:Vec2, v:Vec2):Plane { var a = (v.y - u.y); var b = (u.x - v.x); var c = -(a * u.x + b * u.y); return [a, b, c]; } function inside(v:Vec2, p:Plane) { return v.x * p[0] + v.y * p[1] + p[2] >= 0; } function aabbInside(aabb:AABB, frustum:Array) { for (p in frustum) { if ((p[0] * (p[0] < 0 ? aabb[0] : aabb[2]) + p[1] * (p[1] < 0 ? aabb[1] : aabb[3])) + p[2] < 0) { return false; } } return true; } function clipAABB(aabb:AABB, frustum:Array) { // check for each edge (face in 3d) of AABB, how far along the axis // can move 'all' points so that AABB is minimal, but covers the area of // aabb intersecting frustum. function clipX(i, p:Plane) { if (p[0] == 0) return; var d1 = p[0] * aabb[i] + p[1] * aabb[1] + p[2]; var d2 = p[0] * aabb[i] + p[1] * aabb[3] + p[2]; if (d1 < 0 && d2 < 0) { // can clip var n0 = (-p[2] - p[1]*aabb[1]) / p[0] - aabb[i]; var n1 = (-p[2] - p[1]*aabb[3]) / p[0] - aabb[i]; var n = (n0 * n0 < n1 * n1 ? n0 : n1); aabb[i] += n; } } function clipY(i, p:Plane) { if (p[1] == 0) return; var d1 = p[0] * aabb[0] + p[1] * aabb[i] + p[2]; var d2 = p[0] * aabb[2] + p[1] * aabb[i] + p[2]; if (d1 < 0 && d2 < 0) { // can clip var n0 = (-p[2] - p[0]*aabb[0]) / p[1] - aabb[i]; var n1 = (-p[2] - p[0]*aabb[2]) / p[1] - aabb[i]; var n = (n0 * n0 < n1 * n1 ? n0 : n1); aabb[i] += n; } } for (p in frustum) { clipX(0, p); clipX(2, p); clipY(1, p); clipY(3, p); } } function aabbTotallyInside(aabb:AABB, frustum:Array) { for (p in frustum) { if ((p[0] * (p[0] > 0 ? aabb[0] : aabb[2]) + p[1] * (p[1] > 0 ? aabb[1] : aabb[3])) + p[2] < 0) { return false; } } return true; } // assumption made for this application, that y is 'smaller' than x in all cases function aabbContains(x:AABB, y:AABB) { return y[0] >= x[0] && y[1] >= x[1] && y[2] <= x[2] && y[3] <= x[3]; } function drawFrustum(frustum:Array, colour:Int) { function intersect(a: Plane, b: Plane) { var det = a[0] * b[1] - a[1] * b[0]; if (det == 0) return null; var invDet = 1 / det; return Vec2.get( (a[1]*b[2] - a[2]*b[1]) * invDet, (a[2]*b[0] - a[0]*b[2]) * invDet ); } for (p in frustum) { var list = []; var p0, p1; if (p[1] != 0) { var y0 = -p[2]/p[1]; var y1 = (-p[2]-p[0]*sw)/p[1]; p0 = Vec2.get(0, y0); p1 = Vec2.get(sw, y1); } else { var x0 = -p[2]/p[0]; var x1 = (-p[2]-p[1]*sh)/p[0]; p0 = Vec2.get(x0, 0); p1 = Vec2.get(x1, sh); } for (q in frustum) if (p != q) { if (p0 != null && !inside(p0, q)) { p0 = null; } if (p1 != null && !inside(p1, q)) { p1 = null; } var int = intersect(p, q); if (int != null) { for (r in frustum) if (r != p && r != q) { if (!inside(int, r)) { int = null; break; } } } if (int != null) { list.push(int); } } if (p0 != null) { list.push(p0); } if (p1 != null) { list.push(p1); } list.sort(function (a, b) { var del = (a.x * p[1] - a.y * p[0]) - (b.x * p[1] - b.y * p[0]); return del < 0 ? -1 : del > 0 ? 1 : 0; }); debug.drawLine(list[0], list[list.length - 1], colour); } } function napeAABB(aabb:AABB) { return new nape.geom.AABB(aabb[0], aabb[1], aabb[2] - aabb[0], aabb[3] - aabb[1]); } function shadowFrustum(aabb:AABB, focus:Vec2):Array { // voronoi region check, probably slower for 3d and missing a trick here. var p0, p1; if (focus.x < aabb[0] && focus.y < aabb[1]) { p0 = Vec2.get(aabb[2], aabb[1]); p1 = Vec2.get(aabb[0], aabb[3]); } else if (focus.x < aabb[0] && focus.y > aabb[3]) { p0 = Vec2.get(aabb[0], aabb[1]); p1 = Vec2.get(aabb[2], aabb[3]); } else if (focus.x > aabb[2] && focus.y < aabb[1]) { p0 = Vec2.get(aabb[2], aabb[3]); p1 = Vec2.get(aabb[0], aabb[1]); } else if (focus.x > aabb[2] && focus.y > aabb[3]) { p0 = Vec2.get(aabb[0], aabb[3]); p1 = Vec2.get(aabb[2], aabb[1]); } else if (focus.x < aabb[0]) { p0 = Vec2.get(aabb[0], aabb[1]); p1 = Vec2.get(aabb[0], aabb[3]); } else if (focus.x > aabb[2]) { p0 = Vec2.get(aabb[2], aabb[3]); p1 = Vec2.get(aabb[2], aabb[1]); } else if (focus.y < aabb[1]) { p0 = Vec2.get(aabb[2], aabb[1]); p1 = Vec2.get(aabb[0], aabb[1]); } else { p0 = Vec2.get(aabb[0], aabb[3]); p1 = Vec2.get(aabb[2], aabb[3]); } return [ plane(p0, focus), plane(p0, p1), plane(focus, p1) ]; } function chunkAABB(x, y):AABB { return [ x * dim, y * dim, (x + 1) * dim, (y + 1) * dim ]; } var previousCamera:Vec2 = null; flash.Lib.current.stage.addEventListener(flash.events.MouseEvent.MOUSE_MOVE, function (_) { debug.clear(); var camera = Vec2.get(flash.Lib.current.mouseX, flash.Lib.current.mouseY); if (previousCamera == null) { previousCamera = Vec2.get(); previousCamera.set(camera); return; } var direction = camera.sub(previousCamera); previousCamera.addeq(camera.sub(previousCamera).muleq(0.04)); if (direction.lsq() == 0 || direction.x == 0 || direction.y == 0) { return; } direction.length = 1; var fov = 70; var cameraFrustum = []; direction.angle -= fov / 2; cameraFrustum.push(plane(camera, camera.add(direction))); direction.angle += fov; cameraFrustum.push(plane(camera.add(direction), camera)); direction.angle -= fov / 2; cameraFrustum.push(plane( camera.add(direction.mul(20)), camera.add(direction.perp()).add(direction.mul(20)))); cameraFrustum.push(plane( camera.add(direction.perp()).add(direction.mul(400)), camera.add(direction.mul(400)))); debug.drawCircle(camera, 2, 0xff0000); drawFrustum(cameraFrustum, 0xff0000); var visible = []; // list var occluders = []; // full xy for (y in 0...h) for (x in 0...w) { var chunk = chunks[y * w + x]; var aabb = chunkAABB(x, y); if (aabbInside(aabb, cameraFrustum)) { var orig = [aabb[0], aabb[1], aabb[2], aabb[3]]; clipAABB(aabb, cameraFrustum); var v; var occluder = true; if (camera.x < aabb[0]) { occluder = occluder && chunk.opaque[1]; } if (camera.x > aabb[2]) { occluder = occluder && chunk.opaque[0]; } if (camera.y < aabb[1]) { occluder = occluder && chunk.opaque[3]; } if (camera.y > aabb[3]) { occluder = occluder && chunk.opaque[2]; } if (occluder) occluders.push({ aabb: aabb, distance: Math.pow(camera.x - (aabb[0] + aabb[2]) / 2, 2) + Math.pow(camera.y - (aabb[1] + aabb[3]) / 2, 2) }); else occluders.push(null); visible.push({ aabb: aabb, orig: orig, renderable: true }); } else { occluders.push(null); } } // Iterative combiner // Start at occluder closest to camera, and expand out from camera to find largest occluding rectangle // occluder does not have to be a covering, it can expand out into non-occluder territory as long as // such territory is hidden from the camera already. var ocs = []; for (o in occluders) if (o != null) ocs.push(o); ocs.sort(function (a, b) return a.distance < b.distance ? -1 : 1); var occ = []; while (ocs.length != 0) { var seed = ocs.shift().aabb; var x0 = Std.int((seed[0] + seed[2]) / (2 * dim)); var y0 = Std.int((seed[1] + seed[3]) / (2 * dim)); if (occluders[y0 * w + x0] == null) continue; var x1 = x0; var y1 = y0; // determine directions we should try and expand to given resultant frustum if (camera.x < seed[0] && camera.y < seed[1]) { // search +x +y while (x1 < w-1 && occluders[y0 * w + x1 + 1] != null) x1++; while (y1 < h-1 && occluders[(y1 + 1) * w + x0] != null) y1++; } else if (camera.x < seed[0] && camera.y > seed[3]) { // search +x -y while (x1 < w-1 && occluders[y0 * w + x1 + 1] != null) x1++; while (y0 > 0 && occluders[(y0 - 1) * w + x0] != null) y0--; } else if (camera.x > seed[2] && camera.y < seed[1]) { // search -x +y while (x0 > 0 && occluders[y0 * w + x0 - 1] != null) x0--; while (y1 < h-1 && occluders[(y1 + 1) * w + x1] != null) y1++; } else if (camera.x > seed[2] && camera.y > seed[3]) { // search -x -y while (x0 > 0 && occluders[y0 * w + x0 - 1] != null) x0--; while (y0 > 0 && occluders[(y0 - 1) * w + x1] != null) y0--; } else if (camera.x < seed[0] || camera.x > seed[2]) { // search +-y while (y0 > 0 && occluders[(y0 - 1) * w + x0] != null) y0--; while (y1 < h-1 && occluders[(y1 + 1) * w + x1] != null) y1++; } else { // search +-x while (x0 > 0 && occluders[y0 * w + x0 - 1] != null) x0--; while (x1 < w-1 && occluders[y0 * w + x1 + 1] != null) x1++; } occ.push({ aabb: ([x0 * dim, y0 * dim, x1 * dim + dim, y1 * dim + dim] : Array) }); for (y in y0...y1+1) { for (x in x0...x1+1) { occluders[y * w + x] = null; } } } var ocs = occ; for (k in 0...ocs.length) { var v = ocs[k]; ocs[k] = null; if (v == null) continue; var frustum = shadowFrustum(v.aabb, camera); debug.drawAABB(napeAABB(v.aabb), 0xff00ff); var i = 0; while (i < visible.length) { var q = visible[i]; if (aabbContains(v.aabb, q.aabb) || aabbTotallyInside(q.aabb, frustum)) { q.renderable = false; visible[i] = visible[visible.length - 1]; visible.pop(); } else { i++; } } for (i in 0...ocs.length) { if (ocs[i] != null && ocs[i] != v && aabbTotallyInside(ocs[i].aabb, frustum)) { ocs[i] = null; } } } for (v in visible) { if (v.renderable) { debug.drawAABB(napeAABB([v.orig[0]+1,v.orig[1]+1,v.orig[2]-1,v.orig[3]-1]), 0xff0000); } } debug.flush(); }); } }