Compute shader always has wrong position or scale

I’m using youtube tutorials like a good indie dev to get a compute shader to generate grass. A major difference from the tutorial is I already have my list of world vertices easily because I’m generating my terrain at runtime, but the tutorial assumes a predefined mesh which needs its vertices converted to world coordinates. I already have my vertices in world coordinates which should make this simpler.

However running the below compute shader, if I do matrix multiplication like (rotation X scale&position), everything scales and rotates properly but the positions are all totally wrong. If I multiply (scale&position X rotation), then everything is positioned exactly where it should be but the scale is applied after the rotation, so the meshes are all distorted shapes. I’ve tried transposing things with no luck.

I’ve already adjusted the original code a bit. Right now it’s not doing things the way it should, like it still is using the number of triangles when that won’t be relevant for my purposes, but at the core of it I just want it to output the mesh orientated and position correctly.

#pragma kernel CalculateBladePositions

StructuredBuffer<float3> _TerrainPositions;

RWStructuredBuffer<float4x4> _TransformMatrices;
uniform int _TerrainTriangleCount;
uniform float _Scale;
uniform float _MinBladeHeight;
uniform float _MaxBladeHeight;
uniform float _MinOffset;
uniform float _MaxOffset;
uniform float4x4 _TerrainObjectToWorld;
//uniform float3 _LookDirection;  // New uniform for the direction the object should look in


#define TWO_PI 6.28318530718f

// Function that takes a 2-element seed and returns a random value
// between the min and max bounds.
float randomRange(float2 seed, float min, float max)
{
    float randnum = frac(sin(dot(seed, float2(12.9898, 78.233)))*43758.5453);
    return lerp(min, max, randnum);
}

// Function to rotate around the y-axis by a specified angle.
float4x4 rotationMatrix(float3 anglesDeg)
{
    float sX, cX, sY, cY, sZ, cZ;
    sincos(anglesDeg.x, sX, cX);
    sincos(anglesDeg.y, sY, cY);
    sincos(anglesDeg.z, sZ, cZ);

    float4x4 rotationX = float4x4(1,   0,   0,   0,
                                  0,   cX,  sX, 0,
                                  0,   -sX,  cX,  0,
                                  0,   0,   0,   1);

    float4x4 rotationY = float4x4(cY,  0,   -sY,  0,
                                  0,   1,   0,   0,
                                  sY, 0,   cY,  0,
                                  0,   0,   0,   1);

    float4x4 rotationZ = float4x4(cY,  sY, 0,   0,
                                  -sY,  cY,  0,   0,
                                  0,   0,   1,   0,
                                  0,   0,   0,   1);

    return rotationX * rotationY * rotationZ;
}

// This kernel calculates transformation matrices for each grass blade
// to place them in different positions on the terrain mesh.
[numthreads(64, 1, 1)]
void CalculateBladePositions(uint3 id : SV_DispatchThreadID)
{
    // Avoid running 'overflow' tasks when the number of tasks
    // wasn't divisible by the number of threads.
    if (id.x > _TerrainTriangleCount)
    {
        return;
    }

    float2 randomSeed1 = float2(id.x, id.y);
    float2 randomSeed2 = float2(id.y, id.x);

    float scaleY = _Scale * _MaxBladeHeight;

    float4x4 grassTransformMatrix = float4x4
    (
        _Scale,    0,        0,        _TerrainPositions[id.x].x,
        0,        scaleY,    0,        _TerrainPositions[id.x].y,
        0,        0,        _Scale,    _TerrainPositions[id.x].z,
        0,        0,        0,        1
    );

    float4x4 randomRotationMatrix = rotationMatrix(float3(randomRange(randomSeed1, 0.0f, TWO_PI), randomRange(randomSeed1, 0.0f, TWO_PI), randomRange(randomSeed1, 0.0f, TWO_PI)));

    _TransformMatrices[id.x] = mul(randomRotationMatrix, grassTransformMatrix);
    //_TransformMatrices[id.x] = mul(grassTransformMatrix, randomRotationMatrix);
}

