hey, Trying my hand at runtime terrain deformation. my setup so far (thx to @palten08 and @crysicle ) relevant terrain cs code: Code (CSharp): void GetAreaToModify() { RaycastHit hit; var ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Terrain"))) { if (!terrain) terrain = hit.transform.GetComponent<Terrain>(); terrainPoint = new Vector2((int)((hit.point.x - (rectSize.x * .5f)) - terrain.transform.position.x), (int)((hit.point.z - (rectSize.y * .5f)) - terrain.transform.position.z)); if (terrainPoint != previousTerrainPoint && Vector2.Distance(terrainPoint, previousTerrainPoint) > 1f) { Rect prevRect = new Rect(previousTerrainPoint, rectSize); if (prevRect.height != 0 && prevRect.width != 0 && prevRenderTexture) RestoreTerrain(prevRect); Rect rect = new Rect(terrainPoint, rectSize); ModifyTerrain(rect); previousTerrainPoint = terrainPoint; brush.position = new Vector3((int)hit.point.x, hit.point.y + 0.1f, (int)hit.point.z); } } } void ModifyTerrain(Rect selection) { //terrain = Terrain.activeTerrain; if (trees.Count > 0) { trees.AddRange(terrain.terrainData.treeInstances); terrain.terrainData.treeInstances = trees.ToArray(); trees.Clear(); } //Trees inside circle foreach (var tree in terrain.terrainData.treeInstances) { Vector2 p = new Vector2((tree.position.x * terrain.terrainData.size.x) + terrain.transform.position.x, (tree.position.z * terrain.terrainData.size.z) + terrain.transform.position.z); float d = Mathf.Sqrt(Mathf.Pow(p.x - transform.position.x, 2) + Mathf.Pow(p.y - transform.position.z, 2)); if (d < rectSize.x * .5f) trees.Add(tree); } if (trees.Count > 0) terrain.terrainData.treeInstances = terrain.terrainData.treeInstances.Except(trees).ToArray(); PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selection); Debugger.Log("Rect: " + selection + " contextRect: " + paintContext.pixelRect + "/" + paintContext.pixelSize); RenderTexture terrainRenderTexture = new RenderTexture(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, RenderTextureFormat.R16); terrainRenderTexture.enableRandomWrite = true; Graphics.CopyTexture(paintContext.sourceRenderTexture, terrainRenderTexture); prevRenderTexture = new RenderTexture(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, RenderTextureFormat.R16); Graphics.CopyTexture(paintContext.sourceRenderTexture, prevRenderTexture); float h0 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x, 0, selection.position.y + terrain.transform.position.z)); float h1 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x, 0, selection.position.y + terrain.transform.position.z + rectSize.y)); float h2 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x + rectSize.x, 0, selection.position.y + terrain.transform.position.z + rectSize.y)); float h3 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x + rectSize.x, 0, selection.position.y + terrain.transform.position.z)); height = (((h0 + h1 + h2 + h3) / 4f)/* + terrain.transform.position.y*/) / terrain.terrainData.size.y; terrainPaintShader.SetFloat("height", height*smoothOffset); terrainPaintShader.SetTexture(terrainPaintShader.FindKernel("CSMain"), "heightmap", terrainRenderTexture); terrainPaintShader.SetTexture(terrainPaintShader.FindKernel("CSMain"), "brush", brushTexture); terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), (int)Mathf.Ceil(selection.width / 32), (int)Mathf.Ceil(selection.height / 32), 1); Graphics.CopyTexture(terrainRenderTexture, paintContext.destinationRenderTexture); TerrainPaintUtility.EndPaintHeightmap(paintContext, "Terrain"); terrain.terrainData.SyncHeightmap(); } void RestoreTerrain(Rect selection) { if (baked) { baked = false; return; } PaintContext prevPaintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selection); Graphics.CopyTexture(prevRenderTexture, prevPaintContext.destinationRenderTexture); TerrainPaintUtility.EndPaintHeightmap(prevPaintContext, "Terrain"); terrain.terrainData.SyncHeightmap(); } Compute shader code: Code (CSharp): #pragma kernel CSMain half height; //half2 offset; RWTexture2D<half4> heightmap; Texture2D<half4> brush; [numthreads(32, 32, 1)] void CSMain(uint2 id : SV_DispatchThreadID) { for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { half4 new_heightmap = lerp(heightmap[int2(id.x + (32 * i), id.y + (32 * j))], height, brush[int2(id.x + (32 * i), id.y + (32 * j))]); heightmap[int2(id.x + (32 * i), id.y + (32 * j))] = clamp(new_heightmap, 0, .5); // 0.5, because that's the range devs decided to treat the texture internally. Bugs occur beyond 0.5. } } } my brush 64x64: my result: as you can see i get distortions... things i tried and found out: the brush size, rectSize, computeshader numthreads and terrain height map resolution are all connected. as i understand atm: - I pass a rect to the function TerrainPaintUtility.BeginPaintHeightmap with takes a "snapshot" from the heightmap with the position and size of the passed rect. Heightmap resolution comes into play here. Debugging the PaintContext rect and pixelSize gives sometimes strange values depending on the HeightMap resolution. (ex. i pass a 64x64 rect, but the paintcontext rect prints 18x19 for height/width and 4/4 pixelsize? guessing the pixelsize means resolution of the heightmap somehow) - numthreads 32x32 can handle my 64x64 rect that gets passed as a texture, by offsetting the thread ids in the for loops, (please correct me if I'm wrong). So my assumption was that it does the same for the brush texture. Confusion came when it got unexpected results and started to iterate changing Heightmap resolution, NumThreads, RectSize, ... And found that for example it only works when my Heightmap resolution is set to 2049, my rectsize 64x64, brush texture size 64 and numthreads 32x32 with a double for loop of 2 (as posted in the above). changing Heightmap resolution too 1025 results in it only applying 1/4 of the brush texture in the lerp. as you can see by the sharp corner. (it should lerp from sampled height to calculated height based of the brush texture "alpha") to conclude: I'm confused by the relation between Heightmap resolution, rect / brush size and numthreads. If someone could shed some light on how they affect each other, would be great
I haven't worked with the PaintContext for quite a while, but about a year ago I remember the way it calculated its size by the passed parameters to be shoddy. In certain cases the resolution of the selected area was constantly fluctuating by 1 width/height and really misbehaving when manipulating areas greater than a 2x2 terrains. I moved away from the class and decided to handle the heightmap gathering/scattering by myself so I can't really say much there. As for the manipulation of the heightmap: If your terrainRenderTexture is 64x64, brush is 64x64, selection width and height are 64x64, and numthreads are [32, 32, 1], then all that should be neccesary is passing the dispatch as: Code (CSharp): terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), Mathf.CeilToInt(selection.width / 32f), Mathf.CeilToInt(selection.height / 32f), 1); and the shader code as: Code (CSharp): #pragma kernel CSMain half height; RWTexture2D<half> heightmap; Texture2D<half4> brush; [numthreads(32, 32, 1)] void CSMain(uint2 id : SV_DispatchThreadID) { half new_heightmap = lerp(heightmap[id.xy], height, brush[id.xy].w); heightmap[id.xy] = clamp(new_heightmap, 0, .5); } currently your 2 for loops are doing what the dispatch should do for you by turning the thread parameters into: Code (CSharp): [code=CSharp]terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), 2, 2, 1); [/code] pretty much asking the shader to process a block of [32,32,1] numthreads as 2x2x1 block which will make the shader run a 64x64x1 block with each thread in parallel. For later updates when you decide to manipulate areas which do not match the brush resolution, you'll need to use bilinear or nearest neighbour filtering to sample the data from the brush. This can be done either by making the algorithms yourself inside the shader or using Texture Samplers which you can read up on below. https://docs.unity3d.com/Manual/SL-SamplerStates.html
Just made the changes and it works perfect now, the distortions are gone. So only the relation Heightmap resolution and Rect/Brush size left. For it to work correct I have to follow the rule: Heightmap Resolution-1 == RectSizexRectSize so the bigger the brush the higher i have to set the Heightmap res, which isn't very optimal. (Since SyncHeightmap is the most expensive part of the system, keeping the resolution down really helps performance) I need different sizes of brushes, so i need to break this rule somehow and still keep it working. Strange think about it is i don't get how the "SnapShot" (PaintContext) of the HieghtMap with my 64x64 brush has the same amount of pixels as the whole HeightMap?
I also want a sample package. No matter how much I struggled with PaintContext, I did not understand how to make it work correctly. I would be very grateful.
sorry guys, we ended up scratching that feature from our game. so i abandoned it quite messy. these are the related scripts i found that made it work at some point. I guess u'll have to trail and error with these... If i ever have the time for it, I'll make a nice example package. Anyway glad to help u get this to work, so ask away here
While we're tearing up some terrain, here's my grenade / bomb terrain blaster: Links to full source code and project in the video comments. MakeGeo is presently hosted at these locations: https://bitbucket.org/kurtdekker/makegeo https://github.com/kurtdekker/makegeo https://gitlab.com/kurtdekker/makegeo https://sourceforge.net/p/makegeo