Selaa lähdekoodia

moved terrain from heaps

ncannasse 6 vuotta sitten
vanhempi
commit
b324f66205

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@
 *.lnk
 /bin/*.map
 /bin/shaders
+/out.hl

+ 1 - 1
hide/Ide.hx

@@ -950,7 +950,7 @@ class Ide {
 
 	static function main() {
 		h3d.pass.ShaderManager.STRICT = false; // prevent errors with bad renderer
-		hide.tools.Macros.include(["hide.view","h3d.prim","h3d.scene","h3d.pass","hide.prefab","hxd.prefab","hrt"]);
+		hide.tools.Macros.include(["hide.view","h3d.prim","h3d.scene","h3d.pass","hide.prefab","hrt"]);
 		new Ide();
 	}
 

+ 2 - 2
hide/prefab/terrain/Brush.hx

@@ -65,7 +65,7 @@ class BrushMode {
 
 class BrushPreview {
 
-	var terrain : h3d.scene.pbr.terrain.Terrain;
+	var terrain : hrt.prefab.terrain.TerrainMesh;
 	var tiles : Array<TilePreviewMesh> = [];
 	var grid : h3d.prim.Grid;
 
@@ -146,6 +146,6 @@ class TilePreviewMesh extends h3d.scene.Mesh {
 	override function sync(ctx : h3d.scene.RenderContext) {
 		shader.heightMap = heightMap;
 		shader.heightMapSize = heightMap.width;
-		shader.primSize = Std.instance(parent, h3d.scene.pbr.terrain.Terrain).tileSize;
+		shader.primSize = Std.instance(parent, hrt.prefab.terrain.TerrainMesh).tileSize;
 	}
 }

+ 3 - 3
hide/prefab/terrain/TerrainEditor.hx

@@ -12,7 +12,7 @@ enum RenderMode {
 
 class TerrainRevertData {
 	public var surfaceIndex : Int;
-	public var surface : h3d.scene.pbr.terrain.Surface;
+	public var surface : hrt.prefab.terrain.Surface;
 	public function new(){
 
 	}
@@ -36,7 +36,7 @@ class TileRevertData {
 class TerrainEditor {
 
 	public var currentBrush : Brush;
-	public var currentSurface : h3d.scene.pbr.terrain.Surface;
+	public var currentSurface : hrt.prefab.terrain.Surface;
 	public var tmpTexPath : String;
 	public var textureType = ["_Albedo", "_Normal", "_MetallicGlossAO"];
 	public var autoCreateTile = false;
@@ -58,7 +58,7 @@ class TerrainEditor {
 
 	var terrainPrefab : hrt.prefab.terrain.Terrain;
 	var undo : hide.ui.UndoHistory;
-	var tileTrashBin : Array<h3d.scene.pbr.terrain.Tile> = [];
+	var tileTrashBin : Array<hrt.prefab.terrain.Tile> = [];
 	var paintRevertDatas : Array<TileRevertData> = [];
 	var uvTexPixels : hxd.Pixels.PixelsFloat;
 	var uvTex : h3d.mat.Texture;

+ 218 - 0
hrt/prefab/terrain/Shader.hx

@@ -0,0 +1,218 @@
+package hrt.prefab.terrain;
+
+class Shader extends hxsl.Shader {
+
+	static var SRC = {
+
+		@:import h3d.shader.BaseMesh;
+		@const var SHOW_GRID : Bool;
+		@const var SURFACE_COUNT : Int;
+		@const var CHECKER : Bool;
+		@const var COMPLEXITY : Bool;
+		@const var PARALLAX : Bool;
+
+		@param var heightMapSize : Float;
+		@param var primSize : Float;
+		@param var cellSize : Float;
+
+		@param var albedoTextures : Sampler2DArray;
+		@param var normalTextures : Sampler2DArray;
+		@param var pbrTextures : Sampler2DArray;
+		@param var weightTextures : Sampler2DArray;
+		@param var surfaceIndexMap : Sampler2D;
+		@param var heightMap : Sampler2D;
+		@param var surfaceParams : Array<Vec4, SURFACE_COUNT>;
+		@param var secondSurfaceParams : Array<Vec4, SURFACE_COUNT>;
+
+		@param var heightBlendStrength : Float;
+		@param var blendSharpness : Float;
+
+		@param var parallaxAmount : Float;
+		@param var minStep : Int;
+		@param var maxStep : Int;
+		@param var tileIndex : Vec2;
+
+		var calculatedUV : Vec2;
+		var terrainUV : Vec2;
+		var TBN : Mat3;
+
+		var emissiveValue : Float;
+		var metalnessValue : Float;
+		var roughnessValue : Float;
+		var occlusionValue : Float;
+
+		var tangentViewPos : Vec3;
+		var tangentFragPos : Vec3;
+
+		function vertex() {
+			calculatedUV = input.position.xy / primSize;
+			var terrainUV = (calculatedUV * (heightMapSize - 1)) / heightMapSize;
+			terrainUV += 0.5 / heightMapSize;
+			transformedPosition += (vec3(0,0, textureLod(heightMap, terrainUV, 0).r) * global.modelView.mat3());
+			TBN = mat3(normalize(cross(transformedNormal, vec3(0,1,0))), normalize(cross(transformedNormal,vec3(-1,0,0))), transformedNormal);
+			tangentViewPos = camera.position * TBN;
+			tangentFragPos = transformedPosition * TBN;
+		}
+
+		function getWeight( i : Vec3,  uv : Vec2 ) : Vec3 {
+			var weight = vec3(0);
+			weight.x = weightTextures.getLod(vec3(uv, i.x), 0).r;
+			if( i.y != i.x ) weight.y = weightTextures.getLod(vec3(uv, i.y), 0).r;
+			if( i.z != i.x ) weight.z = weightTextures.getLod(vec3(uv, i.z), 0).r;
+			return weight;
+		}
+
+		function getDepth( i : Vec3,  uv : Vec2 ) : Vec3 {
+			var depth = vec3(0);
+			if( w.x > 0 ) depth.x = pbrTextures.getLod(getsurfaceUV(i.x, uv), 0).a;
+			if( w.y > 0 ) depth.y = pbrTextures.getLod(getsurfaceUV(i.y, uv), 0).a;
+			if( w.z > 0 ) depth.z = pbrTextures.getLod(getsurfaceUV(i.z, uv), 0).a;
+			return 1 - depth;
+		}
+
+		var w : Vec3;
+		var i : Vec3;
+		function getPOMUV( i : Vec3, uv : Vec2 ) : Vec2 {
+			if( !PARALLAX )
+				return uv;
+			else {
+				var viewDir = normalize(tangentViewPos - tangentFragPos);
+				var numLayers = mix(float(maxStep), float(minStep), viewDir.dot(transformedNormal));
+				var layerDepth = 1 / numLayers;
+				var curLayerDepth = 0.;
+				var delta = (viewDir.xy / viewDir.z) * parallaxAmount / numLayers;
+				var curUV = uv;
+				var depth = getDepth(i, curUV);
+				var curDepth = depth.dot(w);
+				var prevDepth = 0.;
+				while( curLayerDepth < curDepth ) {
+					curUV += delta;
+					prevDepth = curDepth;
+					i = surfaceIndexMap.getLod(curUV, 0).rgb * 255;
+					w = getWeight(i, curUV);
+					depth = getDepth(i, curUV);
+					curDepth = depth.dot(w);
+					curLayerDepth += layerDepth;
+				}
+				var prevUV = curUV - delta;
+				var after = curDepth - curLayerDepth;
+				var before = prevDepth - curLayerDepth + layerDepth;
+				var pomUV = mix(curUV, prevUV,  after / (after - before));
+				return pomUV;
+			}
+		}
+
+		function getsurfaceUV( i : Float, uv : Vec2 ) : Vec3 {
+			var id = int(i);
+			var angle = surfaceParams[id].w;
+			var offset = vec2(surfaceParams[id].y, surfaceParams[id].z);
+			var tilling = surfaceParams[id].x;
+			var worldUV = (uv + tileIndex) * tilling + offset;
+			var res = vec2( worldUV.x * cos(angle) - worldUV.y * sin(angle) , worldUV.y * cos(angle) + worldUV.x * sin(angle));
+			var surfaceUV = vec3(res % 1, i);
+			return surfaceUV;
+		}
+
+		function fragment() {
+
+			if( CHECKER ) {
+				var tile = abs(abs(floor(input.position.x)) % 2 - abs(floor(input.position.y)) % 2);
+				pixelColor = vec4(mix(vec3(0.4), vec3(0.1), tile), 1.0);
+				transformedNormal = vec3(0,0,1) * TBN;
+				roughnessValue = mix(0.9, 0.6, tile);
+				metalnessValue = mix(0.4, 0, tile);
+				occlusionValue = 1;
+				emissiveValue = 0;
+			}
+			else if( COMPLEXITY ) {
+				var blendCount = 0 + weightTextures.get(vec3(0)).r * 0;
+				for(i in 0 ... SURFACE_COUNT)
+					blendCount += ceil(weightTextures.get(vec3(calculatedUV, i)).r);
+				pixelColor = vec4(mix(vec3(0,1,0), vec3(1,0,0), blendCount / 3.0) , 1);
+				transformedNormal = vec3(0,0,1) * TBN;
+				emissiveValue = 1;
+				roughnessValue = 1;
+				metalnessValue = 0;
+				occlusionValue = 1;
+			}
+			else {
+				i = surfaceIndexMap.get(calculatedUV).rgb * 255;
+				w = getWeight(i, calculatedUV);
+				var pomUV = getPOMUV(i, calculatedUV);
+				if( PARALLAX ) {
+					i = surfaceIndexMap.get(pomUV).rgb * 255;
+					w = getWeight(i, pomUV);
+				}
+				var h = vec3(0);
+				var surfaceUV1 = getsurfaceUV(i.x, pomUV);
+				var surfaceUV2 = getsurfaceUV(i.y, pomUV);
+				var surfaceUV3 = getsurfaceUV(i.z, pomUV);
+				var pbr1 = vec4(0), pbr2 = vec4(0), pbr3 = vec4(0);
+				if( w.x > 0 ) pbr1 = pbrTextures.get(surfaceUV1).rgba;
+				if( w.y > 0 ) pbr2 = pbrTextures.get(surfaceUV2).rgba;
+				if( w.z > 0 ) pbr3 = pbrTextures.get(surfaceUV3).rgba;
+
+				// Height Blend
+				var h = vec3( 	secondSurfaceParams[i.x].x + pbr1.a * (secondSurfaceParams[i.x].y - secondSurfaceParams[i.x].x),
+								secondSurfaceParams[i.y].x + pbr2.a * (secondSurfaceParams[i.y].y - secondSurfaceParams[i.y].x),
+								secondSurfaceParams[i.z].x + pbr3.a * (secondSurfaceParams[i.z].y - secondSurfaceParams[i.z].x));
+				w = mix(w, vec3(w.x * h.x, w.y * h.y, w.z * h.z), heightBlendStrength);
+
+				// Sharpness
+				var m = max(w.x, max(w.y, w.z));
+				var mw = ceil(w - m + 0.01);
+				w = mix(w, mw, blendSharpness);
+
+				// Blend
+				var albedo = vec3(0);
+				var normal = vec4(0);
+				var pbr = vec4(0);
+				if( w.x > 0 ) {
+					albedo += albedoTextures.get(surfaceUV1).rgb * w.x;
+					normal += normalTextures.get(surfaceUV1).rgba * w.x;
+					pbr += pbr1 * w.x;
+				}
+				if( w.y > 0 ) {
+					albedo += albedoTextures.get(surfaceUV2).rgb * w.y;
+					normal += normalTextures.get(surfaceUV2).rgba * w.y;
+					pbr += pbr2 * w.y;
+				}
+				if( w.z > 0 ) {
+					albedo += albedoTextures.get(surfaceUV3).rgb * w.z;
+					normal += normalTextures.get(surfaceUV3).rgba * w.z;
+					pbr += pbr3 * w.z;
+				}
+				var wSum = w.x + w.y + w.z;
+				albedo /= wSum;
+				pbr /= wSum;
+				normal /= wSum;
+
+				// Output
+				normal = vec4(unpackNormal(normal), 0.0);
+				pixelColor = vec4(albedo, 1.0);
+				transformedNormal = normalize(normal.xyz) * TBN;
+				roughnessValue = 1 - pbr.g * pbr.g;
+				metalnessValue = pbr.r;
+				occlusionValue = pbr.b;
+				emissiveValue = 0;
+			}
+
+			if( SHOW_GRID ) {
+				var gridColor = vec4(1,0,0,1);
+				var tileEdgeColor = vec4(1,1,0,1);
+				var grid : Vec2 = ((input.position.xy.mod(cellSize) / cellSize ) - 0.5) * 2.0;
+				grid = ceil(max(vec2(0), abs(grid) - 0.9));
+				var tileEdge = max( (1 - ceil(input.position.xy / primSize - 0.1 / (primSize / cellSize) )), floor(input.position.xy / primSize + 0.1 / (primSize / cellSize)));
+				emissiveValue = max(max(grid.x, grid.y), max(tileEdge.x, tileEdge.y));
+				pixelColor = mix( pixelColor, gridColor, clamp(0,1,max(grid.x, grid.y)));
+				pixelColor = mix( pixelColor, tileEdgeColor, clamp(0,1,max(tileEdge.x, tileEdge.y)));
+				metalnessValue =  mix(metalnessValue, 0, emissiveValue);
+				roughnessValue = mix(roughnessValue, 1, emissiveValue);
+				occlusionValue = mix(occlusionValue, 1, emissiveValue);
+				transformedNormal = mix(transformedNormal, vec3(0,1,0), emissiveValue);
+			}
+		}
+	};
+
+}
+

+ 71 - 0
hrt/prefab/terrain/Surface.hx

@@ -0,0 +1,71 @@
+package hrt.prefab.terrain;
+
+class Surface {
+	public var albedo : h3d.mat.Texture;
+	public var normal : h3d.mat.Texture;
+	public var pbr : h3d.mat.Texture;
+	public var tilling = 1.0;
+	public var offset : h3d.Vector;
+	public var angle = 0.0;
+	public var minHeight = 0.0;
+	public var maxHeight = 1.0;
+
+	public function new( ?albedo : h3d.mat.Texture, ?normal : h3d.mat.Texture, ?pbr : h3d.mat.Texture ) {
+		this.albedo = albedo;
+		this.normal = normal;
+		this.pbr = pbr;
+		this.offset = new h3d.Vector(0);
+	}
+
+	public function clone() : Surface {
+		var o = new Surface(albedo, normal, pbr);
+		o.tilling = tilling;
+		o.offset.load(offset);
+		o.angle = angle;
+		o.minHeight = minHeight;
+		o.maxHeight = maxHeight;
+		return o;
+	}
+
+	public function dispose() {
+		if( albedo != null ) albedo.dispose();
+		if( normal != null ) normal.dispose();
+		if( pbr != null ) pbr.dispose();
+	}
+}
+
+class SurfaceArray {
+	public var albedo : h3d.mat.TextureArray;
+	public var normal : h3d.mat.TextureArray;
+	public var pbr : h3d.mat.TextureArray;
+	public var surfaceCount : Int;
+	public var params : Array<h3d.Vector> = [];
+	public var secondParams : Array<h3d.Vector> = [];
+
+	public function new( count, res ) {
+		surfaceCount = count;
+		albedo = new h3d.mat.TextureArray(res, res, count, [Target], RGBA);
+		normal = new h3d.mat.TextureArray(res, res, count, [Target], RGBA);
+		pbr = new h3d.mat.TextureArray(res, res, count, [Target], RGBA);
+		albedo.wrap = Repeat;
+		albedo.realloc = null;
+		albedo.preventAutoDispose();
+		normal.wrap = Repeat;
+		normal.realloc = null;
+		normal.preventAutoDispose();
+		pbr.wrap = Repeat;
+		pbr.realloc = null;
+		pbr.preventAutoDispose();
+	}
+
+	public function clone() : SurfaceArray {
+		var o = new SurfaceArray(albedo.layerCount, albedo.width);
+		return o;
+	}
+
+	public function dispose() {
+		if( albedo != null ) albedo.dispose();
+		if( normal != null ) normal.dispose();
+		if( pbr != null ) pbr.dispose();
+	}
+}

+ 5 - 5
hrt/prefab/terrain/Terrain.hx

@@ -21,7 +21,7 @@ class Terrain extends Object3D {
 	public var weightMapResolution : Int = 20;
 	public var autoCreateTile = false;
 	var tmpSurfacesProps : Array<SurfaceProps> = [];
-	public var terrain : h3d.scene.pbr.terrain.Terrain;
+	public var terrain : TerrainMesh;
 	var parallaxAmount = 0.0;
 	var parallaxMinStep : Int = 1;
 	var parallaxMaxStep : Int = 16;
@@ -35,7 +35,7 @@ class Terrain extends Object3D {
 	#if editor
 	var packWeight = new h3d.pass.ScreenFx(new PackWeight());
 	var editor : hide.prefab.terrain.TerrainEditor;
-	var cachedInstance : h3d.scene.pbr.terrain.Terrain;
+	var cachedInstance : TerrainMesh;
 	public var showChecker = false;
 	#end
 
@@ -269,7 +269,7 @@ class Terrain extends Object3D {
 
 	public function loadBinary( ctx : Context ) {
 
-		terrain.surfaceArray = new h3d.scene.pbr.terrain.Surface.SurfaceArray(surfaceCount, surfaceSize);
+		terrain.surfaceArray = new Surface.SurfaceArray(surfaceCount, surfaceSize);
 
 		var resDir = ctx.shared.loadDir(name);
 		if( resDir == null ) return;
@@ -335,11 +335,11 @@ class Terrain extends Object3D {
 			return ctx;
 		}
 		else {
-			terrain = new h3d.scene.pbr.terrain.Terrain(ctx.local3d.getScene());
+			terrain = new TerrainMesh(ctx.local3d.getScene());
 			cachedInstance = terrain;
 		}
 		#else
-		terrain = new h3d.scene.pbr.terrain.Terrain(ctx.local3d);
+		terrain = new TerrainMesh(ctx.local3d);
 		#end
 		terrain.cellCount = getCellCount();
 		terrain.cellSize = getCellSize();

+ 278 - 0
hrt/prefab/terrain/TerrainMesh.hx

@@ -0,0 +1,278 @@
+package hrt.prefab.terrain;
+
+class TerrainMesh extends h3d.scene.Object {
+
+	public var tileSize : Float;
+	public var cellSize : Float;
+	public var cellCount : Int;
+	public var heightMapResolution : Int;
+	public var weightMapResolution : Int;
+	public var showGrid : Bool;
+	public var showChecker : Bool;
+	public var showComplexity : Bool;
+	public var parallaxAmount : Float;
+	public var parallaxMinStep : Int;
+	public var parallaxMaxStep : Int;
+	public var heightBlendStrength : Float;
+	public var blendSharpness : Float;
+	public var tiles : Array<Tile> = [];
+	public var surfaces : Array<Surface> = [];
+	public var surfaceArray : Surface.SurfaceArray;
+	public var copyPass : h3d.pass.Copy;
+
+	public function new(?parent){
+		super(parent);
+		copyPass = new h3d.pass.Copy();
+	}
+
+	override function onRemove() {
+		super.onRemove();
+		for( s in surfaces )
+			s.dispose();
+		if( surfaceArray != null )
+			surfaceArray.dispose();
+	}
+
+	public override function clone( ?o : h3d.scene.Object ) : h3d.scene.Object {
+		var o = new TerrainMesh();
+		o.tileSize = tileSize;
+		o.cellSize = cellSize;
+		o.cellCount = cellCount;
+		o.heightMapResolution = heightMapResolution;
+		o.weightMapResolution = weightMapResolution;
+		o.showGrid = showGrid;
+		o.showChecker = showChecker;
+		o.showComplexity = showComplexity;
+		o.parallaxAmount = parallaxAmount;
+		o.parallaxMinStep = parallaxMinStep;
+		o.parallaxMaxStep = parallaxMaxStep;
+		o.heightBlendStrength = heightBlendStrength;
+		o.blendSharpness = blendSharpness;
+
+		for( i in 0...tiles.length ) {
+			var t = Std.instance(tiles[i].clone(), Tile);
+			t.parent = o;
+			o.tiles.push(t);
+		}
+
+		for( i in 0...surfaces.length )
+			o.surfaces.push(surfaces[i].clone());
+
+		o.surfaceArray = surfaceArray.clone();
+
+		return o;
+	}
+
+	public function getHeight( x : Float, y : Float ) : Float {
+		var z = 0.0;
+		var t = getTileAtWorldPos(x, y);
+		if( t != null ) {
+			tmpVec.set(x, y);
+			var pos = t.globalToLocal(tmpVec);
+			z = t.getHeight(pos.x / tileSize, pos.y / tileSize);
+		}
+		return z;
+	}
+
+	public function getSurface( i : Int ) : Surface {
+		if(i < surfaces.length)
+				return surfaces[i];
+		return null;
+	}
+
+	public function getSurfaceFromTex( albedo, ?normal, ?pbr ) : Surface {
+		for( s in surfaces ) {
+			var valid = false;
+			valid = s.albedo.name == albedo;
+			valid = valid && !( normal != null && s.normal.name != normal );
+			valid = valid && !( pbr != null && s.pbr.name != pbr );
+			if( valid ) return s;
+		}
+		return null;
+	}
+
+	public function addSurface( albedo, normal, pbr ) : Surface {
+		surfaces.push(new Surface(albedo, normal, pbr));
+		return surfaces[surfaces.length - 1];
+	}
+
+	public function addEmptySurface() : Surface {
+		surfaces.push( new Surface() );
+		return surfaces[surfaces.length - 1];
+	}
+
+	public function generateSurfaceArray() {
+		if( surfaces.length == 0 ) return;
+		var surfaceSize = 1;
+		for( i in 0 ... surfaces.length )
+			if( surfaces[i].albedo != null ) surfaceSize = hxd.Math.ceil(hxd.Math.max(surfaces[i].albedo.width, surfaceSize));
+
+		if(surfaceArray != null) surfaceArray.dispose();
+		surfaceArray = new Surface.SurfaceArray(surfaces.length, surfaceSize);
+		for( i in 0 ... surfaces.length ) {
+			if( surfaces[i].albedo != null ) copyPass.apply(surfaces[i].albedo, surfaceArray.albedo, null, null, i);
+			if( surfaces[i].normal != null ) copyPass.apply(surfaces[i].normal, surfaceArray.normal, null, null, i);
+			if( surfaces[i].pbr != null ) copyPass.apply(surfaces[i].pbr, surfaceArray.pbr, null, null, i);
+		}
+		updateSurfaceParams();
+		refreshTex();
+	}
+
+	public function updateSurfaceParams() {
+		for( i in 0 ... surfaces.length ) {
+			surfaceArray.params[i] = new h3d.Vector(surfaces[i].tilling, surfaces[i].offset.x, surfaces[i].offset.y, hxd.Math.degToRad(surfaces[i].angle));
+			surfaceArray.secondParams[i] = new h3d.Vector(surfaces[i].minHeight, surfaces[i].maxHeight, 0, 0);
+		}
+	}
+
+	public function refreshTiles() {
+		for( tile in tiles )
+			if( tile.needAlloc ) {
+				tile.grid.alloc(h3d.Engine.getCurrent());
+				tile.needAlloc = false;
+			}
+	}
+
+	public function refreshMesh() {
+		for( tile in tiles ) {
+			tile.x = tile.tileX * tileSize;
+			tile.y = tile.tileY * tileSize;
+			tile.refreshMesh();
+		}
+		for( tile in tiles )
+			tile.blendEdges();
+	}
+
+	public function refreshTex() {
+		for( tile in tiles ) {
+			tile.refresh();
+		}
+	}
+
+	public function refresh() {
+		refreshMesh();
+		refreshTex();
+	}
+
+	public function createEmptyTile(x : Int, y : Int) : Tile {
+		var tile = getTile(x,y);
+		if(tile == null){
+			tile = new Tile(x, y, this);
+			tile.refreshMesh();
+			tiles.push(tile);
+		}
+		return tile;
+	}
+
+	public function createTile( x : Int, y : Int ) : Tile {
+		var tile = getTile(x,y);
+		if( tile == null ) {
+			tile = new Tile(x, y, this);
+			tile.refreshMesh();
+			tile.refresh();
+			tiles.push(tile);
+		}
+		return tile;
+	}
+
+	public function addTile( tile : Tile, ?replace = false ) {
+		for( t in tiles ) {
+			if( tile == t ) return;
+			if( tile.tileX == t.tileX && tile.tileY == t.tileY ) {
+				if( replace ) {
+					removeTile(t);
+					break;
+				}else
+					return;
+			}
+		}
+		tile.parent = this;
+		tiles.push(tile);
+		addChild(tile);
+	}
+
+	public function removeTileAt( x : Int, y : Int ) : Bool {
+		var t = getTile(x,y);
+		if( t == null ) {
+			removeTile(t);
+			return true;
+		}
+		return false;
+	}
+
+	public function removeTile( t : Tile ) : Bool {
+		if( t == null ) return false;
+		var r = tiles.remove(t);
+		if( r ) t.remove();
+		return r;
+	}
+
+	public function getTileIndex( t : Tile ) : Int {
+		for( i in 0 ... tiles.length )
+			if( t == tiles[i] ) return i;
+		return -1;
+	}
+
+	public function getTile( x : Int, y : Int ) : Tile {
+		var result : Tile = null;
+		for( tile in tiles )
+			if( tile.tileX == x && tile.tileY == y ) result = tile;
+		return result;
+	}
+
+	public function getTileAtWorldPos( x : Float, y : Float ) : Tile {
+		var pos = toLocalPos(x, y);
+		var result : Tile = null;
+		var tileX = Math.floor(pos.x / tileSize);
+		var tileY = Math.floor(pos.y / tileSize);
+		for( tile in tiles )
+			if( tile.tileX == tileX && tile.tileY == tileY ) result = tile;
+		return result;
+	}
+
+	public function createTileAtWorldPos( x : Float, y : Float ) : Tile {
+		var pos = toLocalPos(x, y);
+		var tileX = Math.floor(pos.x / tileSize);
+		var tileY = Math.floor(pos.y / tileSize);
+		var result = getTile(tileX, tileY);
+		return result == null ? createTile(tileX, tileY) : result;
+	}
+
+	public function getTiles( x : Float, y : Float, range : Float, ?create = false ) : Array<Tile> {
+		var pos = toLocalPos(x, y);
+		if( create != null && create ) {
+			var maxTileX = Math.floor((pos.x + range)/ tileSize);
+			var minTileX = Math.floor((pos.x - range)/ tileSize);
+			var maxTileY = Math.floor((pos.y + range)/ tileSize);
+			var minTileY = Math.floor((pos.y - range)/ tileSize);
+			for( x in minTileX ... maxTileX + 1)
+				for( y in minTileY...maxTileY + 1)
+					createTile(x, y);
+		}
+		var result : Array<Tile> = [];
+		for( tile in tiles)
+			if( Math.abs(pos.x - (tile.tileX * tileSize + tileSize * 0.5)) <= range + (tileSize * 0.5)
+			&& Math.abs(pos.y - (tile.tileY * tileSize + tileSize * 0.5)) <= range + (tileSize * 0.5) )
+				result.push(tile);
+		return result;
+	}
+
+	public function getVisibleTiles( c : h3d.Camera ) : Array<Tile> {
+		var res = [];
+		for( tile in tiles ) {
+			var bounds = tile.getBounds();
+			if( c.frustum.hasBounds(bounds) )
+				res.push(tile);
+		}
+		return res;
+	}
+
+	static var tmpVec = new h3d.Vector();
+	inline function toLocalPos( x : Float, y : Float ) {
+		tmpVec.set(x, y);
+		globalToLocal(tmpVec);
+		return tmpVec;
+	}
+}
+
+

+ 614 - 0
hrt/prefab/terrain/Tile.hx

@@ -0,0 +1,614 @@
+package hrt.prefab.terrain;
+
+enum Direction{
+	Up; Down; Left; Right; UpLeft; UpRight; DownLeft; DownRight;
+}
+
+class Tile extends h3d.scene.Mesh {
+
+	public var tileX (default, null) : Int;
+	public var tileY (default, null) : Int;
+	public var heightMap(default, set) : h3d.mat.Texture;
+	public var surfaceIndexMap : h3d.mat.Texture;
+	public var surfaceWeights : Array<h3d.mat.Texture> = [];
+	public var surfaceWeightArray (default, null) : h3d.mat.TextureArray;
+	public var grid (default, null) : h3d.prim.Grid;
+	public var needAlloc = false;
+	public var needNewPixelCapture = false;
+	public var insideFrustrum = false;
+	var heightmapPixels : hxd.Pixels.PixelsFloat;
+	var shader : Shader;
+
+	public function new( x : Int, y : Int , ?parent ) {
+		super(null, null, parent);
+		this.tileX = x;
+		this.tileY = y;
+		shader = new Shader();
+		material.mainPass.addShader(shader);
+		material.mainPass.culling = None;
+		material.shadows = false;
+		this.x = x * getTerrain().tileSize;
+		this.y = y * getTerrain().tileSize;
+		name = "tile_" + x + "_" + y;
+	}
+
+	override function onRemove() {
+		if( heightMap != null )
+			heightMap.dispose();
+		if( surfaceIndexMap != null )
+			surfaceIndexMap.dispose();
+		for( i in 0 ... surfaceWeights.length )
+			if( surfaceWeights[i] != null ) surfaceWeights[i].dispose();
+		if( surfaceWeightArray != null )
+			surfaceWeightArray.dispose();
+	}
+
+	public override function clone( ?o : h3d.scene.Object ) : h3d.scene.Object {
+		var o = new Tile(tileX, tileY, parent);
+		o.heightMap = heightMap.clone();
+		o.surfaceIndexMap = surfaceIndexMap.clone();
+
+		for( i in 0...surfaceWeights.length )
+			o.surfaceWeights.push(surfaceWeights[i].clone());
+
+		o.surfaceWeightArray = new h3d.mat.TextureArray(getTerrain().weightMapResolution, getTerrain().weightMapResolution, surfaceWeights.length, [Target], R8);
+		o.surfaceWeightArray.wrap = Clamp;
+		o.surfaceWeightArray.preventAutoDispose();
+		o.surfaceWeightArray.realloc = null;
+		for( i in 0 ... surfaceWeights.length )
+			if( surfaceWeights[i] != null ) getTerrain().copyPass.apply(surfaceWeights[i], o.surfaceWeightArray, None, null, i);
+
+		o.heightmapPixels = heightmapPixels.clone();
+		return o;
+	}
+
+	function set_heightMap( v ) {
+		shader.heightMap = v;
+		return heightMap = v;
+	}
+
+	inline function getTerrain() {
+		return Std.instance(parent, TerrainMesh);
+	}
+
+	public function getHeightPixels() {
+		if( needNewPixelCapture || heightmapPixels == null )
+			heightmapPixels = heightMap.capturePixels();
+		needNewPixelCapture = false;
+		return heightmapPixels;
+	}
+
+	public function refreshMesh() {
+		if( grid == null || grid.width != getTerrain().cellCount || grid.height != getTerrain().cellCount || grid.cellWidth != getTerrain().cellSize || grid.cellHeight != getTerrain().cellSize ) {
+			if(grid != null) grid.dispose();
+		 	grid = new h3d.prim.Grid( getTerrain().cellCount, getTerrain().cellCount, getTerrain().cellSize, getTerrain().cellSize);
+			primitive = grid;
+			//grid.addUVs(); // Not currently used
+		}
+		computeNormals();
+	}
+
+	public function blendEdges() {
+		var adjTileX = getTerrain().getTile(tileX - 1, tileY);
+		if( adjTileX != null ) {
+			var flags = new haxe.EnumFlags<Direction>();
+        	flags.set(Left);
+			adjTileX.computeEdgesHeight(flags);
+		}
+		var adjTileY = getTerrain().getTile(tileX, tileY - 1);
+		if( adjTileY != null ) {
+			var flags = new haxe.EnumFlags<Direction>();
+        	flags.set(Up);
+			adjTileY.computeEdgesHeight(flags);
+		}
+		var adjTileXY = getTerrain().getTile(tileX - 1, tileY - 1);
+		if( adjTileXY != null ) {
+			var flags = new haxe.EnumFlags<Direction>();
+        	flags.set(UpLeft);
+			adjTileXY.computeEdgesHeight(flags);
+		}
+		var flags = new haxe.EnumFlags<Direction>();
+        flags.set(Left);
+		flags.set(Up);
+		flags.set(UpLeft);
+		computeEdgesHeight(flags);
+		computeNormals();
+
+		computeEdgesNormals();
+	}
+
+	function refreshHeightMap() {
+		if( heightMap == null || heightMap.width != getTerrain().heightMapResolution + 1 ) {
+			var oldHeightMap = heightMap;
+			heightMap = new h3d.mat.Texture(getTerrain().heightMapResolution + 1, getTerrain().heightMapResolution + 1, [Target], RGBA32F );
+			heightMap.name = "terrainHeightMap";
+			heightMap.wrap = Clamp;
+			heightMap.filter = Linear;
+			heightMap.preventAutoDispose();
+			heightMap.realloc = null;
+			if( oldHeightMap != null ) {
+				getTerrain().copyPass.apply(oldHeightMap, heightMap);
+				oldHeightMap.dispose();
+			}
+			needNewPixelCapture = true;
+		}
+	}
+
+	function refreshIndexMap() {
+		if( surfaceIndexMap == null || surfaceIndexMap.width != getTerrain().weightMapResolution ) {
+			var oldSurfaceIndexMap = surfaceIndexMap;
+			surfaceIndexMap = new h3d.mat.Texture(getTerrain().weightMapResolution, getTerrain().weightMapResolution, [Target], RGBA);
+			surfaceIndexMap.name = "terrainSurfaceIndexMap";
+			surfaceIndexMap.filter = Nearest;
+			surfaceIndexMap.preventAutoDispose();
+			surfaceIndexMap.realloc = null;
+			if( oldSurfaceIndexMap != null ) {
+				getTerrain().copyPass.apply(oldSurfaceIndexMap, surfaceIndexMap);
+				oldSurfaceIndexMap.dispose();
+			}
+		}
+	}
+
+	function refreshSurfaceWeights() {
+		if( getTerrain().surfaceArray.surfaceCount > 0 && (surfaceWeights.length != getTerrain().surfaceArray.surfaceCount || surfaceWeights[0].width != getTerrain().weightMapResolution) ) {
+				var oldArray = surfaceWeights;
+				surfaceWeights = new Array<h3d.mat.Texture>();
+				surfaceWeights = [for( i in 0...getTerrain().surfaceArray.surfaceCount ) null];
+				for( i in 0 ... surfaceWeights.length ) {
+					surfaceWeights[i] = new h3d.mat.Texture(getTerrain().weightMapResolution, getTerrain().weightMapResolution, [Target], R8);
+					surfaceWeights[i].name = "terrainSurfaceWeight"+i;
+					surfaceWeights[i].wrap = Clamp;
+					surfaceWeights[i].preventAutoDispose();
+					surfaceWeights[i].realloc = null;
+					if( i < oldArray.length && oldArray[i] != null )
+						getTerrain().copyPass.apply(oldArray[i], surfaceWeights[i]);
+				}
+				for( t in oldArray )
+					if( t != null)
+						t.dispose();
+		}
+	}
+
+	public function refresh() {
+		refreshHeightMap();
+		refreshIndexMap();
+		refreshSurfaceWeights();
+		generateWeightArray();
+	}
+
+	public function generateWeightArray() {
+		if( surfaceWeightArray == null || surfaceWeightArray.width != getTerrain().weightMapResolution || surfaceWeightArray.get_layerCount() != surfaceWeights.length ) {
+			if( surfaceWeightArray != null ) surfaceWeightArray.dispose();
+			surfaceWeightArray = new h3d.mat.TextureArray(getTerrain().weightMapResolution, getTerrain().weightMapResolution, surfaceWeights.length, [Target], R8);
+			surfaceWeightArray.name = "terrainSurfaceWeightArray";
+			surfaceWeightArray.wrap = Clamp;
+			surfaceWeightArray.preventAutoDispose();
+			surfaceWeightArray.realloc = null;
+		}
+		for( i in 0 ... surfaceWeights.length )
+			if( surfaceWeights[i] != null ) getTerrain().copyPass.apply(surfaceWeights[i], surfaceWeightArray, None, null, i);
+	}
+
+	public function computeEdgesHeight( flag : haxe.EnumFlags<Direction> ) {
+
+		if( heightMap == null ) return;
+		var pixels : hxd.Pixels.PixelsFloat = getHeightPixels();
+
+		if( flag.has(Left) ) {
+			var adjTileX = getTerrain().getTile(tileX + 1, tileY);
+			var adjHeightMapX = adjTileX != null ? adjTileX.heightMap : null;
+			if( adjHeightMapX != null ) {
+				var adjpixels : hxd.Pixels.PixelsFloat = adjTileX.getHeightPixels();
+				for( i in 0 ... heightMap.height - 1 ) {
+					pixels.setPixelF(heightMap.width - 1, i, adjpixels.getPixelF(0,i) );
+				}
+			}
+		}
+		if( flag.has(Up) ) {
+			var adjTileY = getTerrain().getTile(tileX, tileY + 1);
+			var adjHeightMapY = adjTileY != null ? adjTileY.heightMap : null;
+			if( adjHeightMapY != null ) {
+				var adjpixels : hxd.Pixels.PixelsFloat = adjTileY.getHeightPixels();
+				for( i in 0 ... heightMap.width - 1) {
+					pixels.setPixelF(i, heightMap.height - 1, adjpixels.getPixelF(i,0) );
+				}
+			}
+		}
+		if( flag.has(UpLeft) ) {
+			var adjTileXY = getTerrain().getTile(tileX + 1, tileY + 1);
+			var adjHeightMapXY = adjTileXY != null ? adjTileXY.heightMap : null;
+			if( adjHeightMapXY != null ) {
+				var adjpixels : hxd.Pixels.PixelsFloat = adjTileXY.getHeightPixels();
+				pixels.setPixelF(heightMap.width - 1, heightMap.height - 1, adjpixels.getPixelF(0,0));
+			}
+		}
+		heightmapPixels = pixels;
+		heightMap.uploadPixels(pixels);
+		needNewPixelCapture = false;
+	}
+
+	public function computeEdgesNormals() {
+		if( grid.normals == null ) return;
+		var t0 = new h3d.col.Point(); var t1 = new h3d.col.Point(); var t2 = new h3d.col.Point();
+		var triCount = Std.int(grid.triCount() / grid.width);
+		var vertexCount = grid.width + 1;
+		var step = hxd.Math.floor(grid.normals.length / grid.width) - 1;
+		var s = hxd.Math.floor(grid.normals.length - grid.normals.length / grid.width + 2);
+		var istep = triCount * 3 - 6;
+		var i0, i1, i2 : Int = 0;
+
+		inline function computeVertexPos( tile : Tile ) {
+			t0.load(tile.grid.points[i0]); t1.load(tile.grid.points[i1]); t2.load(tile.grid.points[i2]);
+			t0.z += tile.getHeight(t0.x / getTerrain().tileSize, t0.y / getTerrain().tileSize);
+			t1.z += tile.getHeight(t1.x / getTerrain().tileSize, t1.y / getTerrain().tileSize);
+			t2.z += tile.getHeight(t2.x / getTerrain().tileSize, t2.y / getTerrain().tileSize);
+		}
+
+		inline function computeNormal() : h3d.col.Point {
+			var n1 = t1.sub(t0);
+			n1.normalize();
+			var n2 = t2.sub(t0);
+			n2.normalize();
+			return n1.cross(n2);
+		}
+
+		var adjUpTile = getTerrain().getTile(tileX, tileY + 1);
+		var adjUpGrid = adjUpTile != null ? adjUpTile.grid: null;
+		var adjDownTile = getTerrain().getTile(tileX, tileY - 1);
+		var adjDownGrid = adjDownTile != null ? adjDownTile.grid: null;
+		var adjLeftTile = getTerrain().getTile(tileX + 1, tileY);
+		var adjLeftGrid = adjLeftTile != null ? adjLeftTile.grid: null;
+		var adjRightTile = getTerrain().getTile(tileX - 1, tileY);
+		var adjRightGrid = adjRightTile != null ? adjRightTile.grid: null;
+		var adjUpRightTile = getTerrain().getTile(tileX - 1, tileY + 1);
+		var adjUpRightGrid = adjUpRightTile != null ? adjUpRightTile.grid: null;
+		var adjUpLeftTile = getTerrain().getTile(tileX + 1, tileY + 1);
+		var adjUpLeftGrid = adjUpLeftTile != null ? adjUpLeftTile.grid: null;
+		var adjDownLeftTile = getTerrain().getTile(tileX + 1, tileY - 1);
+		var adjDownLeftGrid = adjDownLeftTile != null ? adjDownLeftTile.grid: null;
+		var adjDownRightTile = getTerrain().getTile(tileX - 1, tileY - 1);
+		var adjDownRightGrid = adjDownRightTile != null ? adjDownRightTile.grid: null;
+
+		if( adjUpGrid != null && adjUpGrid.normals != null ) {
+			var pos = 0;
+			for( i in 0 ... vertexCount )
+				adjUpGrid.normals[i].set(0,0,0);
+			for( i in 0 ... triCount ) {
+				i0 = adjUpGrid.idx[pos++]; i1 = adjUpGrid.idx[pos++]; i2 = adjUpGrid.idx[pos++];
+				computeVertexPos(adjUpTile);
+				var n = computeNormal();
+				if( i0 <= adjUpGrid.width ) { adjUpGrid.normals[i0].x += n.x; adjUpGrid.normals[i0].y += n.y; adjUpGrid.normals[i0].z += n.z;}
+				if( i1 <= adjUpGrid.width ) { adjUpGrid.normals[i1].x += n.x; adjUpGrid.normals[i1].y += n.y; adjUpGrid.normals[i1].z += n.z;}
+				if( i2 <= adjUpGrid.width ) { adjUpGrid.normals[i2].x += n.x; adjUpGrid.normals[i2].y += n.y; adjUpGrid.normals[i2].z += n.z;}
+			}
+			for( i in 0 ... vertexCount )
+				adjUpGrid.normals[i].normalize();
+			for( i in 1 ... vertexCount - 1 ) {
+				var n = grid.normals[s + i].add(adjUpGrid.normals[i]);
+				n.normalize();
+				grid.normals[s + i].load(n);
+				adjUpGrid.normals[i].load(n);
+			}
+		}
+
+		if( adjDownGrid != null && adjDownGrid.normals != null ) {
+			var pos = triCount * (adjDownGrid.width - 1) * 3;
+			for( i in 0 ... vertexCount)
+				adjDownGrid.normals[s + i].set(0,0,0);
+			for( i in 0 ... triCount ) {
+				i0 = adjDownGrid.idx[pos++]; i1 = adjDownGrid.idx[pos++]; i2 = adjDownGrid.idx[pos++];
+				computeVertexPos(adjDownTile);
+				var n = computeNormal();
+				if( i0 >= (adjDownGrid.width * adjDownGrid.height + adjDownGrid.height) ) { adjDownGrid.normals[i0].x += n.x; adjDownGrid.normals[i0].y += n.y; adjDownGrid.normals[i0].z += n.z;}
+				if( i1 >= (adjDownGrid.width * adjDownGrid.height + adjDownGrid.height) ) { adjDownGrid.normals[i1].x += n.x; adjDownGrid.normals[i1].y += n.y; adjDownGrid.normals[i1].z += n.z;}
+				if( i2 >= (adjDownGrid.width * adjDownGrid.height + adjDownGrid.height) ) { adjDownGrid.normals[i2].x += n.x; adjDownGrid.normals[i2].y += n.y; adjDownGrid.normals[i2].z += n.z;}
+			}
+			for( i in 1 ... vertexCount - 1 )
+				adjDownGrid.normals[s + i].normalize();
+			for( i in 1 ... vertexCount - 1 ){
+				var n = grid.normals[i].add(adjDownGrid.normals[s + i]);
+				n.normalize();
+				grid.normals[i].load(n);
+				adjDownGrid.normals[s + i].load(n);
+			}
+		}
+
+		if( adjLeftGrid != null && adjLeftGrid.normals != null ) {
+			var pos = 0;
+			var istep = triCount * 3 - 6;
+			var needStep = false;
+			for( i in 0 ... vertexCount )
+				adjLeftGrid.normals[i * step].set(0,0,0);
+			for( i in 0 ... triCount ) {
+				i0 = adjLeftGrid.idx[pos++]; i1 = adjLeftGrid.idx[pos++]; i2 = adjLeftGrid.idx[pos++];
+				computeVertexPos(adjLeftTile);
+				var n = computeNormal();
+				if( i0 % (adjLeftGrid.width + 1) == 0 ) { adjLeftGrid.normals[i0].x += n.x; adjLeftGrid.normals[i0].y += n.y; adjLeftGrid.normals[i0].z += n.z;}
+				if( i1 % (adjLeftGrid.width + 1) == 0 ) { adjLeftGrid.normals[i1].x += n.x; adjLeftGrid.normals[i1].y += n.y; adjLeftGrid.normals[i1].z += n.z;}
+				if( i2 % (adjLeftGrid.width + 1) == 0 ) { adjLeftGrid.normals[i2].x += n.x; adjLeftGrid.normals[i2].y += n.y; adjLeftGrid.normals[i2].z += n.z;}
+				if( needStep) pos += istep;
+				needStep = !needStep;
+			}
+			for( i in 0 ... vertexCount )
+				adjLeftGrid.normals[i * step].normalize();
+			for( i in 1 ... vertexCount - 1 ){
+				var n = grid.normals[i * step + (step - 1)].add(adjLeftGrid.normals[i * step]);
+				n.normalize();
+				grid.normals[i * step + (step - 1)].load(n);
+				adjLeftGrid.normals[i * step].load(n);
+			}
+		}
+
+		if( adjRightGrid != null && adjRightGrid.normals != null ) {
+			var pos = (triCount - 2) * 3;
+			var istep = (triCount - 2) * 3;
+			var needStep = false;
+			for( i in 0 ... vertexCount )
+				adjRightGrid.normals[i * step + (step - 1)].set(0,0,0);
+			for( i in 0 ... triCount ) {
+				i0 = adjRightGrid.idx[pos++]; i1 = adjRightGrid.idx[pos++]; i2 = adjRightGrid.idx[pos++];
+				computeVertexPos(adjRightTile);
+				var n = computeNormal();
+				if( (i0 + 1) % (adjRightGrid.width + 1) == 0 ) { adjRightGrid.normals[i0].x += n.x; adjRightGrid.normals[i0].y += n.y; adjRightGrid.normals[i0].z += n.z; }
+				if( (i1 + 1) % (adjRightGrid.width + 1) == 0 ) { adjRightGrid.normals[i1].x += n.x; adjRightGrid.normals[i1].y += n.y; adjRightGrid.normals[i1].z += n.z; }
+				if( (i2 + 1) % (adjRightGrid.width + 1) == 0 ) { adjRightGrid.normals[i2].x += n.x; adjRightGrid.normals[i2].y += n.y; adjRightGrid.normals[i2].z += n.z; }
+				if( needStep) pos += istep;
+				needStep = !needStep;
+			}
+			for( i in 0 ... vertexCount )
+				adjRightGrid.normals[i * step + (step - 1)].normalize();
+			for( i in 1 ... vertexCount - 1 ) {
+				var n = grid.normals[i * step].add(adjRightGrid.normals[i * step + (step - 1)]);
+				n.normalize();
+				grid.normals[i * step].load(n);
+				adjRightGrid.normals[i * step + (step - 1)].load(n);
+			}
+		}
+
+		var topLeft = grid.points.length - 1;
+		var downLeft= step - 1;
+		var downRight = 0;
+		var upRight = step * grid.height;
+
+		var n = new h3d.col.Point();
+		if( adjUpRightGrid != null && adjUpRightGrid.normals != null ) {
+			var pos = (triCount) * 3 - 6;
+			i0 = adjUpRightGrid.idx[pos++]; i1 = adjUpRightGrid.idx[pos++]; i2 = adjUpRightGrid.idx[pos++];
+			computeVertexPos(adjUpRightTile);
+			n = computeNormal();
+			i0 = adjUpRightGrid.idx[pos++]; i1 = adjUpRightGrid.idx[pos++]; i2 = adjUpRightGrid.idx[pos++];
+			computeVertexPos(adjUpRightTile);
+			n = n.add(computeNormal());
+			n.normalize();
+		}
+		if( adjRightGrid != null && adjRightGrid.normals != null ) n = n.add(adjRightGrid.normals[topLeft]);
+		if( adjUpGrid != null && adjUpGrid.normals != null ) n = n.add(adjUpGrid.normals[downRight]);
+		n = n.add(grid.normals[upRight]);
+		n.normalize();
+		if( adjUpRightGrid != null && adjUpRightGrid.normals != null ) adjUpRightGrid.normals[downLeft].load(n);
+		if( adjRightGrid != null && adjRightGrid.normals != null ) adjRightGrid.normals[topLeft].load(n);
+		if( adjUpGrid != null && adjUpGrid.normals != null ) adjUpGrid.normals[downRight].load(n);
+		grid.normals[upRight].load(n);
+
+		n.set(0,0,0);
+		if( adjUpLeftGrid != null && adjUpLeftGrid.normals != null ) {
+			var pos = 0;
+			i0 = adjUpLeftGrid.idx[pos++]; i1 = adjUpLeftGrid.idx[pos++]; i2 = adjUpLeftGrid.idx[pos++];
+			computeVertexPos(adjUpLeftTile);
+			n = computeNormal();
+			n.normalize();
+		}
+		if( adjLeftGrid != null && adjLeftGrid.normals != null ) n = n.add(adjLeftGrid.normals[upRight]);
+		if( adjUpGrid != null && adjUpGrid.normals != null ) n = n.add(adjUpGrid.normals[downLeft]);
+		n = n.add(grid.normals[topLeft]);
+		n.normalize();
+		if( adjUpLeftGrid != null && adjUpLeftGrid.normals != null ) adjUpLeftGrid.normals[downRight].load(n);
+		if( adjLeftGrid != null && adjLeftGrid.normals != null ) adjLeftGrid.normals[upRight].load(n);
+		if( adjUpGrid != null && adjUpGrid.normals != null ) adjUpGrid.normals[downLeft].load(n);
+		grid.normals[topLeft].load(n);
+
+		n.set(0,0,0);
+		if( adjDownLeftGrid != null && adjDownLeftGrid.normals != null ) {
+			var pos = (triCount) * 3 * (adjDownLeftGrid.height - 1) ;
+			i0 = adjDownLeftGrid.idx[pos++]; i1 = adjDownLeftGrid.idx[pos++]; i2 = adjDownLeftGrid.idx[pos++];
+			computeVertexPos(adjDownLeftTile);
+			n = computeNormal();
+			i0 = adjDownLeftGrid.idx[pos++]; i1 = adjDownLeftGrid.idx[pos++]; i2 = adjDownLeftGrid.idx[pos++];
+			computeVertexPos(adjDownLeftTile);
+			n = n.add(computeNormal());
+			n.normalize();
+		}
+		if( adjLeftGrid != null && adjLeftGrid.normals != null ) n = n.add(adjLeftGrid.normals[downRight]);
+		if( adjDownGrid != null && adjDownGrid.normals != null ) n = n.add(adjDownGrid.normals[topLeft]);
+		n = n.add(grid.normals[downLeft]);
+		n.normalize();
+		if( adjDownLeftGrid != null && adjDownLeftGrid.normals != null ) adjDownLeftGrid.normals[upRight].load(n);
+		if( adjLeftGrid != null && adjLeftGrid.normals != null ) adjLeftGrid.normals[downRight].load(n);
+		if( adjDownGrid != null && adjDownGrid.normals != null ) adjDownGrid.normals[topLeft].load(n);
+		grid.normals[downLeft].load(n);
+
+		n.set(0,0,0);
+		if( adjDownRightGrid != null && adjDownRightGrid.normals != null ) {
+			var pos = triCount * 3 * adjDownRightGrid.width - 3;
+			i0 = adjDownRightGrid.idx[pos++]; i1 = adjDownRightGrid.idx[pos++]; i2 = adjDownRightGrid.idx[pos++];
+			computeVertexPos(adjDownRightTile);
+			n = computeNormal();
+			n.normalize();
+		}
+		if( adjRightGrid != null && adjRightGrid.normals != null ) n = n.add(adjRightGrid.normals[downLeft]);
+		if( adjDownGrid != null && adjDownGrid.normals != null ) n = n.add(adjDownGrid.normals[upRight]);
+		n = n.add(grid.normals[downRight]);
+		n.normalize();
+		if( adjDownRightGrid != null && adjDownRightGrid.normals != null ) adjDownRightGrid.normals[topLeft].load(n);
+		if( adjRightGrid != null && adjRightGrid.normals != null ) adjRightGrid.normals[downLeft].load(n);
+		if( adjDownGrid != null && adjDownGrid.normals != null ) adjDownGrid.normals[upRight].load(n);
+		grid.normals[downRight].load(n);
+
+		if( adjUpTile != null ) adjUpTile.needAlloc = true;
+		if( adjDownTile != null ) adjDownTile.needAlloc = true;
+		if( adjLeftTile != null ) adjLeftTile.needAlloc = true;
+		if( adjRightTile != null ) adjRightTile.needAlloc = true;
+		if( adjUpLeftTile != null ) adjUpLeftTile.needAlloc = true;
+		if( adjDownLeftTile != null ) adjDownLeftTile.needAlloc = true;
+		if( adjUpRightTile != null ) adjUpRightTile.needAlloc = true;
+		if( adjDownRightTile != null ) adjDownRightTile.needAlloc = true;
+		this.needAlloc = true;
+	}
+
+	public function computeNormals() {
+		if( grid.normals == null ) grid.normals = new Array<h3d.col.Point>();
+		grid.normals = [
+		for ( i in 0...grid.points.length ) {
+			if( i < grid.normals.length ) {
+				grid.normals[i].set(0,0,0);
+				grid.normals[i];
+			} else
+				new h3d.col.Point();
+		}];
+
+		var t0 = new h3d.col.Point(); var t1 = new h3d.col.Point(); var t2 = new h3d.col.Point();
+		var pos = 0;
+		for( i in 0...grid.triCount() ) {
+			var i0, i1, i2;
+			if( grid.idx == null ) {
+				i0 = pos++; i1 = pos++; i2 = pos++;
+			} else {
+				i0 = grid.idx[pos++]; i1 = grid.idx[pos++]; i2 = grid.idx[pos++];
+			}
+			t0.load(grid.points[i0]); t1.load(grid.points[i1]); t2.load(grid.points[i2]);
+			if(heightMap != null){
+				t0.z += getHeight(t0.x / getTerrain().tileSize, t0.y / getTerrain().tileSize);
+				t1.z += getHeight(t1.x / getTerrain().tileSize, t1.y / getTerrain().tileSize);
+				t2.z += getHeight(t2.x / getTerrain().tileSize, t2.y / getTerrain().tileSize);
+			}
+			var n1 = t1.sub(t0);
+			n1.normalizeFast();
+			var n2 = t2.sub(t0);
+			n2.normalizeFast();
+			var n = n1.cross(n2);
+			grid.normals[i0].x += n.x; grid.normals[i0].y += n.y; grid.normals[i0].z += n.z;
+			grid.normals[i1].x += n.x; grid.normals[i1].y += n.y; grid.normals[i1].z += n.z;
+			grid.normals[i2].x += n.x; grid.normals[i2].y += n.y; grid.normals[i2].z += n.z;
+		}
+		for( n in grid.normals )
+			n.normalize();
+
+		needAlloc = true;
+	}
+
+	public function getHeight( u : Float, v : Float, ?fast = false ) : Float {
+		var pixels = getHeightPixels();
+		if( pixels == null ) return 0.0;
+		if( heightMap.filter == Linear && !fast ) {
+			inline function getPix(u, v) {
+				return pixels.getPixelF(Std.int(hxd.Math.clamp(u, 0, pixels.width - 1)), Std.int(hxd.Math.clamp(v, 0, pixels.height - 1))).r;
+			}
+			var px = u * (heightMap.width - 1) + 0.5;
+            var py = v * (heightMap.width - 1) + 0.5;
+			var pxi = hxd.Math.floor(px);
+            var pyi = hxd.Math.floor(py);
+			var c00 = getPix(pxi, pyi);
+			var c10 = getPix(pxi + 1, pyi);
+			var c01 = getPix(pxi, pyi + 1);
+			var c11 = getPix(pxi + 1, pyi + 1);
+			var wx = px - pxi;
+			var wy = py - pyi;
+			var a = c00 * (1 - wx) + c10 * wx;
+			var b = c01 * (1 - wx) + c11 * wx;
+			return a * (1 - wy) + b * wy;
+
+		}
+		else{
+			var x = hxd.Math.floor(u * (heightMap.width - 1) + 0.5);
+			var y = hxd.Math.floor(v * (heightMap.height - 1) + 0.5);
+			return pixels.getPixelF(x, y).r;
+		}
+	}
+	var cachedBounds : h3d.col.Bounds;
+	var cachedHeightBound : Bool = false;
+	function computeBounds() {
+		if( cachedBounds == null ) {
+			cachedBounds = getBounds();
+			cachedBounds.zMax = 0;
+			cachedBounds.zMin = 0;
+		}
+		if( cachedBounds != null && cachedHeightBound == false && heightMap != null ) {
+			for( u in 0 ... heightMap.width ) {
+				for( v in 0 ... heightMap.height ) {
+					var h = getHeight(u / heightMap.width, v / heightMap.height, true);
+					cachedBounds.zMin = cachedBounds.zMin > h ? h : cachedBounds.zMin;
+					cachedBounds.zMax = cachedBounds.zMax < h ? h : cachedBounds.zMax;
+				}
+			}
+			var absPos = getAbsPos();
+			cachedBounds.zMax += absPos.tz;
+			cachedBounds.zMin += absPos.tz;
+			cachedHeightBound = true;
+		}
+	}
+
+	public dynamic function beforeEmit() : Bool { return true; };
+	override function emit( ctx:h3d.scene.RenderContext ) {
+		if( !isReady() ) return;
+		computeBounds();
+		insideFrustrum = ctx.camera.frustum.hasBounds(cachedBounds);
+		var b = beforeEmit();
+		if( b && insideFrustrum )
+			super.emit(ctx);
+	}
+
+	override function sync(ctx:h3d.scene.RenderContext) {
+		if( !isReady() ) return;
+
+		shader.SHOW_GRID = getTerrain().showGrid;
+		shader.SURFACE_COUNT = getTerrain().surfaceArray.surfaceCount;
+		shader.CHECKER = getTerrain().showChecker;
+		shader.COMPLEXITY = getTerrain().showComplexity;
+		shader.PARALLAX = getTerrain().parallaxAmount != 0;
+
+		shader.heightMapSize = heightMap.width;
+		shader.primSize = getTerrain().tileSize;
+		shader.cellSize = getTerrain().cellSize;
+
+		if( !shader.CHECKER && !shader.COMPLEXITY ) {
+			shader.albedoTextures = getTerrain().surfaceArray.albedo;
+			shader.normalTextures = getTerrain().surfaceArray.normal;
+			shader.pbrTextures = getTerrain().surfaceArray.pbr;
+			shader.weightTextures = surfaceWeightArray;
+			shader.heightMap = heightMap;
+			shader.surfaceIndexMap = surfaceIndexMap;
+
+			shader.surfaceParams = getTerrain().surfaceArray.params;
+			shader.secondSurfaceParams = getTerrain().surfaceArray.secondParams;
+			shader.tileIndex.set(tileX, tileY);
+			shader.parallaxAmount = getTerrain().parallaxAmount;
+			shader.minStep = getTerrain().parallaxMinStep;
+			shader.maxStep = getTerrain().parallaxMaxStep;
+			shader.heightBlendStrength = getTerrain().heightBlendStrength;
+			shader.blendSharpness = getTerrain().blendSharpness;
+		}
+	}
+
+	function isReady() {
+		if( primitive == null )
+			return false;
+
+		if( !getTerrain().showChecker || getTerrain().showComplexity ) {
+			if( getTerrain().surfaceArray == null || getTerrain().surfaceArray.surfaceCount == 0 || surfaceWeights.length != getTerrain().surfaceArray.surfaceCount )
+				return false;
+
+			for( i in 0 ... surfaceWeights.length )
+				if( surfaceWeights[i] == null || surfaceWeights[i].isDisposed() )
+					return false;
+		}
+
+		if( heightMap == null || heightMap.isDisposed() )
+			return false;
+
+		return true;
+	}
+
+	override function getLocalCollider() : h3d.col.Collider {
+		return null;
+	}
+}