Below is from the actual .shader file. The matrices from above are assigned to objectToWorld becase that’s what the original code was doing. What confuses me is why this all seems to include the position twice, and why that works anyway as long as I do the matrix calculation a certain way. I’ve meshed with this a bunch and basically nothing but exactly this code will output anything at all (and it’s funny because I can see the number of triangles in the scene jump, but I don’t actually see them anywhere)

            v2f vert(appdata v)
            {
                v2f o;

                float4 positionOS = float4(_Positions[v.vertexID], 1.0f);
                float4x4 objectToWorld = _TransformMatrices[v.instanceID];

                o.positionWS = mul(objectToWorld, positionOS);
                o.positionCS = mul(UNITY_MATRIX_VP, o.positionWS);
                o.uv = _UVs[v.vertexID];

                return o;
            }

I kind of have the issue narrowed down. The problem is my rotations seem to apply to both each individual instanced mesh as well as that mesh’s position around world origin as a whole.

So if I have a quad, and I give it a rotation of 90 degrees around the Y axis and a position off the world origin, it rotates 90 degrees around it’s own Y axis but then also rotates 90 degrees around the world Y-axis.

I cannot figure out why it’s doing this, I have tried injecting and removing and skipping so many matrices in this process. I either get nothing out or I get them all doubling up on some calculation.

I did find the documentation on Graphics.RenderPrimitivesIndexed to be somewhat comparable to the example I was already using, but it’s matrix is very simple whereas mine comes from a comptue shader with an issue I don’t understand.

Compute shader simplified section:

float4x4 rotationMatrix(float3 angles)
{
    float sX, cX, sY, cY, sZ, cZ;
    sincos(angles.x, sX, cX);
    sincos(angles.y, sY, cY);
    sincos(angles.z, sZ, cZ);

    float4x4 rotationX = float4x4(1,   0,   0,   0,
                                  0,   cX,  sX, 0,
                                  0,   -sX,  cX,  0,
                                  0,   0,   0,   1);

    float4x4 rotationY = float4x4(cY,  0,   -sY,  0,
                                  0,   1,   0,   0,
                                  sY, 0,   cY,  0,
                                  0,   0,   0,   1);

    float4x4 rotationZ = float4x4(cZ,  sZ, 0,   0,
                                  -sZ,  cZ,  0,   0,
                                  0,   0,   1,   0,
                                  0,   0,   0,   1);
    return rotationX; // * rotationY * rotationZ;
}

[numthreads(64, 1, 1)]
void CalculateBladePositions(uint3 id : SV_DispatchThreadID)
{
    if (id.x > _IndexCount)
    {
        return;
    }

    float2 randomSeed1 = float2(id.x, id.y);
    float2 randomSeed2 = float2(id.y, id.x);
    float2 randomSeed3 = float2(id.x, id.x);

    float scaleY = _Scale * randomRange(randomSeed1, _MinBladeHeight, _MaxBladeHeight);

    float4x4 grassTransformMatrix = float4x4
    (
        _Scale*10,    0,        0,        _TerrainPositions[id.x].x,
        0,        scaleY*10,    0,        _TerrainPositions[id.x].y,
        0,        0,        _Scale*10,    _TerrainPositions[id.x].z,
        0,        0,        0,        1
    );

    float4x4 randomRotationMatrix = rotationMatrix(float3(randomRange(randomSeed1, 0.0f, TWO_PI), randomRange(randomSeed2, 0.0f, TWO_PI), randomRange(randomSeed3, 0.0f, TWO_PI)));

    _TransformMatrices[id.x] = mul(randomRotationMatrix, grassTransformMatrix);
}

Shader:

            v2f vert(appdata v)
            {
                v2f o;

                float4 positionOS = float4(_Positions[v.vertexID], 1.0f);
                float4x4 objectToWorld = _TransformMatrices[v.instanceID];

                o.positionWS = mul(objectToWorld, positionOS);
                o.positionCS = mul(UNITY_MATRIX_VP, o.positionWS);
                o.uv = _UVs[v.vertexID];

                return o;
            }