Search Unity

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:
    249
    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:
    249
    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:
    249
    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:
    95
    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:
    249
    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:
    424
    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)
     
    Nyapsora, Rowlan and andrew-lukasik like this.
  7. wyattt_

    wyattt_

    Unity Technologies

    Joined:
    May 9, 2018
    Posts:
    424
    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.  
     
    tatsuuuuuuu likes this.
  8. dan_wipf

    dan_wipf

    Joined:
    Jan 30, 2017
    Posts:
    314
    @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:
    13,363
    is writing to rendertexture of terrain supported in unity 2018 or is only 2019 ? Because i cant find that option
     
  10. jbooth

    jbooth

    Joined:
    Jan 6, 2014
    Posts:
    5,461
    So, if a user paints a value in instanced mode, it will clamp at 0.5 and render correctly, but when you turn off instancing, the parts of the terrain that are at the ceiling will suddenly wrap to the floor. And only halving half the precision is, well, not great.

    With Draw Instanced off:



    With Draw Instanced on:


    Unfortunately the current state of Draw Instancing is not great. I'd love to switch to requiring it, but I doubt I'll be able to do that any time soon. First, as long as tessellation doesn't work with Draw Instanced in Surface Shaders, I have to support not having Draw Instanced on as well. Second, when draw instanced is disabled, TerrainData.normalmapTexture will return null. This means that to access the normal map in a shader, I have to generate my own - which is not hard, but it's extra data per terrain, and I have to have the user manually update it any time the terrain is changed. When some of my users are using 50 terrains, it can add up to a lot of memory quick.

    Either Draw Instance should always be able to be used, or I should always be able to get the Height map and normal map textures should I need them (they could be generate on request if the terrain isn't using them?). Right now, I end up having to duplicate a ton of data when I need height/normal data access via shaders, such as for my terrain blending or dynamic streams features, because I don't know if that data will be available or not, and the user could toggle something at any time.
     
    Rowlan and Bordeaux_Fox like this.