Search code examples
unity-game-engineshaderhlsl

Recreating Quaternion.FromToRotation in hlsl with a compute shader


I'm working on a grass instancer using compute shaders. Each grass blade shall be rotated to the ground normal.

The problem: I can't use Unity functions like Quaternion.FromToRotation and they musst be implemented on the compute shader and I don't know where I messed up. Currently all instances are rotated on their side. wrong rotation of grass blades

Before doing this on a compute shader I was using raycasts and I've used this function:

private Quaternion GetRotationFromNormal(Vector3 normal)
    {
        Vector3 eulerIdentiy = Quaternion.ToEulerAngles(Quaternion.identity);
        eulerIdentiy.x += 90; //can be removed or changed, depends on your mesh orientation

        if (_randomYAxisRotation) eulerIdentiy.y += UnityEngine.Random.Range(-_maxYRotation, _maxYRotation);

        return Quaternion.FromToRotation(Vector3.up, normal) * Quaternion.Euler(eulerIdentiy);
    }

Each grass blade has a position that I use to calculate the terrain UVs for sampling the heightmap and I have already verified that this step works.

The calculation of the normal looks like this (GetTerrainHeight is just a wrapper for sampling the texture):

float3 CalculateTerrainNormal(float2 uv)
{
    float texelSize = 1.0 / (float) terrainHeightmapResolution;

    float heightL = GetTerrainHeight(uv + float2(-texelSize, 0.0));
    float heightR = GetTerrainHeight(uv + float2(texelSize, 0.0));
    float heightD = GetTerrainHeight(uv + float2(0.0, -texelSize));
    float heightU = GetTerrainHeight(uv + float2(0.0, texelSize));

    float3 tangentX = float3(2.0 * texelSize, heightR - heightL, 0.0);
    float3 tangentZ = float3(0.0, heightU - heightD, 2.0 * texelSize);

    return normalize(cross(tangentZ, tangentX));
}

Each grass blade rotation is calculated like this:

float4 EulerToQuaternion(float pitch, float yaw, float roll) {
    float pitchRad = radians(pitch);
    float yawRad = radians(yaw);
    float rollRad = radians(roll);

    float cy = cos(yawRad * 0.5);
    float sy = sin(yawRad * 0.5);
    float cp = cos(pitchRad * 0.5);
    float sp = sin(pitchRad * 0.5);
    float cr = cos(rollRad * 0.5);
    float sr = sin(rollRad * 0.5);

    float4 q;
    q.x = sr * cp * cy - cr * sp * sy;
    q.y = cr * sp * cy + sr * cp * sy;
    q.z = cr * cp * sy - sr * sp * cy;
    q.w = cr * cp * cy + sr * sp * sy;

    return q;
}

float4 AxisAngleToQuaternion(float3 axis, float angle)
{
    float halfAngle = angle * 0.5;
    float s = sin(halfAngle);
    
    float4 q;
    q.xyz = axis * s;
    q.w = cos(halfAngle);
    
    return q;
}

float4 FromToQuaternion(float3 from, float3 to)
{
    float3 axis = normalize(cross(from, to));
    float dotProduct = dot(from, to);
    float angle = acos(dotProduct);      
    
    return AxisAngleToQuaternion(axis, angle);
}

//...
// For each grass blade: 

float3 normal = CalculateTerrainNormal(uv);

float4 rotationToNormal = FromToQuaternion(float3(0.0, 1.0, 0.0), normal);
        
//For random y rotation
float angle = Random11(instanceSeed) * 360.0;
//Somwhow messed up here to since x has to be used as y axis
float4 yRotation = EulerToQuaternion(angle, 0, 90.);
float4 finalRotation = normalize(rotationToNormal * yRotation); 

float4x4 instanceTransform = CreateTRSMatrix(instancePos, finalRotation, scale);
// ...

Note: when setting float4 yRotation = EulerToQuaternion(angle, 0, 90.); , the 90 is needed as an offset since my 3D model is not oriented correctly.

I can't figure out what exaclty is wrong here. Any hints would be apprecieated.


Solution

  • It's a bit difficult to follow your code and I'm not trying to verify your euler angles to quaternion conversion. However this line:

    float4 finalRotation = normalize(rotationToNormal * yRotation);
    

    doesn't make much sense. In HLSL when you multiply two float4 with each other, you're doing a component wise multiplication (Hadamard product). This is not how you multiply two quaternions with each other.

    A quick google search gave me this quaternion library for HLSL. I can't verify if this is implemented 100% correct, but qmul looks right at first glance.

    Quaternions are not just 4d vectors. It has 3 imaginary units and the multiplication isn't that straight forward. HLSL doesn't have a built-in way to multiply quaternions as there is no built-in hamilton product in place.