2
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ What's the precision of your angles? Instead of always caculating sin and cos you could just have 2 tables with the values in it. \$\endgroup\$
    – ott--
    Commented Jun 3, 2013 at 11:43
  • 1
    \$\begingroup\$ @ott--: I'll leave the optimization for later :) \$\endgroup\$ Commented Jun 3, 2013 at 12:48

1 Answer 1

3
\$\begingroup\$

Probably the simplest way to modify your code to get rotation (roll) is to modify the calculation of upVector. Specifically, upVector should be rotated around the planeNormal axis by the desired roll angle; then the calculation of rightVector and all the vertex positions will incorporate the rotation, too.

A convenient formula for this is Rodrigues' rotation formula, which expresses rotation about an axis by an angle in terms of vector operations.

\$\endgroup\$
1
  • \$\begingroup\$ That worked perfectly. Thanks a lot. However, I'm wondering if the way I've written it is really necessary in a shader. It seems really verbose and I would have expected that there is a shorter way to express the same thing. \$\endgroup\$ Commented Jun 3, 2013 at 10:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .