Search Unity

Question Mesh: Efficient change position/rotation/scale in vertex shader (working example)

Discussion in 'Shaders' started by vdweller, Jun 19, 2020.

  1. vdweller

    vdweller

    Joined:
    May 24, 2020
    Posts:
    7
    Hi all!

    I have written a method which draws a "sprite" (a textured quad mesh) with an arbitrary position, rotation around a pivot point (0-1f of texture width/height in world units) and scale. My (working) code in CPU is:

    Code (CSharp):
    1.     public static Matrix4x4 MatrixFullTransform(Vector3 offset, Ray axis, float rotation, Vector3 scale) {
    2.         return Matrix4x4.Translate(offset - axis.origin) * Matrix4x4.TRS(axis.origin, Quaternion.AngleAxis(rotation, axis.direction), scale) * Matrix4x4.TRS(-axis.origin, Quaternion.identity, Vector3.one);
    3.     }
    Code (CSharp):
    1.     public static void DrawSprite(string name, int subImage, float x, float y, float xoffset, float yoffset, float depth, float angle, float xscale, float yscale) {
    2.         CSprite spr = GraphicsLibrary.dict_CSprite[name];
    3.         CSpriteSubImg sprSub = spr.subImg[subImage];
    4.         float offx = xoffset * sprSub.widthUnits;
    5.         float offy = yoffset * sprSub.heightUnits;
    6.         Graphics.DrawMesh(sprSub.mesh, MatrixFullTransform(new Vector3(x, y, depth), new Ray(new Vector3(offx, offy, 0f), new Vector3(0f, 0f, 1f)), angle, new Vector3(xscale, yscale, 1f)), sprSub.mat, 0);
    7.     }
    This works as expected and it simulates a draw_sprite function that can be found in other engines, without the need to add any gameobjects.

    My attempt to move some calculations in the GPU is:
    (in the DrawSprite method):
    Code (CSharp):
    1.         Ray axis = new Ray(new Vector3(offx, offy, 0f), new Vector3(0, 0, 1));
    2.         Vector3 scale = new Vector3(xscale, yscale, 1f);
    3.         Vector3 offset = new Vector3(x, y, depth);
    4.         Material m = sprSub.mat;
    5.  
    6.         m.SetMatrix("_m1", Matrix4x4.Translate(offset - axis.origin));
    7.         m.SetMatrix("_m2", Matrix4x4.TRS(axis.origin, Quaternion.AngleAxis(angle, axis.direction), scale));
    8.         m.SetMatrix("_m3", Matrix4x4.TRS(-axis.origin, Quaternion.identity, Vector3.one));
    9.        
    10.        
    11.         Graphics.DrawMesh(sprSub.mesh, Vector3.zero, Quaternion.identity, m, 0);
    And the shader is:
    Code (CSharp):
    1. Shader "shader_sprite"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Pass
    10.         {
    11.             CGPROGRAM
    12.             #pragma vertex vert
    13.             #pragma fragment frag
    14.  
    15.             // vertex shader inputs
    16.             struct appdata
    17.             {
    18.                 float4 vertex : POSITION; // vertex position
    19.                 float2 uv : TEXCOORD0; // texture coordinate
    20.  
    21.             };
    22.             struct v2f
    23.             {
    24.                 float2 uv : TEXCOORD0;
    25.                 float4 pos : SV_POSITION;
    26.             };
    27.  
    28.             float4x4 _m1,_m2,_m3;
    29.  
    30.             v2f vert (appdata v)
    31.             {
    32.                 v2f o;
    33.                 o.uv = v.uv;
    34.                 float4x4 p = mul(_m1, mul(_m2,_m3));
    35.                 o.pos = UnityObjectToClipPos(mul(p, v.vertex));
    36.                 return o;
    37.             }
    38.  
    39.             sampler2D _MainTex;
    40.             fixed4 frag (v2f i) : SV_Target
    41.             {
    42.                 return tex2D(_MainTex, i.uv);
    43.             }
    44.             ENDCG
    45.         }
    46.     }
    47. }
    My questions are:
    1) Is there a more efficient way to do this? As it is now, the GPU way is ~4x faster than the CPU way, but I feel it could be quite faster. Maybe if i changed some transformations?
    2) In the GPU way, setting a negative scale doesn't draw the mesh at all, however in the CPU way it works fine. Any way around this?

    Thanks in advance.
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    There's kind of no need to pass in three Matrix4x4 / float4x4 values to the shader, or do the 3 extra matrix multiplies there. Just pass in the offset, angle * axis, and the scale and construct the matrix in the shader. It'd also be much faster to apply the matrices to the vertex position rather mul them together before applying it.

    I'm also not entirely sure what the m3 matrix is for. What's the end result you're looking to achieve? There's probably a much cheaper way of doing all of this.
     
  3. vdweller

    vdweller

    Joined:
    May 24, 2020
    Posts:
    7
    Well I decided to just pass an array of floats to the shader instead of using all these convoluted matrices haha

    My code now is:
    Code (CSharp):
    1.     public static void DrawSprite(string name, int subImage, float x, float y, float xoffset, float yoffset, float depth, float angle, float xscale, float yscale) {
    2.         CSprite spr = GraphicsLibrary.dict_CSprite[name];
    3.         CSpriteSubImg sprSub = spr.subImg[subImage];
    4.         float offx = xoffset * sprSub.widthUnits;
    5.         float offy = yoffset * sprSub.heightUnits;
    6.  
    7.         Material m = sprSub.mat;
    8.         m.SetFloatArray("data", new[] { angle, x, y, offx, offy, xscale, yscale});
    9.         Graphics.DrawMesh(sprSub.mesh, new Vector3(x, y, depth), Quaternion.identity, m, 0);
    10.     }
    And the shader:
    Code (CSharp):
    1. Shader "shader_sprite"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Pass
    10.         {
    11.             CGPROGRAM
    12.  
    13.             #pragma vertex vert
    14.             #pragma fragment frag
    15.  
    16.             static const float ANGTORAD = 3.14159265f / 180.0;
    17.  
    18.             // vertex shader inputs
    19.             struct appdata
    20.             {
    21.                 float4 vertex : POSITION; // vertex position
    22.                 float2 uv : TEXCOORD0; // texture coordinate
    23.  
    24.             };
    25.             struct v2f
    26.             {
    27.                 float2 uv : TEXCOORD0;
    28.                 float4 pos : SV_POSITION;
    29.             };
    30.  
    31.             float data[7];
    32.  
    33.             v2f vert (appdata v)
    34.             {
    35.                 v2f o;
    36.                 o.uv = v.uv;
    37.  
    38.                 float angle = data[0];
    39.                 float posx = data[1];
    40.                 float posy = data[2];
    41.                 float pivotx = data[3];
    42.                 float pivoty = data[4];
    43.                 float xscale = data[5];
    44.                 float yscale = data[6];
    45.              
    46.                 float anglerad = angle * ANGTORAD;
    47.                 float px = posx + pivotx;
    48.                 float py = posy + pivoty;
    49.                 float newX = px + (v.vertex.x - px) * xscale * cos(anglerad) - (v.vertex.y - py) * yscale * sin(anglerad);
    50.                 float newY = py + (v.vertex.x - px) * xscale * sin(anglerad) + (v.vertex.y - py) * yscale * cos(anglerad);
    51.  
    52.                 o.pos = UnityObjectToClipPos(float4(newX - pivotx, newY - pivoty, v.vertex.z, v.vertex.w));
    53.  
    54.                 return o;
    55.             }
    56.  
    57.             sampler2D _MainTex;
    58.             fixed4 frag (v2f i) : SV_Target
    59.             {
    60.                 return tex2D(_MainTex, i.uv);
    61.             }
    62.             ENDCG
    63.         }
    64.     }
    65. }
    Now it is almost twice as fast as my original approach.

    The issue now is that when scaling is negative, the quad isn't drawn. I guess I have to use the absolute scale and just flip the uvs in the fragment shader, right? Or is there another trick?
     
    Last edited: Jun 20, 2020
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Shaders default to using back face culling. You need to disable culling by adding
    Cull Off
    in the pass before the
    CGPROGRAM
    block.
    https://docs.unity3d.com/Manual/SL-CullAndDepth.html

    The triangle facing is determined by each triangle’s vertex clockwise draw order in screen space. When you have a negative scale, it means the order is effectively reversed, so you’re now seeing the backs of the triangles.
     
  5. vdweller

    vdweller

    Joined:
    May 24, 2020
    Posts:
    7
    I knew about the clockwise vertex order but I didn't know about the Cull Off property. Thanks a lot!