DirectX projekt i Visual Studio 2005

3. Anvendelse af modeller i applikationen

Start med at gennemføre de tre første punkter i Eksempel 1, eller anvend Copy & paste.

  1. Definition af en model
  2. Yaw, pitch og roll
  3. Bevægelse i modellen
  4. Ændring af skærmbilledets størrelse
  5. Styring af flyet

Kommende udfordringer og udvidelser

1. Definition af en model

Hvis man anvender mange figurer af samme slags i en tegning, kan det være en stor fordel at definere figuren i en klasse, som man så kan lave instanser af efter behov. I den første iteration vil jeg vise, hvordan man kan definere en terning i en klasse. Terningen defineres som en enhedsterning placeret med centrum i (0, 0, 0). At der er tale om en enhedsterning betyder, at siderne i terningen har længden 1. Dette bevirker, at man forholdsvis let kan ændre størrelsen på terningen - simpelthen ved at skalere den med en faktor svarende til den længde på siderne, man ønsker. I den følgende kodesekvens er der først defineret en abstrakt Model klasse, som indeholder nogle attributter og metoder, som vil være fælles for flere typer af figurer.

 	public abstract class Model
	{
   		private Vector3 position; // X, Y, Z
   		private int color; // System.Drawing.Color
   		private float size; // Scale factor


		public Model(Vector3 aPosition, int aColor, float aSize)
		{
   			position = aPosition;
   			color = aColor;
   			size = aSize;
   		}


		public int Color 
   		{
   			get { return color; }
   			set { color = value; }
   		}


		public Vector3 Position
   		{
   			get { return position; }
   			set { position = value; }
   		}


		public float Size
   		{
   			get { return size; }
   			set { size = value; }
   		}
	}
    public class Terning : Model
{
private CustomVertex.PositionColored[] vb1 = null;
private CustomVertex.PositionColored[] vb2 = null;
private CustomVertex.PositionColored[] vb3 = null;
private CustomVertex.PositionColored[] vb4 = null;
private CustomVertex.PositionColored[] vb5 = null;
private CustomVertex.PositionColored[] vb6 = null;
 	public Terning(Vector3 aPosition, int aColor, float aSize)
   		: base(aPosition, aColor, aSize)
   	{
   		int i;
   		// Forside
   		vb1 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb1[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, -0.5F), Color);
   		vb1[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, -0.5F), Color);
   		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, -0.5F), Color);
   		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, -0.5F), Color);
   		// Højre
   		vb2 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, -0.5F), Color);
   		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, -0.5F), Color);
   		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, 0.5F), Color);
   		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, 0.5F), Color);
   		// Venstre
   		vb3 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb3[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, 0.5F), Color);
   		vb3[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, -0.5F), Color);
   		vb3[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, -0.5F), Color);
   		vb3[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, 0.5F), Color);
   		// Bagside
   		vb4 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb4[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, 0.5F), Color);
   		vb4[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, 0.5F), Color);
   		vb4[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, 0.5F), Color);
   		vb4[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, 0.5F), Color);
   		// Top
   		vb5 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb5[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, -0.5F), Color);
   		vb5[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.5F, 0.5F), Color);
   		vb5[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, 0.5F), Color);
   		vb5[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.5F, -0.5F), Color);
   		// Bund
   		vb6 = new CustomVertex.PositionColored[4];
   		i = 0;
   		vb6[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, -0.5F), Color);
   		vb6[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, -0.5F), Color);
   		vb6[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, -0.5F, 0.5F), Color);
   		vb6[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, -0.5F, 0.5F), Color);
   	}
 	public void Render(Device device, Matrix m)
   	{
   		device.Transform.World = Matrix.Scaling(Size, Size, Size) * m *
   			Matrix.Translation(Position);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb1);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb2);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb3);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb4);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb5);
   		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 2, vb6);
   	}

I klassen Terning er de 6 sider defineret i hver sin vertex tabel - dette skyldes, at jeg i dette eksempel vil prøve at tegne terningens sider med TriangleFan teknikken. I den situation behøver man kun definere 4 punkter for at tegne de to trekanter, der udgør en af siderne i terningen. Til gengæld skal hver side tegnes for sig, som det også fremgår af klassens Render metode. Render metoden starter i øvrigt med en opsætning af den transformation, som hvert objekt skal udsættes for:

  1. Først en skalering, hvor den ønskede størrelse på objektet dannes.
  2. Herefter multipliceres en matrix, som Render metoden har med som input parameter. Denne kan være sammensat af skaleringer, rotationer og flytninger alt efter behov. Opsætningen heraf sker i applikationens Render metode.
  3. Endelig flyttes objektet til den position i koordinatsystemet, hvor det skal være placeret.

2. Yaw, pitch, roll

I første del af dette eksempel fik vi lavet en klasse, Terning, som beskrev en model, hvoraf der kunne indsættes flere instanser i figuren. Figurerne kunne sættes i bevægelse ved at udføre forskellige transformationer (translate, scaling, rotation) på dem - matricen, som beskriver transformationen, sendes til figurens Render metode som en parameter.

I spil har man ofte brug for objekter, der kan bevæge sig uafhængigt af hinanden i rummet. I sådan et tilfælde kan det være hensigtsmæssigt, at objektet udover informationer om den aktuelle position også indeholder informationer om objektets orientering i rummet. Når man skal beskrive et objekts orientering i rummet, anvendes ofte tre begreber fra flyvningens verden, hvor begreberne bruges til beskrivelse af flyets orientering omkring de tre akser x, y og z: Yaw, pitch og roll.

Yaw beskriver flyets orientering omkring Y-aksen og i et fly styres dette med haleroret. Et andet ord herfor er azimuth.

Pitch beskriver orienteringen af flyets næse, og dette styres i flyet med højderoret. Dette kaldes og deklinationen.

Roll beskriver flyets orientering i forhold til flyets længderetning, og dette styres med balanceklapper, som sidder på vingerne.

I den følgende kode defineres en PaperPlane klasse, som kan danne udgangspunkt for et eksperiment med et flyvende objekt:

public class PaperPlane : Model
{
	private CustomVertex.PositionColored[] vb1 = null;
	private CustomVertex.PositionColored[] vb2 = null;
	private int secondColor;
	public int SecondColor 
	{
		get { return secondColor; }
	}
	public PaperPlane(Vector3 aPosition, int aColor, int aSecondColor, float aSize)
		: base(aPosition, aColor, aSize)
	{
		secondColor = aSecondColor;
		int i;
		// Overside
		vb1 = new CustomVertex.PositionColored[6];
		i = 0;
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.0F, 0.0F, 0.5F), Color);
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.0F, -0.5F), Color);
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.2F, 0.0F, -0.5F), Color);
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(0.0F, -0.5F, -0.5F), Color);
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(-0.2F, 0.0F, -0.5F), Color);
		vb1[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.0F, -0.5F), Color);
		// Underside
		vb2 = new CustomVertex.PositionColored[6];
		i = 0;
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.0F, 0.0F, 0.5F), SecondColor);
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(-0.5F, 0.0F, -0.5F), SecondColor);
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(-0.2F, 0.0F, -0.5F), SecondColor);
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.0F, -0.5F, -0.5F), SecondColor);
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.2F, 0.0F, -0.5F), SecondColor);
		vb2[i++] = new CustomVertex.PositionColored(new Vector3(0.5F, 0.0F, -0.5F), SecondColor);
	}
	public void Render(Device device, Matrix m)
	{
		device.Transform.World = Matrix.Scaling(Size, Size, Size) * 
			Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll) *
			m * Matrix.Translation(Position);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb1);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb2);
	}
}

I klassen Form1 tilføjes erklæringen af et PaperPlane objekt:

	private PaperPlane p1 = new PaperPlane(new Vector3(0, -100, 200), 
		System.Drawing.Color.Beige.ToArgb(), System.Drawing.Color.Gray.ToArgb(), 100F);

I Form1 klassens Render metode tilføjes følgende linier:

	p1.Yaw = angle1;
	p1.Pitch = (float)(2F * Math.PI / 360F) * -15F;
	p1.Roll = (float) (2F * Math.PI / 360F) * 45F;
	p1.Render(device, Matrix.Identity);

Papirsflyveren er drejet både om X- og Z-aksen og roterer langsomt om Y-aksen. Følgende billede er et øjebliksbillede fra en afprøvning af programmet.

 

3. Bevægelse i modellen

I denne modifikation af programmet skal vi se, hvordan vi kan give modellen noget liv. Vi vil sende vores papirflyver afsted på en lille flyvetur, hvor den starter øverst til venstre på skærmen og i nogle cirkler flyver nedad. PaperPlane klassen udvides med et par nye properties:

	private int secondColor;
	private float speed;
	private bool animate;
	public int SecondColor 
	{
		get { return secondColor; }
	}
	public float Speed
	{
		get { return speed; }
		set { speed = value; }
	}
	public bool Animate
	{
		get { return animate; }
		set { animate = value; }
	}

