Energy Ripple Geometry Shader (C++, DirectX)

Home / Portfolio / Programming / Energy Ripple Geometry Shader (C++, DirectX)

A geometry shader exam assignment

Features:

  • Tesselation
  • Sine based offsets
  • Real time interaction to create even more offsets
  • Passing offset to the pixel shader to implement even more effects

This is my exam assignment for a class called Graphics programming. It required you to write a shader with a geometry pass and I decided to do a shader that could be used in my other project.

The Energy ripple shader allows you to send waves through an object by first tesselating the entire object, offsetting the vertex according to it’s normal and implementing a sine wave in 2 directions. This creates the constant rippling effect throughout the object. Then by sending float4 to your object containing (x,y,z) coordinates and a timer in the last float you can create ripples accross the entire object. These offsets are then passed to the pixelshader and are used to calculate the colour and opacity.

Have a video:

/*

DX10 GS Energy Rippling

keywords: Energy Geometry Ripples DX10

*/

cbuffer cbPerObject
{
	// The World View Projection Matrix
	float4x4 m_MatrixWorldViewProj : WorldViewProjection;
	float4x4 m_MatrixWorld : World;

	//the number of impacts currently registered to the object 
	int m_numImpacts;

	//Color
	float4 m_Color;

	//maxrange till fully faded
	float m_fadeRange;

	//width of a ripple
	float m_rippleWidth;

	//maxheight of a ripple
	float m_rippleMaxHeight;

	//time till fully faded
	float m_fadeTime;

	//an array of float3 objects containing the position
	//& and array containing the timers since impact

	};

//rasterizer
RasterizerState DisableCulling { CullMode = NONE; };

float4 m_Impacts[40];
float m_ImpactTimes[40];

Texture2D m_EnergyBaseTexture;

float m_Time = 1.0f;
float m_WaveHeight = 0.05f;
float m_WaveFrequency = 12.0f;
SamplerState samLinear
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = Wrap;// or Mirror or Clamp or Border
    AddressV = Wrap;// or Mirror or Clamp or Border
};

BlendState AlphaBlending 
{     
	BlendEnable[0] = TRUE;
	SrcBlend = SRC_ALPHA;
    DestBlend = INV_SRC_ALPHA;
	BlendOp = ADD;
	SrcBlendAlpha = ONE;
	DestBlendAlpha = ZERO;
	BlendOpAlpha = ADD;
	RenderTargetWriteMask[0] = 0x0f;
};

DepthStencilState EnableDepthWriting
{
	DepthEnable = TRUE;
};

// The main vertex shader
// STRUCTS
// vertex shader data
struct VS_DATA
{
	float3 Position : POSITION;
	float3 Normal : NORMAL;
    float2 TexCoord : TEXCOORD;
};

// a triangle made up of three vertices
struct Triangle
{
	VS_DATA v0;
	VS_DATA v1;
	VS_DATA v2;
};

// geometry shader data
struct GS_DATA
{
	float Height : TEXCOORD1;
	float4 Position : SV_POSITION;
	float3 Normal : NORMAL;
	float2 TexCoord : TEXCOORD0;
};

// VERTEX SHADER
VS_DATA MainVS(VS_DATA vsData)
{
	return vsData;
}

float CalculateHeight(VS_DATA vertex)
{
	//calculate the world position of the current vertex to eventually find the distance the impact point
	float4 vertexPosWorld = mul(float4(vertex.Position,1),m_MatrixWorld);

	//for each impact point, we are going to check how high the resulting ripple at the location of that vertex should be
	//we will use the greatest height found as the offset for the vertex
	// --- this is with the assumption that the time passed is increasing in nature per impact 
	//and the impacts are removed within C++ when the maxtime has been passed ---
	int loop = m_numImpacts;
	float totalHeight = 0;

	[loop]  for(int i = 0; i < loop; ++i)
	{
		float distanceToImpact = distance(vertexPosWorld,m_Impacts[i]);

		if(distanceToImpact < m_fadeRange)
		{
			//calculate the interpolation amount for finding the currentRippleHeight
			float relativeTime = m_Impacts[i].w/m_fadeTime;

			//calculate the current highest part of the ripple by lerping between 0 and m_rippleMaxHeight
			float currentRippleHeight = (1-relativeTime)*m_rippleMaxHeight;//= lerp(m_rippleMaxHeight,0,relativeTime);

			//calculate the interp amount for the current height of the point 
			float distanceToRippleTop = relativeTime*m_fadeRange;
			float heightInterpAmount;

			if(distanceToImpact < distanceToRippleTop)
				heightInterpAmount = (distanceToRippleTop - distanceToImpact)/(m_rippleWidth/2);
			else
				heightInterpAmount = (distanceToImpact - distanceToRippleTop)/(m_rippleWidth/2);

			//lerp between 0 and currentrippleheight using heightInterpAmount 
			//to receive the intended height for the current impactRipple
			float intendedHeight = lerp(currentRippleHeight,0,heightInterpAmount);

			//check if it is higher than the previous ripples, if so: update the totalheight
			if(intendedHeight > totalHeight){
				totalHeight = intendedHeight;
			}
		}	

		continue;
	}

	float xAmplitude = m_WaveHeight;
	float xPeriod = m_WaveFrequency;
	float xXShift = m_Time;

	float zAmplitude = xAmplitude/1.25f;
	float zPeriod = m_WaveFrequency;
	float zXShift = m_Time;

	totalHeight += xAmplitude * sin(xPeriod * vertex.TexCoord.x + xXShift);
	totalHeight += zAmplitude * sin(zPeriod * vertex.TexCoord.y + zXShift);

	return totalHeight;
}

