Search Unity

  1. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Terrain terrainData.heightmapTexture float value range

Discussion in 'World Building' started by andrew-lukasik, May 4, 2019.

  1. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    167
    Hi,
    While rendering to terrainData.heightmapTexture I discovered that writing 1.0f to pixels doesn't result in terrain of maximum height (as specified in "Terrain Height" inspector field) but 0.5 does (1.0 is twice that and not available for manual brush edits).
    Seems odd/surprising but I expect there is sensible reason behind it. Can sb explain this behavior?


    (image shows terrain after rendering a sine wave (0.0-1.0 range) to it. I was using a compute shader > Graphics.CopyTexture > terrainData.DirtyHeightmapRegion path)

    Code (CSharp):
    1. #pragma kernel CSMain
    2.  
    3. RWTexture2D<float> Result;
    4. float time = 0.0f;
    5. float scale = 1.0f;
    6.  
    7. [numthreads(8,8,1)]
    8. void CSMain ( uint3 id : SV_DispatchThreadID )
    9. {
    10.     Result[id.xy] = (sin( (id.x + time)*scale )+1.0)*0.5;
    11. }
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class DispatchTerrainComputeShader : MonoBehaviour
    4. {
    5.  
    6.     [SerializeField] Terrain _terrain = null;
    7.     [SerializeField] TerrainHeightmapSyncControl terrainHeightmapSyncControl = TerrainHeightmapSyncControl.None;
    8.     [SerializeField] RectInt region = new RectInt{ width = 100 , height = 100 };
    9.     [SerializeField] ComputeShader _computeShader = null;
    10.     [SerializeField] float scale = 0.25f;
    11.     [SerializeField] bool syncHeightmap = false;
    12.  
    13.     RenderTexture RESULT;
    14.     bool dispatched;
    15.  
    16.  
    17.     void Update ()
    18.     {
    19.         if( dispatched )
    20.         {
    21.             var terrainData = _terrain.terrainData;
    22.             var heightmapTexture = terrainData.heightmapTexture;
    23.             Graphics.CopyTexture(
    24.                 RESULT , 0 , 0 , 0 , 0 , region.width , region.height ,
    25.                 heightmapTexture , 0 , 0 , region.x , region.y
    26.             );
    27.             terrainData.DirtyHeightmapRegion( region , terrainHeightmapSyncControl );
    28.             if( syncHeightmap ) terrainData.SyncHeightmap();
    29.             RenderTexture.ReleaseTemporary( RESULT );
    30.      
    31.             dispatched = false;
    32.         }
    33.  
    34.         //if( Input.GetKeyDown( KeyCode.Space ) )
    35.         {
    36.             var heightmapTexture = _terrain.terrainData.heightmapTexture;
    37.             var descriptor = heightmapTexture.descriptor;
    38.             descriptor.width = region.width;
    39.             descriptor.height = region.height;
    40.             descriptor.enableRandomWrite = true;
    41.             RESULT = RenderTexture.GetTemporary( descriptor );
    42.             Graphics.CopyTexture(
    43.                 heightmapTexture , 0 , 0 , region.x , region.y ,region.width , region.height ,
    44.                 RESULT , 0 , 0 , 0 , 0
    45.             );
    46.      
    47.             int kernelHandle = _computeShader.FindKernel( "CSMain" );
    48.             _computeShader.SetTexture( kernelHandle , "Result" , RESULT );
    49.             _computeShader.SetFloat( "time" , Time.time );
    50.             _computeShader.SetFloat( "scale" , scale );
    51.             _computeShader.Dispatch( kernelHandle , Mathf.Max(RESULT.width/8,1) , Mathf.Max(RESULT.height/8,1) , 1 );
    52.      
    53.             dispatched = true;
    54.         }
    55.     }
    56.  
    57.     void OnDestroy ()
    58.     {
    59.         if( RESULT!=null || RESULT.IsCreated() )
    60.         {
    61.             RenderTexture.ReleaseTemporary( RESULT );
    62.             RESULT = null;
    63.         }
    64.     }
    65.  
    66. }
     
    Last edited: May 8, 2019
    EirikWahl likes this.
  2. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    167
    My guess so far is that it has something to do with R16_UNORM format, an integer type ranging from 0 to uint16.maxvalue, and the way how it's being converted to/from float32 on GPU results in this halved range (??)
     
    Last edited: May 4, 2019
  3. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    167
    For CPU this data is formatted as unsigned 16bit integer (range 0 to 65535). But for GPU it's R16_UNORM where UNORM describes hardware conversion: unsigned & normalized (ie: 0.0-1.0 on read/write as a float).

    From what I can deduct - in order for this conversion to go astray like this it must be (wrongly?) cast to signed int16 just before GPU conversion does the rest - treating 32767 as 1.0 and not 0.5 (?). But ... that would mean there is bug somewhere in the pipeline and I don't wan't to jump to this conclusion too hastily.

    (copied from my other thread)
     
  4. crysicle

    crysicle

    Joined:
    Oct 24, 2018
    Posts:
    91
    I've reported this as a bug about a month ago. There is more to this than just a false limit, the heightmap's position and collider itself starts behaving wrongly if you check out the video.

    For now i would suggest to divide the heightmap data by 2 if you are looking to import heightmaps adequately.

    https://fogbugz.unity3d.com/default.asp?1144260_ve1ono6e1iupb7o1
     
    EirikWahl and andrew-lukasik like this.
  5. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    167
    I was loosing my mind over this - no useful documentation, not even one code sample anywhere on the internet (I don't mean brushes but workflow for rendering to heightmap directly outside editor), any proofs this api even works.
    Well, at least we know it is a dead end now for now.

    This is a relief, thank you for letting me know!
     
  6. wyattt_

    wyattt_

    Unity Technologies

    Joined:
    May 9, 2018
    Posts:
    333
    This is correct. The heightmap implementation itself is signed but is treated as unsigned when rendering so we only have half the precision available to use for height values. That's why all of our Terrain painting shaders clamp the returned value between 0f and .5f so that we don't end up writing signed values into the heightmap. If you were to put in values greater than .5, you'll see the Terrain surface "wrap" to negative height values. I can't say why this was done but it probably has stayed this way because it would take a lot of code changes to make either of them signed or unsigned to match.

    The values are normalized so that we can get the most precision we can out of the .5f for a given Terrain's max height. 0 being a world height offset of 0 and .5f being terrain.terrainData.size.y (the max height)
     
    andrew-lukasik likes this.
  7. wyattt_

    wyattt_

    Unity Technologies

    Joined:
    May 9, 2018
    Posts:
    333
    If you take a peek into UnityCG.cginc, you can see what we do in our functions for packing and unpacking Heightmap data


    Code (CSharp):
    1.  
    2. #define API_HAS_GUARANTEED_R16_SUPPORT !(SHADER_API_VULKAN || SHADER_API_GLES || SHADER_API_GLES3)
    3.  
    4. float4 PackHeightmap(float height)
    5. {
    6.     #if (API_HAS_GUARANTEED_R16_SUPPORT)
    7.         return height;
    8.     #else
    9.         uint a = (uint)(65535.0f * height);
    10.         return float4((a >> 0) & 0xFF, (a >> 8) & 0xFF, 0, 0) / 255.0f;
    11.     #endif
    12. }
    13.  
    14. float UnpackHeightmap(float4 height)
    15. {
    16.     #if (API_HAS_GUARANTEED_R16_SUPPORT)
    17.         return height.r;
    18.     #else
    19.         return (height.r + height.g * 256.0f) / 257.0f; // (255.0f * height.r + 255.0f * 256.0f * height.g) / 65535.0f
    20.     #endif
    21. }
    22.  
     
  8. dan_wipf

    dan_wipf

    Joined:
    Jan 30, 2017
    Posts:
    283
    @wyatttt so could you tell me how’d go for more precision if i use GetHeigts(worldpos.z,worldpos.x,terrain width, terrain heights)? because if i use sampleheight, it’s completly a diffrent result..
     
  9. nasos_333

    nasos_333

    Joined:
    Feb 13, 2013
    Posts:
    8,631
    is writing to rendertexture of terrain supported in unity 2018 or is only 2019 ? Because i cant find that option
     
unityunity