Today I was working on a particle system and I can't figure out how to give the particle billboards a rotation (so to have them spinning clockwise or counter-clockwise while facing the player). I assume the core of the problem is my complete lack of understanding of trigonometry.
So, first of all, here is my geometry shader that will produce quads that rotate around their center:
[maxvertexcount(4)]
void GeometryShaderProc( point VertexShaderInput particle[1], inout TriangleStream<PixelShaderInput> triStream ) {
PixelShaderInput vertex = (PixelShaderInput)0;
vertex.Color = particle[ 0 ].Color;
vertex.Texture = particle[ 0 ].Texture;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
float QuadSizeX = particle[ 0 ].Size.x / ViewportDimensions.x * 0.5;
float QuadSizeY = particle[ 0 ].Size.y / ViewportDimensions.y * 0.5;
float a = sin( particle[ 0 ].Position.w ) * QuadSizeX;
float b = cos( particle[ 0 ].Position.w ) * QuadSizeY;
float c = cos( particle[ 0 ].Position.w ) * QuadSizeX;
float d = sin( particle[ 0 ].Position.w ) * QuadSizeY;
// Left Top
vertex.Position.x = particle[ 0 ].Position.x - c;
vertex.Position.y = particle[ 0 ].Position.y - d;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 0 );
triStream.Append( vertex );
// Left Bottom
vertex.Position.x = particle[ 0 ].Position.x - a;
vertex.Position.y = particle[ 0 ].Position.y + b;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 1 );
triStream.Append( vertex );
// Right Top
vertex.Position.x = particle[ 0 ].Position.x + a;
vertex.Position.y = particle[ 0 ].Position.y - b;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 0 );
triStream.Append( vertex );
// Right Bottom
vertex.Position.x = particle[ 0 ].Position.x + c;
vertex.Position.y = particle[ 0 ].Position.y + d;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 1 );
triStream.Append( vertex );
}
In case it isn't obvious, I'm using the w
component of the input vector as the input value for the rotation.
So far so great, until I move the camera and the quads aren't facing the camera any more. So, obviously I need to rotate them.
An approach to solving that was quickly found online, so I adjusted the shader:
[maxvertexcount(4)]
void GeometryShaderProc( point VertexShaderInput particle[1], inout TriangleStream<PixelShaderInput> triStream ) {
PixelShaderInput vertex = (PixelShaderInput)0;
vertex.Color = particle[ 0 ].Color;
vertex.Texture = particle[ 0 ].Texture;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
float3 planeNormal = particle[ 0 ].Position.xyz - CameraPosition;
planeNormal.y = 0.0f;
planeNormal = normalize(planeNormal);
float3 upVector = float3(0.0f, 1.0f, 0.0f);
float3 rightVector = normalize(cross(planeNormal, upVector));
// Left Top
vertex.Position.xyz = particle[ 0 ].Position.xyz - rightVector + upVector;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 0 );
triStream.Append( vertex );
// Left Bottom
vertex.Position.xyz = particle[ 0 ].Position.xyz - rightVector;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 1 );
triStream.Append( vertex );
// Right Top
vertex.Position.xyz = particle[ 0 ].Position.xyz + rightVector + upVector;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 0 );
triStream.Append( vertex );
// Right Bottom
vertex.Position.xyz = particle[ 0 ].Position.xyz + rightVector;
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 1 );
triStream.Append( vertex );
}
Great! Now my quads are always facing the camera, but they no longer rotate around their center. I thought it would be simple enough to combine both approaches, but I just keep failing.
So, how do you do this, properly?
After applying the solution proposed by Nathan Reed and fixing a couple of other issues, I've now finally arrived at this version:
[maxvertexcount(4)]
void GeometryShaderProc( point VertexShaderInput particle[1], inout TriangleStream<PixelShaderInput> triStream ) {
PixelShaderInput vertex = (PixelShaderInput)0;
vertex.Color = particle[ 0 ].Color;
vertex.Texture = particle[ 0 ].Texture;
vertex.Position.z = particle[ 0 ].Position.z;
vertex.Position.w = 1;
float3 planeNormal = particle[ 0 ].Position.xyz - CameraPosition;
planeNormal = normalize( planeNormal );
float3 upVector = float3( 0.0f, 1.0f, 0.0f );
// Rotate up vector around plane normal
float ux = planeNormal.x * upVector.x;
float uy = planeNormal.x * upVector.y;
float uz = planeNormal.x * upVector.z;
float vx = planeNormal.y * upVector.x;
float vy = planeNormal.y * upVector.y;
float vz = planeNormal.y * upVector.z;
float wx = planeNormal.z * upVector.x;
float wy = planeNormal.z * upVector.y;
float wz = planeNormal.z * upVector.z;
float alpha = sin( particle[ 0 ].Position.w );
float beta = cos( particle[ 0 ].Position.w );
upVector.x = planeNormal.x * ( ux + vy + wz ) + ( upVector.x * ( planeNormal.y * planeNormal.y + planeNormal.z * planeNormal.z ) - planeNormal.x * ( vy + wz ) ) * beta + ( -wy + vz ) * alpha;
upVector.y = planeNormal.y * ( ux + vy + wz ) + ( upVector.y * ( planeNormal.x * planeNormal.x + planeNormal.z * planeNormal.z ) - planeNormal.y * ( ux + wz ) ) * beta + ( wx - uz ) * alpha;
upVector.z = planeNormal.z * ( ux + vy + wz ) + ( upVector.z * ( planeNormal.x * planeNormal.x + planeNormal.y * planeNormal.y ) - planeNormal.z * ( ux + vy ) ) * beta + ( -vx + uy ) * alpha;
float3 rightVector = normalize( cross( planeNormal, upVector ) );
upVector = cross( rightVector, planeNormal );
rightVector *= particle[ 0 ].Size.x;
upVector *= particle[ 0 ].Size.y;
// Left Top
vertex.Position.xyz = particle[ 0 ].Position.xyz - ( rightVector * 0.5 ) + ( upVector * 0.5 );
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 0 );
triStream.Append( vertex );
// Left Bottom
vertex.Position.xyz = particle[ 0 ].Position.xyz - ( rightVector * 0.5 ) - ( upVector * 0.5 );
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 0, 1 );
triStream.Append( vertex );
// Right Top
vertex.Position.xyz = particle[ 0 ].Position.xyz + ( rightVector * 0.5 ) + ( upVector * 0.5 );
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 0 );
triStream.Append( vertex );
// Right Bottom
vertex.Position.xyz = particle[ 0 ].Position.xyz + ( rightVector * 0.5 ) - ( upVector * 0.5 );
vertex.Position = mul( float4( vertex.Position.xyz, 1.0 ), ViewProjection );
vertex.TextureCoordinate = float2( 1, 1 );
triStream.Append( vertex );
}
The first problem I had was that the resulting rotation would happen around one corner of the quad, not the center. Another issue was that I was using an approach designed for billboards, which stand on the "ground". So they were never fully rotated at the camera.
Given that the components of the up vector are all one and zero, it could probably simplified further, but for the time being, I wanted to leave it verbose. Hopefully the shader compiler realizes this fact as well.
sin
andcos
you could just have 2 tables with the values in it. \$\endgroup\$