dev.nlited.com

>>

Moving Along the Surface

<<<< prev
next >>>>

2017-11-27 05:07:35 chip Page 2072 📢 PUBLIC

Nov 26 2017

I want to place an object on the surface of the grid and move it around, following the contour of the terrain.

  1. Create a "pawn" object.
  2. Update its position.
  3. Render the object.

Steps 1 and 3 are easy, just copy how the sun object is created and use a cylinder instead.


Create Pawn object: class Game { private: std::unique_ptr<GeometricPrimitive> objPawn; Vector3 pawnPt; Matrix mtrxPawn; }; void Game::CreateDevice2(void) { objPawn= GeometricPrimitive::CreateCylinder(d3dContext.Get(),0.25f,gridSz/100.0f); pawnPt= Vector3(0,0,0); } void Game::Update2(float totalTime,float elapsedTime) { mtrxPawn= Matrix::Identity; mtrxPawn.Translation(pawnPt); } void Game::RenderObjects(XMMATRIX &view) { objPawn->Draw(mtrxPawn,view,mtrxProj); }

Step 2 is the tricky part. I need a new function to find the vertex that is nearest a given (x,z) position on the grid. The elevation is taken directly from vertex.position.y and applied to the shape position.

Position the Pawn: class Game { private: VertexPositionNormalColor &getVertex(float x0, float y0); }; //Returns nearest vertex VertexPositionNormalColor &Game::getVertex(float x0, float y0) { int _x0= (int)(tileCt*(x0/gridSz)); _x0= (_x0<0) ? 0 : (_x0>=tileCt) ? tileCt-1 : _x0; int _y0= (int)(tileCt*(y0/gridSz)); _y0= (_y0<0) ? 0 : (_y0>=tileCt) ? tileCt-1 : _y0; return(getVertex(_x0,_y0)); } void Game::Update2(float totalTime,float elapsedTime) { VertexPositionNormalColor vtxPawn= getVertex(pawnPt.x,pawnPt.z); pawnPt.y= vtxPawn.position.y + 0.125f; mtrxPawn.Translation(pawnPt); }

I want the object to be flat against the surface, instead of always pointing straight up. The vertex normal vector is used to orient the shape flat against the surface by using it for the "UP" transform.

Pawn orientation: void Game::Update2(float totalTime, float elapsedTime) { VertexPositionNormalColor vtxPawn= getVertex(pawnPt.x,pawnPt.z); pawnPt.y= vtxPawn.position.y + 0.125f; mtrxPawn.Translation(pawnPt); mtrxPawn.Up(vtxPawn.normal); }

I use a simple "pong" motion to move the object across the grid, rebounding off the edges.