I slutningen af metoden InitializeGraphips tilføjes følgende linier:

	// Papirflyverens orientering i rummet:
	p1.Yaw = (float)(2F * Math.PI / 360F) * 0F;
	p1.Pitch = (float)(2F * Math.PI / 360F) * 10F;
	p1.Roll = (float)(2F * Math.PI / 360F) * -10F;
	p1.Animate = true;

De tre egenskaber Yaw, Pitch og Roll indstilles:

  1. Yaw, som definerer flyets rotation om Y-aksen, sættes til 0 grader, hvilket starter flyet i en retning parallelt med Z-aksen.
  2. Pitch, som definerer flyets rotation om X-aksen, sættes til 10 grader, hvilket starter flyet med næsen sænket.
  3. Roll, som definerer flyets rotation om Z-aksen, sættes til -10 grader, hvilket starter flyet med en lille krængning til højre - dette spiller ingen rolle i de følgende beregninger; men får bevægelsen til at se mere realistisk ud.

Bemærk, at der er lavet omregninger, så data for yaw, pitch og roll kan gives i grader i stedet for radianer. Det kan være lidt nemmere at danne sig et mentalt billede af en vinkel angivet i grader, fremfor vinklen angivet i radianer.

Med egenskaben Animate kan man bestemme, om animationen skal køre eller billedet være fastfrosset.

I Render metoden tilføjes kodelinier, som skal bruges til at beregne den næste position for papirflyveren. I dette program anvendes en stærkt forenklet model for flyets adfærd, som ikke tager højde for alle aerodynamikkens love. I denne beregning tages der hensyn til flyverens Yaw og Pitch. Ved beregningen tages der udgangspunkt i et koordinatsystem med koordinaterne (0, 0, 0). Dette koordinatsystem orienteres i rummet med Yaw og Pitch - herved kommer koordinatsystemets Z-akse til at pege i den retning, som flyet vil flyve i det næste trin. Den afstand flyet tilbagelægger er defineret ved Speed, og derfor translateres koordinatsystemet i Z-aksens retning med denne afstand.

Yaw, Pitch og Roll forbliver uændrede og bruges til en identisk beregning i næste trin. Flyet fortsætter således i et højredrejet spin nedad.

	public void Render(Device device, Matrix m)
	{
		device.Transform.World = Matrix.Scaling(Size, Size, Size) * 
			Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll) *
			m * Matrix.Translation(Position);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb1);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb2);
		if (Animate)
		{
			// Beregning af næste position
			Vector3 v = new Vector3(0, 0, 0);
			v.TransformCoordinate(Matrix.Translation(new Vector3(0, 0, Speed)) * 
				Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll));
			Position += v;
            Yaw += 0.0075F; // Fortsæt drejning til højre
} }

4. Ændring af skærmbilledets størrelse

Det kan undertiden være ønskeligt at ændre vinduets størrelse, således at der eventuelt også bliver ændret på forholdet mellem skærmbilledets bredde og højde - den såkaldte aspect ratio. I sådan en situation skal vi have defineret en event handler, som kan redefinere vores device, således at dens opsætning er tilpasset den nye størrelse på vinduet.

Der oprettes en event handler med følgende indhold:

	protected override void OnResize(EventArgs e)
	{
		// Definering af device til DirectX Rendering
		PresentParameters presentParams = new PresentParameters();
		presentParams.Windowed = true;
		presentParams.SwapEffect = SwapEffect.Discard;
		device = new Device(0, DeviceType.Hardware, this,
			CreateFlags.SoftwareVertexProcessing, presentParams);
		//Placering af kameraet
		device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,
			this.Width / this.Height, 0f, 50f);
		device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, -500),
   			new Vector3(0, 0, 0), new Vector3(0, 1, 0));
   			device.RenderState.Lighting = false;
	}

Indholdet er i dette tilfælde stort set identisk med indholdet i metoden InitializeGraphics; men i andre situationer kan der være andre ændringer i opsætningen - f.eks. en anden kameraplacering.

I klassens construktor tilføjes følgende kodelinie:

		this.OnResize(null);

5. Styring af flyet

Det er nu på tide at se på, hvordan man kan styre flyet - og på den måde give indtryk af at arbejde med en rigtig flysimulator. I første omgang vil vi indbygge en simpel styring af flyet ved hjælp af tastaturet:

Pitch styres med pil op og pil ned på samme måde som i et fly: Tryk på pil op svarer til at trykke styrepinden fremad, hvilket får flyets næse til at dykke nedad - tryk på pil ned svarer til at trække styrepinden tilbage, hvilket får flyets næse til at pege opad. Bevægelsen opad eller nedad øges, så længe knappen holdes nede - når knappen slippes, fortsætter flyet ligeud i den retning, det har på dette tidspunkt.