void CreateVertex(inout TriangleStream<GS_DATA> triStream, float3 pos, float3 normal, float2 texCoord, float height)
{
	//Step 1. Create a GS_DATA object
	GS_DATA gsData = (GS_DATA)0;

	// passing the extra height variable for peak detection
	gsData.Height = height;

	//Step 2. Transform the position using the WVP Matrix and assign it to (GS_DATA object).Position (Keep in mind: float3 -> float4)
	gsData.Position = mul(float4(pos.x, pos.y, pos.z, 1), m_MatrixWorldViewProj);

	//Step 3. Transform the normal using the World Matrix and assign it to (GS_DATA object).Normal (Only Rotation, No translation!)
	gsData.Normal = mul(float4(normal.x, normal.y, normal.z, 1), m_MatrixWorldViewProj);

	//Step 4. Assign texCoord to (GS_DATA object).TexCoord
	gsData.TexCoord = texCoord;

	//Step 5. Append (GS_DATA object) to the TriangleStream parameter (TriangleStream::Append(...))
	triStream.Append(gsData);
}

[maxvertexcount(100)]
void RippleGenerator(triangle VS_DATA vertices[3], inout TriangleStream<GS_DATA> triStream)
{
	// calculate the midpoints of each of the triangle's edges
	// do this for position, normal and texture coordinates
	VS_DATA newVertsLevel0[3];
	newVertsLevel0[0].Position = 0.5f * (vertices[0].Position + vertices[1].Position);
	newVertsLevel0[0].Normal = 0.5f * (vertices[0].Normal+ vertices[1].Normal);
	newVertsLevel0[0].TexCoord = 0.5f * (vertices[0].TexCoord + vertices[1].TexCoord);

	newVertsLevel0[1].Position = 0.5f * (vertices[1].Position + vertices[2].Position);
	newVertsLevel0[1].Normal = 0.5f * (vertices[1].Normal+ vertices[2].Normal);
	newVertsLevel0[1].TexCoord = 0.5f * (vertices[1].TexCoord + vertices[2].TexCoord);

	newVertsLevel0[2].Position = 0.5f * (vertices[2].Position + vertices[0].Position);
	newVertsLevel0[2].Normal = 0.5f * (vertices[2].Normal+ vertices[0].Normal);
	newVertsLevel0[2].TexCoord = 0.5f * (vertices[2].TexCoord + vertices[0].TexCoord);

	// tessellation level 0
	// use the 3 new vertices to make four triangles from the one original triangle
	Triangle newTrisLevel0[4];
	//035
	newTrisLevel0[0].v0 = vertices[0];
	newTrisLevel0[0].v1 = newVertsLevel0[0];
	newTrisLevel0[0].v2 = newVertsLevel0[2];
	//534
	newTrisLevel0[1].v0 = vertices[1];
	newTrisLevel0[1].v1 = newVertsLevel0[1];
	newTrisLevel0[1].v2 = newVertsLevel0[0];
	//341
	newTrisLevel0[2].v0 = vertices[2];
	newTrisLevel0[2].v1 = newVertsLevel0[2];
	newTrisLevel0[2].v2 = newVertsLevel0[1];
	//542
	newTrisLevel0[3].v0 = newVertsLevel0[0];
	newTrisLevel0[3].v1 = newVertsLevel0[1];
	newTrisLevel0[3].v2 = newVertsLevel0[2];

	for(int i = 0; i < 4; ++i)
	{
		// tessellation level 1
		// do the same, but this time, within those four newly created triangles
		VS_DATA newVertsLevel1[3];
		newVertsLevel1[0].Position = 0.5f * (newTrisLevel0[i].v0.Position + newTrisLevel0[i].v1.Position);
		newVertsLevel1[0].Normal = 0.5f * (newTrisLevel0[i].v0.Normal+ newTrisLevel0[i].v1.Normal);
		newVertsLevel1[0].TexCoord = 0.5f * (newTrisLevel0[i].v0.TexCoord + newTrisLevel0[i].v1.TexCoord);

		newVertsLevel1[1].Position = 0.5f * (newTrisLevel0[i].v1.Position + newTrisLevel0[i].v2.Position);
		newVertsLevel1[1].Normal = 0.5f * (newTrisLevel0[i].v1.Normal+ newTrisLevel0[i].v2.Normal);
		newVertsLevel1[1].TexCoord = 0.5f * (newTrisLevel0[i].v1.TexCoord + newTrisLevel0[i].v2.TexCoord);

		newVertsLevel1[2].Position = 0.5f * (newTrisLevel0[i].v2.Position + newTrisLevel0[i].v0.Position);
		newVertsLevel1[2].Normal = 0.5f * (newTrisLevel0[i].v2.Normal+ newTrisLevel0[i].v0.Normal);
		newVertsLevel1[2].TexCoord = 0.5f * (newTrisLevel0[i].v2.TexCoord + newTrisLevel0[i].v0.TexCoord);

		Triangle newTrisLevel1[4];
		//035
		newTrisLevel1[0].v0 = newTrisLevel0[i].v0;
		newTrisLevel1[0].v1 = newVertsLevel1[0];
		newTrisLevel1[0].v2 = newVertsLevel1[2];
		//534
		newTrisLevel1[1].v0 = newTrisLevel0[i].v1;
		newTrisLevel1[1].v1 = newVertsLevel1[1];
		newTrisLevel1[1].v2 = newVertsLevel1[0];
		//341
		newTrisLevel1[2].v0 = newTrisLevel0[i].v2;
		newTrisLevel1[2].v1 = newVertsLevel1[2];
		newTrisLevel1[2].v2 = newVertsLevel1[1];
		//542
		newTrisLevel1[3].v0 = newVertsLevel1[0];
		newTrisLevel1[3].v1 = newVertsLevel1[1];
		newTrisLevel1[3].v2 = newVertsLevel1[2];

		for(int k = 0; k < 4; ++k)
		{
			// for each of the created triangles, we have to change its position to create a ripple
			// we're not doing this in the vertex shader because we've already passed this stage
			// for each of the variables, calculate its height based on the distance to the impact point

			//transform the vertices with the height found by using CalculateHeight
			// draw the newly created geometry

			float height = CalculateHeight(newTrisLevel1[k].v0);
			newTrisLevel1[k].v0.Position += height*normalize(newTrisLevel1[k].v0.Normal);
			CreateVertex(triStream,newTrisLevel1[k].v0.Position,newTrisLevel1[k].v0.Normal,newTrisLevel1[k].v0.TexCoord,height);

			height = CalculateHeight(newTrisLevel1[k].v1);
			newTrisLevel1[k].v1.Position += height*normalize(newTrisLevel1[k].v1.Normal);
			CreateVertex(triStream,newTrisLevel1[k].v1.Position,newTrisLevel1[k].v1.Normal,newTrisLevel1[k].v1.TexCoord,height);

			height = CalculateHeight(newTrisLevel1[k].v2);
			newTrisLevel1[k].v2.Position += height*normalize(newTrisLevel1[k].v2.Normal);
			CreateVertex(triStream,newTrisLevel1[k].v2.Position, newTrisLevel1[k].v2.Normal,newTrisLevel1[k].v2.TexCoord,height);

			triStream.RestartStrip();

		}
	}
}

// The main pixel shader
float4 MainPS(GS_DATA input) : SV_TARGET 
{
	float4 diff = lerp(m_Color,float4(1,1,1,1),input.Height/m_rippleMaxHeight);

	float alpha = lerp(m_EnergyBaseTexture.Sample(samLinear,input.TexCoord).r,3,input.Height/m_rippleMaxHeight);

	diff.a =clamp(alpha,0.1f,0.6f);

	return diff;
}

// Default Technique
technique10 DefaultTechnique {
	pass p0 {
		SetRasterizerState(DisableCulling);	
		SetVertexShader(CompileShader(vs_4_0, MainVS()));
		SetGeometryShader(CompileShader(gs_4_0, RippleGenerator()));
		SetPixelShader(CompileShader(ps_4_0, MainPS()));

		SetDepthStencilState(EnableDepthWriting,0);
		SetBlendState(AlphaBlending, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF);
	}
}