[传奇技术]【HGE】绘图底层
HGE是基于DX8.0的二维游戏引擎,多年没有更新了。 而我们知道Dx8.0跟DX9.0C是不同层次的。其实基本绘图逻辑差别不是太大,只是性能方面肯定不在一个水平上面。 让我感觉很大困惑的是,HGE的绘图结构效率到底适不适合即时大型网络游戏渲染?因为它的绘图逻辑是基于以前的DX7.0的绘图思想。 先分析它的架构: - 1 (*
- 2 ** HGE Primitive type constants
- 3 *)
- 4 const
- 5 HGEPRIM_LINES = 2;
- 6 HGEPRIM_TRIPLES = 3;
- 7 HGEPRIM_QUADS = 4;
- 8
- 9 (*
- 10 ** HGE Vertex structure
- 11 *)
- 12 type
- 13 THGEVertex = record
- 14 X, Y: Single; // screen position
- 15 Z: Single; // Z-buffer depth 0..1
- 16 Col: Longword; // color
- 17 TX, TY: Single; // texture coordinates
- 18 end;
- 19 PHGEVertex = ^THGEVertex;
- 20 THGEVertexArray = array [0..MaxInt div 32 - 1] of THGEVertex;
- 21 PHGEVertexArray = ^THGEVertexArray;
- 22 TCustomVertex = packed record
- 23 x, y, z: single; // Position
- 24 rhw: single; // Reciprocal of homogeneous w
- 25 Col: Longword; // Vertex Color
- 26 tu, tv: single; // Texture coordinates
- 27 end;
- 28 PCustomVertex = ^TCustomVertex;
- 29
- 30 (*
- 31 ** HGE Triple structure三角形结构
- 32 *)
- 33 type
- 34 THGETriple = record
- 35 V: array [0..2] of THGEVertex;
- 36 Tex: ITexture;
- 37 Blend: Integer;
- 38 end;
- 39 PHGETriple = ^THGETriple;
- 40
- 41 (*
- 42 ** HGE Quad structure四边形结构
- 43 *)
- 44 type
- 45 THGEQuad = record
- 46 V: array [0..3] of THGEVertex;
- 47 Tex: ITexture;
- 48 Blend: Integer;
- 49 end;
- 50 PHGEQuad = ^THGEQuad;
复制代码FVF常量定义: 1 const2 D3DFVF_HGEVERTEX = D3DFVF_XYZ or D3DFVF_DIFFUSE or D3DFVF_TEX1;3 VertexDef = D3DFVF_XYZRHW or D3DFVF_DIFFUSE or D3DFVF_TEX1;4 VERTEX_BUFFER_SIZE = 4000; //静态缓冲区的大小基本参数
上面这个过程在D3D编程里面,大家应该很熟悉了,定义顶点结构、定义FVF常量结构。
接着应该是创建顶点缓冲和索引缓冲,HGE定义的是静态缓冲,也就是说缓冲区是在显示卡内存里面的。 继续看它创建缓冲区的代码: 其实这部分是非常关键和重要的,直接影响到引擎的性能。 - 1 function THGEImpl.InitLost: Boolean; //接口的子类实现部分
- 2 var
- 3 Target: IInternalTarget;
- 4 PIndices: PWord;
- 5 N: Word;
- 6 I: Integer;
- 7 begin
- 8 Result := False;
- 9
- 10 // Store render target
- 11
- 12 FScreenSurf := nil;
- 13 FScreenDepth := nil;
- 14
- 15 {$IFDEF HGE_DX8}
- 16 FD3DDevice.GetRenderTarget(FScreenSurf);
- 17 {$ELSE}
- 18 FD3DDevice.GetRenderTarget(0,FScreenSurf);
- 19 {$ENDIF}
- 20 FD3DDevice.GetDepthStencilSurface(FScreenDepth);
- 21
- 22 for I := 0 to FTargets.Count - 1 do begin
- 23 Target := IInternalTarget(FTargets[I]);
- 24 Target.Lost;
- 25 end;
- 26
- 27 // Create Vertex buffer
- 28 {$IFDEF HGE_DX8}
- 29 if (Failed(FD3DDevice.CreateVertexBuffer(VERTEX_BUFFER_SIZE * SizeOf(THGEVertex),
- 30 D3DUSAGE_WRITEONLY,D3DFVF_HGEVERTEX,D3DPOOL_DEFAULT,FVB)))
- 31 {$ELSE}//这些是DX9部分
- 32 if (Failed(FD3DDevice.CreateVertexBuffer(VERTEX_BUFFER_SIZE * SizeOf(THGEVertex),
- 33 D3DUSAGE_WRITEONLY,D3DFVF_HGEVERTEX,D3DPOOL_DEFAULT,FVB,nil)))
- 34
- 35 //D3DUSAGE_WRITEONLY指定应用程序只能写缓存。它允许驱动程序分配最适合的内存地址作为写缓存。注意如果从创建好的这种缓存中读数据,将会返回错误信息。
- 36
- 37
- 38 {$ENDIF}
- 39 then begin
- 40 PostError('Can''t create D3D vertex buffer');
- 41 Exit;
- 42 end;
- 43
- 44 {$IFDEF HGE_DX8}
- 45 FD3DDevice.SetVertexShader(D3DFVF_HGEVERTEX);
- 46 FD3DDevice.SetStreamSource(0,FVB,SizeOf(THGEVertex));
- 47 {$ELSE}//这些是DX9部分
- 48 FD3DDevice.SetVertexShader(nil);
- 49 FD3DDevice.SetFVF(D3DFVF_HGEVERTEX);
- 50 FD3DDevice.SetStreamSource(0,FVB,0,SizeOf(THGEVertex));
- 51 {$ENDIF}
- 52
- 53 // Create and setup Index buffer
- 54
- 55 {$IFDEF HGE_DX8}
- 56 if (Failed(FD3DDevice.CreateIndexBuffer(VERTEX_BUFFER_SIZE * 6 div 4 * SizeOf(Word),
- 57 D3DUSAGE_WRITEONLY,D3DFMT_INDEX16,D3DPOOL_DEFAULT,FIB)))
- 58 {$ELSE}//这些是DX9部分
- 59 if (Failed(FD3DDevice.CreateIndexBuffer(VERTEX_BUFFER_SIZE * 6 div 4 * SizeOf(Word),
- 60 D3DUSAGE_WRITEONLY,D3DFMT_INDEX16,D3DPOOL_DEFAULT,FIB,nil)))
- 61 {$ENDIF}
- 62 then begin
- 63 PostError('Can''t create D3D index buffer');
- 64 Exit;
- 65 end;
- 66
- 67 N := 0;
- 68 {$IFDEF HGE_DX8}
- 69 if (Failed(FIB.Lock(0,0,PByte(PIndices),0))) then
- 70 {$ELSE}//这些是DX9部分
- 71 if (Failed(FIB.Lock(0,0,Pointer(PIndices),0))) then
- 72 {$ENDIF}
- 73 begin
- 74 PostError('Can''t lock D3D index buffer');
- 75 Exit;
- 76 end;
- 77
- 78 for I := 0 to (VERTEX_BUFFER_SIZE div 4) - 1 do begin
- 79 PIndices^ := N ; Inc(PIndices);
- 80 PIndices^ := N+1; Inc(PIndices);
- 81 PIndices^ := N+2; Inc(PIndices);
- 82 PIndices^ := N+2; Inc(PIndices);
- 83 PIndices^ := N+3; Inc(PIndices);
- 84 PIndices^ := N; Inc(PIndices);
- 85 Inc(N,4);
- 86 end;
- 87
- 88 FIB.Unlock;
- 89 {$IFDEF HGE_DX8}
- 90 FD3DDevice.SetIndices(FIB,0);
- 91 {$ELSE}//这些是DX9部分
- 92 FD3DDevice.SetIndices(FIB);
- 93 {$ENDIF}
- 94
- 95 // Set common render states
- 96
- 97 //pD3DDevice->SetRenderState( D3DRS_LASTPIXEL, FALSE ); ignore this
- 98 FD3DDevice.SetRenderState(D3DRS_CULLMODE,D3DCULL_NONE);
- 99 FD3DDevice.SetRenderState(D3DRS_LIGHTING,0);
- 100
- 101 FD3DDevice.SetRenderState(D3DRS_ALPHABLENDENABLE,1);
- 102 FD3DDevice.SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
- 103 FD3DDevice.SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);
- 104
- 105 FD3DDevice.SetRenderState(D3DRS_ALPHATESTENABLE,1);
- 106 FD3DDevice.SetRenderState(D3DRS_ALPHAREF,1);
- 107 FD3DDevice.SetRenderState(D3DRS_ALPHAFUNC,D3DCMP_GREATEREQUAL);
- 108
- 109 FD3DDevice.SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_MODULATE);
- 110 FD3DDevice.SetTextureStageState(0,D3DTSS_COLORARG1,D3DTA_TEXTURE);
- 111 FD3DDevice.SetTextureStageState(0,D3DTSS_COLORARG2,D3DTA_DIFFUSE);
- 112
- 113 FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAOP, D3DTOP_MODULATE);
- 114 FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
- 115 FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAARG2,D3DTA_DIFFUSE);
- 116
- 117 {$IFDEF HGE_DX8}
- 118 FD3DDevice.SetTextureStageState(0,D3DTSS_MIPFILTER, D3DTEXF_POINT);
- 119 {$ELSE}//这些是DX9部分
- 120 FD3DDevice.SetSamplerState(0,D3DSAMP_MIPFILTER, D3DTEXF_POINT);
- 121 {$ENDIF}
- 122
- 123 if (FTextureFilter) then begin
- 124 {$IFDEF HGE_DX8}
- 125 FD3DDevice.SetTextureStageState(0,D3DTSS_MAGFILTER,D3DTEXF_LINEAR);
- 126 FD3DDevice.SetTextureStageState(0,D3DTSS_MINFILTER,D3DTEXF_LINEAR);
- 127 {$ELSE}//这些是DX9部分
- 128 FD3DDevice.SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR);
- 129 FD3DDevice.SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);
- 130 {$ENDIF}
- 131 end else begin
- 132 {$IFDEF HGE_DX8}
- 133 FD3DDevice.SetTextureStageState(0,D3DTSS_MAGFILTER,D3DTEXF_POINT);
- 134 FD3DDevice.SetTextureStageState(0,D3DTSS_MINFILTER,D3DTEXF_POINT);
- 135 {$ELSE}//这些是DX9部分
- 136 FD3DDevice.SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_POINT);
- 137 FD3DDevice.SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_POINT);
- 138 {$ENDIF}
- 139 end;
- 140
- 141 FPrim := 0;
- 142 FCurPrimType := HGEPRIM_QUADS;
- 143 FCurBlendMode := BLEND_DEFAULT;
- 144 FCurTexture := nil;
- 145
- 146 FD3DDevice.SetTransform(D3DTS_VIEW,FMatView);
- 147 FD3DDevice.SetTransform(D3DTS_PROJECTION,FMatProj);
- 148
- 149 Result := True;
- 150 end;
复制代码这份代码是HGE包含DX9的,其实很不好,DX9跟DX8在一些方面是不兼容。 显然顶点缓冲区是只写属性,因为最后是从索引缓冲里面读取数据。 这个顶点缓冲的大小是固定的,很难说够不够用。如果不够用怎么办?还没有看到这部分的代码在那里。 或者说,根本没有完全使用完整个顶点缓冲区的容量。 下面再看看他的代码: - 1 function THGEImpl.Gfx_BeginScene(const Target: ITarget): Boolean;
- 2 var
- 3 {$IFDEF HGE_DX8}
- 4 Surf, Depth: IDirect3DSurface8;
- 5 {$ELSE}
- 6 Surf, Depth: IDirect3DSurface9;
- 7 {$ENDIF}
- 8 HR: HResult;
- 9 begin
- 10 Result := False;
- 11
- 12 HR := FD3DDevice.TestCooperativeLevel;
- 13 if (HR = D3DERR_DEVICELOST) then
- 14 Exit;
- 15 if (HR = D3DERR_DEVICENOTRESET) then
- 16 if (not GfxRestore) then
- 17 Exit;
- 18
- 19 if Assigned(FVertArray) then begin
- 20 PostError('Gfx_BeginScene: Scene is already being rendered');
- 21 Exit;
- 22 end;
- 23
- 24 if (Target <> FCurTarget) then begin
- 25 if Assigned(Target) then begin
- 26 Target.Tex.Handle.GetSurfaceLevel(0,Surf);
- 27 Depth := (Target as IInternalTarget).Depth;
- 28 end else begin
- 29 Surf := FScreenSurf;
- 30 Depth := FScreenDepth;
- 31 end;
- 32
- 33 {$IFDEF HGE_DX8}
- 34 if (Failed(FD3DDevice.SetRenderTarget(Surf,Depth)))
- 35 {$ELSE}
- 36 if (Failed(FD3DDevice.SetRenderTarget(0,Surf)))
- 37 {$ENDIF}
- 38 then begin
- 39 PostError('Gfx_BeginScene: Can''t set render target');
- 40 Exit;
- 41 end;
- 42 if Assigned(Target) then begin
- 43 Surf := nil;
- 44 if Assigned((Target as IInternalTarget).Depth) then
- 45 FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE)
- 46 else
- 47 FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_FALSE);
- 48 SetProjectionMatrix(Target.Width,Target.Height);
- 49 end else begin
- 50 if (FZBuffer) then
- 51 FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE)
- 52 else
- 53 FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_FALSE);
- 54 SetProjectionMatrix(FScreenWidth,FScreenHeight);
- 55 end;
- 56
- 57 FD3DDevice.SetTransform(D3DTS_PROJECTION,FMatProj);
- 58 D3DXMatrixIdentity(FMatView);
- 59 FD3DDevice.SetTransform(D3DTS_VIEW,FMatView);
- 60
- 61 FCurTarget := Target;
- 62 end;
- 63 FD3DDevice.BeginScene;
- 64 {$IFDEF HGE_DX8}
- 65 FVB.Lock(0,0,PByte(FVertArray),0);
- 66 {$ELSE}
- 67 FVB.Lock(0,0,Pointer(FVertArray),0);
- 68 {$ENDIF}
- 69 Result := True;
- 70 end;
复制代码
应该看到了,这个是HGE每一帧里面需要调用的开始渲染函数。
在每一帧开始的时候就锁定顶点缓冲,然后把数据拷贝进缓冲区里面。 那么我们了解到的情况是:经常对静态缓冲加解锁是不明智的,因为只有等驱动完成了所有挂起的命令之后才能返回该缓冲的指针。如果经常这样做,这会导致CPU和GPU很多不必要的同步,这样性能将会变得很差。 何况是每一帧开始之后才填充数据,这种方式跟以前的DDRAW7的绘图模式完全是一样的。 - procedure THGEImpl.Gfx_EndScene;
- begin
- RenderBatch(True);
- FD3DDevice.EndScene;
- if (FCurTarget = nil) then
- FD3DDevice.Present(nil,nil,0,nil);
- end;
复制代码结束渲染之前调用了一次RenderBatch函数,并且传入参数为True。 看看这个函数的功能: - procedure THGEImpl.RenderBatch(const EndScene: Boolean);
- begin
- if Assigned(FVertArray) then begin
- FVB.Unlock;
- if (FPrim <> 0) then begin
- case FCurPrimType of
- HGEPRIM_QUADS:
- {$IFDEF HGE_DX8}
- FD3DDevice.DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,FPrim shl 2,0,FPrim shl 1);
- {$ELSE}
- FD3DDevice.DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,FPrim shl 2,0,FPrim shl 1);
- {$ENDIF}
- HGEPRIM_TRIPLES:
- FD3DDevice.DrawPrimitive(D3DPT_TRIANGLELIST,0,FPrim);
- HGEPRIM_LINES:
- FD3DDevice.DrawPrimitive(D3DPT_LINELIST,0,FPrim);
- end;
- FPrim := 0; //绘制完毕,清零了,好像不用累计
- end;
- if (EndScene) then //结束渲染之前执行
- FVertArray := nil
- else
- {$IFDEF HGE_DX8}
- FVB.Lock(0,0,PByte(FVertArray),0);
- {$ELSE}
- FVB.Lock(0,0,Pointer(FVertArray),0); //常规的做法,我们是使用一个无类型指针,这里他使用的是FVertArray这样的指针
- {$ENDIF}
- end;
- end;
复制代码 1 FVertArray: PHGEVertexArray;这个函数是创建精灵的时候,需要调用的函数,也是其它单元经常调用的。 - 1 procedure THGEImpl.Gfx_RenderQuad(const Quad: THGEQuad);
- 2 begin
- 3 if Assigned(FVertArray) then begin
- 4 if (FCurPrimType <> HGEPRIM_QUADS)
- 5 or (FPrim >= VERTEX_BUFFER_SIZE div HGEPRIM_QUADS)
- 6 or (FCurTexture <> Quad.Tex)
- 7 or (FCurBlendMode <> Quad.Blend)
- 8 then begin
- 9 RenderBatch;
- 10 FCurPrimType := HGEPRIM_QUADS;
- 11 if (FCurBlendMode <> Quad.Blend) then
- 12 SetBlendMode(Quad.Blend);
- 13 if (Quad.Tex <> FCurTexture) then begin
- 14 if Assigned(Quad.Tex) then
- 15 FD3DDevice.SetTexture(0,Quad.Tex.Handle)
- 16 else
- 17 FD3DDevice.SetTexture(0,nil);
- 18 FCurTexture := Quad.Tex;
- 19 end;
- 20 end;
- 21
- 22 Move(Quad.V,FVertArray[FPrim * HGEPRIM_QUADS],
- 23 SizeOf(THGEVertex) * HGEPRIM_QUADS);
- 24 Inc(FPrim);
- 25 end;
- 26 end;
复制代码这个函数的调用刚好处于开始渲染和结束渲染之间。Move把Quad的数据复制到顶点缓冲里面。要知道Quad结构的数据在被调用之前就已经填充好了。然后再复制入缓冲区里面。 就是说,从渲染开始到渲染结束,都是处于Lock锁定的状态下。所以很难看出HGE的绘图高效在那里。显然这样的渲染方式不适合大量绘制图元,更不用说应用于大型或者超大型网络游戏里面了。 因为锁定状态是按照默认锁定的,就是在GPU绘图这段时间里面,CPU就一直在等待之中,而且系统就处于挂起状态,如果是绘制大量图元呢,结果是可想而知的。 按照我们常规的逻辑,当引擎渲染开始之后,最理想的状态是,这个时候不必再去计算和处理各种数据或者是再锁定缓冲区去修改里面的数据,而是按照渲染序列,一次性批量地进行渲染。 说真的,我看了很久,看不出HGE能够胜任大型网络游戏的优点在那里。怎么看,都好像适合以前那些小型游戏开发。 网上的资料都是研究怎么去学习,还没有看到有人去研究它的实现代码结构方面。希望了解这个引擎的人说下。
好像也不对,看一段代码: - 1 hge->Gfx_BeginScene(); //开始渲染,LOCK锁定缓冲区
- 2 bgspr->Render(0,0); //第一次调用 FHGE.Gfx_RenderQuad(FQuad);
- 3
- 4 for(i=0;i<nObjects;i++)
- 5 {
- 6 pObjects[i].x+=pObjects[i].dx*dt;
- 7 if(pObjects[i].x>SCREEN_WIDTH || pObjects[i].x<0) pObjects[i].dx=-pObjects[i].dx;
- 8 pObjects[i].y+=pObjects[i].dy*dt;
- 9 if(pObjects[i].y>SCREEN_HEIGHT || pObjects[i].y<0) pObjects[i].dy=-pObjects[i].dy;
- 10 pObjects[i].scale+=pObjects[i].dscale*dt;
- 11 if(pObjects[i].scale>2 || pObjects[i].scale<0.5) pObjects[i].dscale=-pObjects[i].dscale;
- 12 pObjects[i].rot+=pObjects[i].drot*dt;
- 13
- 14 spr->SetColor(pObjects[i].color);
- //循环调用 FHGE.Gfx_RenderQuad(FQuad);
- 15 spr->RenderEx(pObjects[i].x, pObjects[i].y, pObjects[i].rot, pObjects[i].scale);
- 16 }
- 17
- 18 fnt->printf(7,7,"UP and DOWN to adjust number of hares: %d\nSPACE to change blending mode: %d\nFPS: %d", nObjects, nBlend, hge->Timer_GetFPS());
- 19 hge->Gfx_EndScene(); //解锁缓冲区,结束渲染
复制代码- int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) //程序入口
- ``````````````
- fnt=new hgeFont("font2.fnt");
- spr=new hgeSprite(tex,0,0,64,64);
- spr->SetHotSpot(32,32); //在这里生成一个矩形
- ------------------------------------------
- procedure THGESprite.Render(const X, Y: Single);
- var
- TempX1, TempY1, TempX2, TempY2: Single;
- begin
- TempX1 := X - FHotX;
- TempY1 := Y - FHotY;
- TempX2 := X + FWidth - FHotX;
- TempY2 := Y + FHeight - FHotY;
- FQuad.V[0].X := TempX1; FQuad.V[0].Y := TempY1;
- FQuad.V[1].X := TempX2; FQuad.V[1].Y := TempY1;
- FQuad.V[2].X := TempX2; FQuad.V[2].Y := TempY2;
- FQuad.V[3].X := TempX1; FQuad.V[3].Y := TempY2;
- FHGE.Gfx_RenderQuad(FQuad);
- end;
- procedure THGESprite.SetHotSpot(const X, Y: Single);
- begin
- FHotX := X;
- FHotY := Y;
- end;
复制代码显然在锁定缓冲区的同时,进行各种运算生成数据,然后批量地进行绘制图元。从流程可以看出来,第一次生成——就是说初次复制到缓冲区的数据是不被立即渲染,而是在第二次生成数据并且调用Gfx_RenderQuad这个函数的时候,才被渲染。也就是上一次的数据放到下一次进行渲染,这样形成了一个延迟渲染的流程。显然这些针对的是批量渲染算法。
显然它只需要锁定一次缓冲区,就可以批量地绘制大量的图元,同时它不需要等到把所有的数据填充入缓冲区之后,再批量地绘制。好像比较适合数据量大的情况,当然这些对于绘制二维图元来说,应该说是足够了。 解决方案在另一个帖子里面。
|