Pawn motion: class Game { private: std::unique_ptr<GeometricPrimitive> objPawn; Vector3 pawnPt; Vector3 pawnMv; Matrix mtrxPawn; }; void Game::CreateDevice2(void) { objPawn= GeometricPrimitive::CreateCylinder(d3dContext.Get(),0.25f,gridSz/100.0f); pawnPt= Vector3(0,0,0); pawnMv= Vector3(0.1f,0,0.1f); } void Game::Update2(float totalTime, float elapsedTime) { // Move the pawn along the surface of the grid. mtrxPawn= Matrix::Identity; pawnPt+= pawnMv*elapsedTime; if(pawnPt.x < 0.0f) { pawnPt.x= pawnMv.x= (float)rand()/RAND_MAX + 0.10f; } else if(pawnPt.x >= gridSz) { pawnMv.x= -((float)rand()/RAND_MAX + 0.10f); pawnPt.x= gridSz + pawnMv.x; } if(pawnPt.z < 0.0f) { pawnPt.z= pawnMv.z= (float)rand()/RAND_MAX + 0.10f; } else if(pawnPt.z >= gridSz) { pawnMv.z= -((float)rand()/RAND_MAX + 0.10f); pawnPt.z= gridSz + pawnMv.z; } VertexPositionNormalColor vtxPawn= getVertex(pawnPt.x,pawnPt.z); pawnPt.y= vtxPawn.position.y + 0.125f; mtrxPawn.Translation(pawnPt); mtrxPawn.Up(vtxPawn.normal); }
Direct3D simple grid

This is close but there are a couple problems. There is a severe jump in elevation as pawn jumps from one vertex to another. The solution is to interpolate the elevation between the 4 nearest vertices. This interpolation should also be applied to the normal vector.

The object is distorted by the UP transformation. I think this is because using the UP transformation is not the right answer; it is transforming the view instead of rotating the object.

This is my first attempt at interpolation:

getVertex with interpolation: //Returns nearest vertex void Game::getVertex(float x0, float z0, VertexPositionNormalColor &vtxDst) { int nX= (int)(tileCt*(x0/gridSz)); nX= (nX<0) ? 0 : (nX>=tileCt) ? tileCt-1 : nX; int nZ= (int)(tileCt*(z0/gridSz)); nZ= (nZ<0) ? 0 : (nZ>=tileCt) ? tileCt-1 : nZ; vtxDst= getVertex(nX,nZ); if(nX+1 < tileCt && nZ+1 < tileCt) { // (x0,z0) will always be greater than pt(nX,nZ) // Interpolate D(nX,nZ)+U(nX+1,nZ)+R(nX+1+nZ+1)+(nX,nZ+1) Vector3 vT(x0,vtxDst.position.y,z0); VertexPositionNormalColor v0(vtxDst); VertexPositionNormalColor v1(getVertex(nX+1,nZ));; VertexPositionNormalColor v2(getVertex(nX+1,nZ+1)); VertexPositionNormalColor v3(getVertex(nX,nZ+1)); float d0= SimpleMath::Vector3::Distance(vT,v0.position); float d1= SimpleMath::Vector3::Distance(vT,v1.position); float d2= SimpleMath::Vector3::Distance(vT,v2.position); float d3= SimpleMath::Vector3::Distance(vT,v3.position); float dSum= d0+d1+d2+d3; // TODO: Convert from Vector3 to XMFLOAT3 Vector3 pos= ((v0.position*d0+v1.position*d1+v2.position*d2+v3.position*d3)/dSum); Vector3 nml= ((v0.normal*d0+v1.normal*d1+v2.normal*d2+v3.normal*d3)/dSum); vtxDst.position= pos; vtxDst.normal= nml; } }

This seems to be only marginally better, the movement is still very jerky. Part of the problem is that the center of the object is not the base. My simplistic initial approach was to elevate the tracking point vertically (+y) by half the height of the cylinder. This is wrong for (at least) two reasons. First, the distance from the surface up to the center is not the distance from the center down through the surface normal, resulting in the base either floating above or extend down through the surface. Second, I am using the surface normal from directly under the center instead of the surface normal under the base which is visually wrong.

I should be projecting the tracking point up through the surface normal to determine the center point.

Direct3D simple grid  Direct3D simple grid 

I created a vector from the base to the center of the shape (0,0.125,0), multiplied by the surface normal, and applied it as a translation to the matrix.

Pawn update: VertexPositionNormalColor vtxPawn; getVertex(pawnPt.x,pawnPt.z,vtxPawn); //pawnPt.y= vtxPawn.position.y + 0.125f; mtrxPawn.Translation(pawnPt); Vector3 ptCenter(0,0.125f,0); ptCenter*= vtxPawn.normal; mtrxPawn.Translation(-ptCenter); mtrxPawn.Up(vtxPawn.normal);

The results were not at all what I expected and left me perplexed...

My weighted average code is wrong, by multiplying the distance I am biasing in favor of distance instead of against. I need to determine the maximum distance and use the difference. My second attempt is smoother (although still jerky in some areas) and the base is much closer to the surface.

Direct3D simple grid

Interpolation: //Returns nearest vertex void Game::getVertex(float x0, float z0, VertexPositionNormalColor &vtxDst) { int nX= (int)(tileCt*((x0-rGrid.left)/(rGrid.right - rGrid.left))); nX= (nX<0) ? 0 : (nX>=tileCt) ? tileCt-1 : nX; int nZ= (int)(tileCt*((z0-rGrid.top)/(rGrid.bottom - rGrid.top))); nZ= (nZ<0) ? 0 : (nZ>=tileCt) ? tileCt-1 : nZ; vtxDst= getVertex(nX,nZ); if(nX+1 < tileCt && nZ+1 < tileCt) { // (x0,z0) will always be greater than pt(nX,nZ) // Interpolate D(nX,nZ)+U(nX+1,nZ)+R(nX+1+nZ+1)+(nX,nZ+1) Vector3 vT(x0,vtxDst.position.y,z0); VertexPositionNormalColor v0(vtxDst); VertexPositionNormalColor v1(getVertex(nX+1,nZ));; VertexPositionNormalColor v2(getVertex(nX+1,nZ+1)); VertexPositionNormalColor v3(getVertex(nX,nZ+1)); // Determine the distance from vT to each known vertex. float d0= SimpleMath::Vector3::Distance(vT,v0.position); float d1= SimpleMath::Vector3::Distance(vT,v1.position); float d2= SimpleMath::Vector3::Distance(vT,v2.position); float d3= SimpleMath::Vector3::Distance(vT,v3.position); float dMax= std::max(d0,std::max(d1,std::max(d2,d3))); // Bias each vertex so that the closer vertex has more influence. Vector3 dv0= (1.0f-(d0/dMax))*v0.position; Vector3 dn0= (1.0f-(d0/dMax))*v0.normal; Vector3 dv1= (1.0f-(d1/dMax))*v1.position; Vector3 dn1= (1.0f-(d1/dMax))*v1.normal; Vector3 dv2= (1.0f-(d2/dMax))*v2.position; Vector3 dn2= (1.0f-(d2/dMax))*v2.normal; Vector3 dv3= (1.0f-(d3/dMax))*v3.position; Vector3 dn3= (1.0f-(d3/dMax))*v3.normal; float dSum= 4.0f - (d0/dMax + d1/dMax + d2/dMax + d3/dMax); Vector3 pos= (dv0+dv1+dv2+dv3)/dSum; Vector3 nml= (dn0+dn1+dn2+dn3)/dSum; vtxDst.position= pos; vtxDst.normal= nml; } }

I still don't know how to properly apply the normal the object's transformation matrix. My math skillz need some serious help. Math Books I rented 3D Math Primer for Graphics and Game Development, 2nd Edition for $20... I am waiting for this massive book to download...

I need some visual aids to help me understand what is going on. I want to draw some spheres and lines between the various points, which means creating a thin shape class to make it easier.

The new GameObj class makes it easier to separate the code for drawing specific things from the overall game framework.

The declarations:

Game.h: typedef struct RectF_s { float left,top,right,bottom; } RECTF; class Game; class GameObj { public: static HRESULT Create(GameObj *&pObj, const char *Type, Game *pGame, ID3D11DeviceContext *pDC); static void Destroy(GameObj *&pObj); virtual ~GameObj(void); virtual void Update(float secTotal, float secElapsed, Keyboard *pKB, Mouse *pMouse); virtual void Render(const XMMATRIX &view, const XMMATRIX &proj); const Vector3 &GetPt(void); protected: GameObj(Game *pGame); Game *pGame; //Data Vector3 pos; //Current position Vector3 vel; //Current velocity Vector3 dir; //Face: x=roll y=yaw z=pitch Matrix xform; //Transform matrix private: }; class Game { public: const RECTF &GetArena(void) { return(rGrid); }; VertexPositionNormalColor &getVertex(int x0, int y0) { return(vecVertex.at(y0*tileCt+x0)); }; void getVertex(float x0, float y0, VertexPositionNormalColor &vtxDst); private: RECTF rGrid; //Grid dimensions ... GameObj *pPawn; //Glides across the surface. };

The GameObj code:

GameObj.cpp: /*************************************************************************/ /** GameObj.cpp: A simple game object. **/ /** (C)2017 nlited systems, chip doran **/ /*************************************************************************/ #include "pch.h" #include <new> #include <stdio.h> #include <windows.h> #include <d2d1_2.h> #include <d2d1_1helper.h> #include <dwrite_1.h> #include <wrl/client.h> #include "Game.h" #pragma message(__FILE__": Optimizer disabled.") #pragma optimize("",off) using namespace DirectX; #define Zero(O) memset(&O,0,sizeof(O)) #define SafeRelease(pObj) { if(pObj) pObj->Release(); pObj= 0; } class ObjPawn: public GameObj { public: static HRESULT Create(GameObj *&pObj, Game *pGame, ID3D11DeviceContext *pDC); ~ObjPawn(void); virtual void Update(float secTotal, float secElapsed, Keyboard *pKB, Mouse *pMouse); virtual void Render(const XMMATRIX &view, const XMMATRIX &proj); protected: private: ObjPawn(Game *pGame); HRESULT Create2(ID3D11DeviceContext *pDC); //Data float Hgt; std::unique_ptr<PrimitiveBatch<VertexPositionNormalColor>> pBatch; std::unique_ptr<GeometricPrimitive> pShape; //Matrix mtrxPawn; //Matrix mtrxShadow; }; /*************************************************************************/ /** Base GameObj **/ /*************************************************************************/ GameObj::GameObj(Game *_pGame) { pGame= _pGame; } GameObj::~GameObj(void) { } HRESULT GameObj::Create(GameObj *&pObj, const char *Type, Game *pGame, ID3D11DeviceContext *pDC) { HRESULT Err= 0; if(stricmp("PAWN",Type)==0) { Err= ObjPawn::Create(pObj,pGame,pDC); } else { Err= ERROR_NOT_FOUND; } return(Err); } void GameObj::Destroy(GameObj *&pObj) { if(pObj) delete pObj; pObj= 0; } void GameObj::Update(float secTotal, float secElapsed, Keyboard *pKB, Mouse *pMouse) { } void GameObj::Render(const XMMATRIX &view, const XMMATRIX &proj) { } /*************************************************************************/ /** Pawn **/ /*************************************************************************/ ObjPawn::ObjPawn(Game *_pGame): GameObj(_pGame) { Hgt= 0.25f; } ObjPawn::~ObjPawn(void) { } HRESULT ObjPawn::Create(GameObj *&_pObj, Game *_pGame, ID3D11DeviceContext *pDC) { HRESULT Err; ObjPawn *pObj; if(!(pObj= new ObjPawn(_pGame))) { Err= Warn(ERROR_NO_MEM,"ObjPawn:Create: NoMem(%d)",sizeof(*pObj)); } else if(FAILED(Err= pObj->Create2(pDC))) { delete pObj; } else { _pObj= pObj; } return(Err); } HRESULT ObjPawn::Create2(ID3D11DeviceContext *pDC) { HRESULT Err= 0; pBatch= std::make_unique<PrimitiveBatch<VertexPositionNormalColor>>(pDC); pShape= GeometricPrimitive::CreateCylinder(pDC,Hgt,Hgt/10.0f); pos= Vector3(0,0,0); dir= vel= Vector3(0.1f,0,0.1f); return(Err); } void ObjPawn::Update(float secTotal, float secElapsed, Keyboard *pKB, Mouse *pMouse) { RECTF rArena; rArena= pGame->GetArena(); xform= Matrix::Identity; pos+= vel*secElapsed; if(pos.x < rArena.left) { pos.x= vel.x= (0.010f*(float)rand())/RAND_MAX + 0.10f; } else if(pos.x >= rArena.right) { vel.x= -((0.010f*(float)rand())/RAND_MAX + 0.10f); pos.x= rArena.right + vel.x; } if(pos.z < rArena.top) { pos.z= vel.z= (0.010f*(float)rand())/RAND_MAX + 0.10f; } else if(pos.z >= rArena.bottom) { vel.z= -((0.010f*(float)rand())/RAND_MAX + 0.10f); pos.z= rArena.bottom + vel.z; } VertexPositionNormalColor vtxPawn; //TODO: Interpolate vtxPawn with neighboring vertices. pGame->getVertex(pos.x,pos.z,vtxPawn); pos.y= vtxPawn.position.y; // + 0.125f; Vector3 ptCenter= (Hgt/2.0f)*vtxPawn.normal; ptCenter+= pos; xform.Translation(ptCenter); xform.Up(vtxPawn.normal); //Plane plane(pos,vtxPawn.normal); //mtrxShadow= mtrxPawn.CreateShadow(light,plane); } void ObjPawn::Render(const XMMATRIX &view, const XMMATRIX &proj) { // Draw reference line through pos. VertexPositionNormalColor v0(Vector3(pos.x,0,pos.z),Vector3::UnitX,Colors::Brown); VertexPositionNormalColor v1(Vector3(pos.x,10.0f,pos.z),Vector3::UnitX,Colors::White); pBatch->Begin(); pBatch->DrawLine(v0,v1); pBatch->End(); pShape->Draw(xform,view,proj); //TODO: Figure out how to draw the pawn's shadow. //objPawn->Draw(mtrxShadow,view,mtrxProj); } //EOF: GAMEOBJ.CPP

The game code:

Game.cpp: void Game::CreateDevice2(void) { ... GameObj::Create(pPawn,"PAWN",this,d3dContext.Get()); } void Game::Update2(float totalTime, float elapsedTime) { if(!doPause) { ... pPawn->Update(totalTime,elapsedTime,keyboard.get(),mouse.get()); } } void Game::RenderObjects(XMMATRIX &view) { // Reset the DC effects before rendering each object. dxEffect->Apply(d3dContext.Get()); objSun->Draw(mtrxSun,view,mtrxProj); dxEffect->Apply(d3dContext.Get()); pPawn->Render(view,mtrxProj); }

Mistake icon

Adding the reference line took longer than expected. It seemed all that was required was creating a new vertex batch processor, setting two vertices, and drawing the line. But nothing appeared. It wasn't until I happened to look up and saw the line originating from the sun that I realized what was happening. There is a note in the DirectXTK docs: "When Draw is called, it will set the states needed to render with the effect. Existing state is not save or restored. For efficiency, it simply sets the state it requires to render and assumes that any subsequent rendering will overwrite state that it needs."

I need to reset the view transform, which is part of the Effects, by re-applying the effect to the D3D device context before rendering each game object.


It is too hard to understand exactly what is happening while the pawn is zooming around. I added a single-step mode to make it easier to stop the action when something strange happens. I press SCROLL to pause, then + or - to advance or rewind one frame.

The single-stepping was very helpful. Now I know the problem is in the Y interpolation, the normal vector looks fine. The pawns moves across the tile at roughly the same altitude until it crosses the next vertex then drops down.

Direct3D simple grid  Direct3D simple grid 



WebV7 (C)2018 nlited | Rendered by tikope in 122.449ms | 3.15.237.229