Yaw og roll styres med knapperne pil til højre og pil til venstre. Jeg har her valgt at bruge en koordineret funktion af de to bevægelser. I et fly styres yaw med pedaler og roll med styrepinden. Det kan være vanskeligt at styre så meget forskelligt med fingrene på et tastatur, hvorfor man da også i flysimulatorer ofte anvender en koordineret funktion, hvis der ikke anvendes et egentligt flightstick.

Tryk på højre pil svarer til at trykke på højre pedal og dreje styrepinden til højre samtidig. Tryk på venstre pil svarer til at trykke på venstre pedal og dreje styrepinden til venstre samtidig. Her fortsætter flyet i den valgte drejning, indtil der kompenseres ved tryk på modsatte pil på tastaturet. I virkeligheden vil flyet langsomt rette ud fra drejningen og lægge sig med vingerne vandret i luften. Man kan prøve at overveje, hvordan en sådan effekt kan kodes ind i programmet.

Styring af tastaturet kræver et nyt namespace og reference i programmet:

using Microsoft.DirectX.DirectInput;

I klassen PaperPlane tilføjes en property, som kan bruges til at huske flyets drejning, som skal fortsætte, indtil kompensation i modsat retning udføres.

	private float yawIncrement;
 	public float YawIncrement
	{
		get { return yawIncrement; }
		set { yawIncrement = value; }
	}

Der skal laves nogle ændringer i variablerne i klassen Form1, idet et objekt til håndtering af keyboardet skal tilføjes. Klassen hertil hedder også Device ligesom klassen til håndtering af skærmen. Vi er derfor nødt til at henvise til det aktuelle namespace for at skelne mellem dem:

	// Global variables for this project
	private Microsoft.DirectX.Direct3D.Device device = null; // Rendering device
	private Microsoft.DirectX.DirectInput.Device keyb = null; // Keyboard device

Initialisering af den nye keyboard device sker i en ny metode, InitializeKeyboard, som kaldes fra Main metoden:

	public void InitializeKeyboard()
	{
		keyb = new Microsoft.DirectX.DirectInput.Device(SystemGuid.Keyboard);
		keyb.SetCooperativeLevel(this, CooperativeLevelFlags.Background | CooperativeLevelFlags.NonExclusive);
		keyb.Acquire();
	}

Til indlæsning af data fra keyboardet indsættes i PaperPlane klassen følgende metode, som kaldes i starten af Render metoden:

	private void ReadUserInput(Microsoft.DirectX.DirectInput.Device keyb)
	{
		KeyboardState keys = keyb.GetCurrentKeyboardState();
		if (keys[Key.Right])
		{ // Flyet drejer og krænger til højre
			YawIncrement += 0.001f;
			Roll -= 0.01f;
		}
		if (keys[Key.Left])
		{ // Flyet drejer og krænger til venstre
			YawIncrement -= 0.001f;
			Roll += 0.01f;
		}
		if (keys[Key.Down])
		{ // Styrepind tilbage - flyet hæver næsen
			Pitch -= 0.01f;
		}
		if (keys[Key.Up])
		{ // Styrepind fremad - flyet sænker næsen
			Pitch += 0.01f;
		}
	}

Og her er til slut hele Render metoden fra PaperPlane klassen:

	public void Render(Microsoft.DirectX.Direct3D.Device device, 
		Microsoft.DirectX.DirectInput.Device keyb, Matrix m)
	{
		this.ReadUserInput(keyb);
		device.Transform.World = Matrix.Scaling(Size, Size, Size) * 
			Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll) *
			m * Matrix.Translation(Position);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb1);
		device.DrawUserPrimitives(PrimitiveType.TriangleFan, 4, vb2);
		if (Animate)
		{
			// Beregning af næste position
			Vector3 v = new Vector3(0, 0, 0);
			v.TransformCoordinate(Matrix.Translation(new Vector3(0, 0, Speed))
				* Matrix.RotationYawPitchRoll(Yaw, Pitch, Roll));
			Position += v;
			Yaw += YawIncrement;
		}
	}

Tilbage er kun at ønske som rigtige flightsimmers: "Happy landings". Vedhæftede arkiv-fil indeholder en mappe med koden, som svarer til eksemplet hertil.

Kommende udfordringer og udvidelser

Udvidelse med mulighed for forskellige views: Towerview, Trackplane, Cockpit, Fly-by

Texture til ground-plane

Bygninger på jorden

Collission-detection

Z-buffer håndtering/korrekt dybdesortering

Forbedret flightmodel

Skygger


Sidst opdateret: 16. februar 2007

Index