Search Unity

Object-To-Clip for Canvas Image Shader

Discussion in 'Shaders' started by JohannesMP, Oct 7, 2018.

  1. JohannesMP

    JohannesMP

    Joined:
    Nov 4, 2016
    Posts:
    21
    TL;DR
    When applying a shader to a UI.Image object in a Canvas, UNITY_MATRIX_MVP appears to be relative to the Canvas, and not the Image object itself being drawn.

    Am I correct in this assumption? Is there a way to compute clip-space coordinates of a point in the Image object's local space, and not only relative to the Canvas root? (ideally completely from within the shader, not requiring additional behaviours, etc).


    Overview
    I'm looking to compute x_min, x_max, y_min and y_max of a UI.Image on screen in a shader as they appear in this mockup:

    I intend on using this in further post-processing effects, but that isn't directly relevant here.

    My basic approach is to manually compute the clip-space positions of the 4 corners using UnityObjectToClipPos (which just uses the built-in transformation matrix UNITY_MATRIX_MVP), and then get the min/max values from those.

    I have found this to work for SpriteRenderer or MeshRenderer components, but for UI.Image components in a canvas UNITY_MATRIX_MVP appears to only be relative to the canvas parent, and not the image object itself, meaning I cannot use it to calculate the positions relative to the image.

    Details
    You can find the full shader here: https://gist.github.com/JohannesMP/5a74fcc41d77f281a5900021c01f2356

    For a given object-space coordinate UnityObjectToClipPos is used to first get the homogeneous clip-space coordinate, which is then shifted to be in screen UV space (values ranging from 0 to 1 when visible on screen):

    Code (CSharp):
    1. inline float2 ObjToScreenUV(in float3 obj_pos)
    2. {
    3.     float4 clip_pos4d = UnityObjectToClipPos(obj_pos);
    4.     float2 clip_pos2d = clip_pos4d.xy / clip_pos4d.w;
    5. #if UNITY_UV_STARTS_AT_TOP
    6.     return float2(1 + clip_pos2d.x, 1 - clip_pos2d.y) / 2;
    7. #else
    8.     return (1 + clip_pos2d) / 2;
    9. #endif
    10. }
    Since the 4 vertices of a unit quad are trivial to define in object space (just offset by 0.5 on the object-space X/Y axes), the on-screen min/max values can be computed as follows:


    Code (CSharp):
    1. inline float4 GetQuadUVBounds()
    2. {
    3.     // 1. Get relative screen position of 4 corners quad
    4.     float2 bl = ObjToScreenUV(float3(-0.5, -0.5, 0));
    5.     float2 tl = ObjToScreenUV(float3(-0.5, 0.5, 0));
    6.     float2 br = ObjToScreenUV(float3(0.5, -0.5, 0));
    7.     float2 tr = ObjToScreenUV(float3(0.5, 0.5, 0));
    8.  
    9.     // 2. Get min/max of x and y
    10.     float min_x = min(min(bl.x, tl.x), min(br.x, tr.x));
    11.     float max_x = max(max(bl.x, tl.x), max(br.x, tr.x));
    12.     float min_y = min(min(bl.y, tl.y), min(br.y, tr.y));
    13.     float max_y = max(max(bl.y, tl.y), max(br.y, tr.y));
    14.  
    15.     return float4(min_x, min_y, max_x, max_y);
    16. }
    Please note that I am not looking to optimize the min/max logic at this time.

    To verify that the min/max values were correctly calculated they can be used for the red and green channel as the object is drawn.

    For example, to verify that x_min and y_min works, the following fragment shader can be used:


    Code (CSharp):
    1. half4 frag () : COLOR
    2. {
    3.     // Min is bounds.xy, max is bounds.zw
    4.     float4 bounds = GetQuadUVBounds();
    5.     return float4(bounds.xy, 0, 1);
    6. }
    Which for MeshRenderer quads and SpriteRenderer objects results in this:

    • As x_min increases, so does the red channel.
    • As y_min increases, so does the green channel.
    Testing the max values also yields the expected results.

    However when testing the same shader on a UI.Image component in a canvas, it does not work as expected:

    Despite the UI.Image object (which has the shader set as its material) being moved on the screen, its color does not change. However when in the editor scene view the editor camera is moved, then the color does change.

    This seems to imply that UnityObjectToClipPos, and by extension the UNITY_MATRIX_MVP matrix it depends on, are in relation to the Canvas Parent object, and not the Image object inside the parent.


    Question
    Am I correct in my assumption that UNITY_MATRIX_MVP is relative to the canvas renderer, and not the Image object being drawn? If so, is there another way to get an MVP matrix specifically for the Image object? Alternatively, are there any other means by which the Clip-space position of a point in the Image's local space could be computed within a shader?
     
    Last edited: Oct 7, 2018