在HLSL计算着色器中实现Quaternion.FromToRotation修复草实例旋转异常
草实例化系统旋转侧翻问题排查提示
在计算着色器开发草实例化系统时,需要让每片草叶根据地面法线旋转,但无法使用Unity内置的Quaternion.FromToRotation等函数,自行实现后所有草实例出现侧翻错误。
原CPU射线检测实现代码
此前通过射线检测实现时使用的旋转计算函数:
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); }
地形法线计算代码
已验证草叶位置计算正常,当前地形法线计算代码:
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)); }
当前计算着色器旋转实现代码
自行实现的四元数相关函数及草叶旋转计算:
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); // ...
注:设置float4 yRotation = EulerToQuaternion(angle, 0, 90.);中的90是因为3D模型初始朝向问题所需的偏移。
排查关键点
- 四元数乘法顺序错误:Unity中Quaternion乘法是右乘逻辑(
a * b表示先应用b的旋转,再应用a的旋转),你当前代码中rotationToNormal * yRotation的顺序和原CPU代码的FromToRotation(...) * Quaternion.Euler(...)逻辑相反,应改为yRotation * rotationToNormal。 - 欧拉角参数顺序完全颠倒:原CPU代码是给欧拉角X轴加90度偏移,Y轴加随机旋转;但你当前调用
EulerToQuaternion(angle, 0, 90.)时,把随机Y轴旋转的角度放到了X轴(pitch)参数,把90度偏移放到了Z轴(roll)参数,正确调用应为EulerToQuaternion(90., angle, 0.)。 - FromToQuaternion边界情况未处理:当
from和to向量几乎平行时,cross(from, to)会得到零向量,normalize后会出现非法值。需补充判断:float4 FromToQuaternion(float3 from, float3 to) { float dotProduct = dot(from, to); // 向量几乎同向,返回单位四元数 if (dotProduct > 0.999999) return float4(0,0,0,1); // 向量几乎反向,返回绕X轴旋转180度的四元数 if (dotProduct < -0.999999) return float4(1,0,0,0); float3 axis = normalize(cross(from, to)); float angle = acos(dotProduct); return AxisAngleToQuaternion(axis, angle); } - 四元数乘法实现验证:检查
CreateTRSMatrix中或自定义的四元数乘法逻辑是否符合标准四元数乘法规则,错误的乘法实现会直接导致旋转混乱。 - 法线方向验证:可以临时将法线值输出到颜色缓冲区(比如把
normal.xzy映射到RGB范围),确认法线方向是否符合地形起伏,避免法线反转导致旋转错误。
内容的提问来源于stack exchange,提问作者Buttermilch
相关产品推荐
相关产品推荐

