Terrain?

BlitzMax Modules Forums/MiniB3D/Terrain?

Hi, just a small question.
Will the Terrain/Heightmap-stuff be implemented in MiniB3D? Or is the only way a Meshterrain?

I'll probably add a simplistic terrain system (no LOD), but anything more complex is unlikely to be added for a while.

There was a MiniB3d thread some months ago related to mesh terrains: here

fredborg posted a useful LoadTerrain function in that thread. I’ve been meaning to modify his code to incorporate some sort of LOD system (‘chunking’, perhaps), but I just don’t know if that will be enough… If I can get motivated I'll do some experiments and post my results here.

Okay, before I try anything, can somebody please explain to me why the following Blitz3D code is ten times faster than the equivalent BlitzMax + MiniB3d code when rendering a mesh? (You’ll need to provide a heightmap and a texture).

Blitz3D:

Graphics3D 800,600

SetBuffer BackBuffer()

Global Camera = CreateCamera()
PositionEntity Camera, 0, 200, -10
CameraRange Camera, 0.1, 1000.0

Local light = CreateLight(1)

Local tex = LoadTexture("tex.png")

Text 5, 5, "Loading terrain..."
Flip

Local Terrain = LoadMeshTerrain("hmap.png")

PositionEntity Terrain, 0, 0, 0
ScaleEntity Terrain, 10, 500, 10

EntityTexture Terrain, tex

Global MoveSpeed# = 5.0

Local old_ms = MilliSecs()
Local renders
Local fps

Global mxs#
Global mys#

While Not KeyHit(1)		

	Cls

	HandleInput

	RenderWorld

	renders = renders + 1

	If MilliSecs() -old_ms >= 1000
		old_ms = MilliSecs()
		fps = renders
		renders = 0
	EndIf

	Text 5, 5, "FPS: " + fps 

	Flip False

Wend

End

Function handleInput()
	
	mxs = mxs + (MouseXSpeed() / 3.0)
	mys = mys + (MouseYSpeed() / 3.0)

	RotateEntity Camera, mys, -mxs, 0

	MoveMouse GraphicsWidth() / 2, GraphicsHeight() / 2
		
	MouseXSpeed()
	MouseYSpeed()
		
	If KeyDown(200) Or KeyDown(17) Or MouseDown(2)
		MoveEntity Camera, 0.0, 0.0, MoveSpeed
	EndIf

	If KeyDown(208) Or KeyDown(31) 
		MoveEntity Camera, 0.0, 0.0, -MoveSpeed
	EndIf

	If KeyDown(32) Then MoveEntity Camera, MoveSpeed, 0.0, 0.0

	If KeyDown(30) Then MoveEntity Camera, -MoveSpeed, 0.0, 0.0
	
End Function		

;http://www.blitzbasic.com/Community/posts.php?topic=62118
Function LoadMeshTerrain(url$, parent = 0)

	Local img = LoadImage(url$)

	SetBuffer ImageBuffer(img)

	Local mesh = CreateMesh(parent)
	Local surf = CreateSurface(mesh)

	Local uscale# = 1.0 / ImageWidth(img)
	Local vscale# = 1.0 / ImageHeight(img)

	For x = 0 To ImageWidth(img) - 1
		For y = 0 To ImageHeight(img) - 1
		
			argb = ReadPixel(x, y)
			height# = (((argb Shl 16) And $FF) + ((argb Shl 8) And $FF) + (argb And $FF)) / (3.0 * 255.0)

			v = AddVertex(surf, x, height, y, x * uscale, y * vscale)
			VertexNormal surf, v, 0, 0, 0
			
		Next
	Next

	For x = 0 To ImageWidth(img) - 2
		For y = 0 To ImageHeight(img) - 2
		
			v0 = x * ImageHeight(img) + y
			v1 = v0 + 1
			v2 = v1 + ImageHeight(img)
			v3 = v0 + ImageHeight(img)
			
			t0 = AddTriangle(surf, v0, v1, v2)
			t1 = AddTriangle(surf, v0, v2, v3)

		Next
	Next

	For v = 0 To CountVertices(surf) - 1
		nx# = VertexNX(surf, v)
		ny# = VertexNY(surf, v)
		nz# = VertexNZ(surf, v)
		d# = 1.0 / Sqr(nx * nx + ny * ny + nz * nz)
		VertexNormal surf, v, nx * d, ny * d, nz * d
	Next

	UpdateNormals mesh

	SetBuffer BackBuffer()
	
	FreeImage img

	Return mesh

End Function








BlitzMax + MiniB3D:


SuperStrict

Import "c:\temp\MiniB3D-v041\minib3d.bmx"

Graphics3D 800,600

Global Camera:TCamera = CreateCamera()
PositionEntity Camera, 0, 200, -10
CameraRange Camera, 0.1, 1000.0

Local light:TLight = CreateLight(1)

Local tex:TTexture = LoadTexture("tex.png")

Text 5, 5, "Loading terrain..."
Flip

Local Terrain:TMesh = LoadTerrain("hmap.png")

PositionEntity Terrain, 0, 0, 0
ScaleEntity Terrain, 10, 500, 10

EntityTexture Terrain,tex

Global MoveSpeed:Float = 5.0

Local old_ms:Int = MilliSecs()
Local renders:Int
Local fps:Int

Global mxs:Float
Global mys:Float

While Not KeyDown(KEY_ESCAPE)		

	Cls

	HandleInput

	RenderWorld

	renders :+ 1

	If MilliSecs() -old_ms >= 1000
		old_ms = MilliSecs()
		fps = renders
		renders = 0
	EndIf

	Text 5, 5, "FPS: " + String(fps)

	Flip False

Wend

End

Function handleInput()
	
	mxs :+ (MouseXSpeed() / 3.0)
	mys :+ (MouseYSpeed() / 3.0)

	RotateEntity Camera, mys, -mxs, 0

	MoveMouse GraphicsWidth() / 2, GraphicsHeight() / 2
		
	MouseXSpeed()
	MouseYSpeed()
		
	If KeyDown(KEY_UP) Or KeyDown(KEY_W) Or MouseDown(2)
		MoveEntity Camera, 0.0, 0.0, MoveSpeed
	EndIf

	If KeyDown(KEY_DOWN) Or KeyDown(KEY_S) 
		MoveEntity Camera, 0.0, 0.0, -MoveSpeed
	EndIf

	If KeyDown(KEY_D) Then MoveEntity Camera, MoveSpeed, 0.0, 0.0

	If KeyDown(KEY_A) Then MoveEntity Camera, -MoveSpeed, 0.0, 0.0
	
EndFunction		

'http://www.blitzbasic.com/Community/posts.php?topic=62118
Function LoadTerrain:TMesh(url:Object, parent:TEntity = Null)

	Local pix:TPixmap

	If TPixmap(url)
		pix = TPixmap(url)
	Else
		pix = LoadPixmap(url)
		If pix = Null Return Null
	EndIf

	Local mesh:TMesh = CreateMesh(parent)
	Local surf:TSurface = CreateSurface(mesh, Null)

	Local uscale:Float = 1.0 / pix.width
	Local vscale:Float = 1.0 / pix.height

	For Local x:Int = 0 To pix.width - 1
		For Local y:Int = 0 To pix.height - 1
		
			Local argb:Int = pix.ReadPixel(x, y)
			Local height:Float = (((argb Shl 16) & $FF) + ((argb Shl 8) & $FF) + (argb & $FF)) / (3.0 * 255.0)

			Local v:Int = AddVertex(surf, x, height, y, x * uscale, y * vscale)
			VertexNormal surf, v, 0, 0, 0
			
		Next
	Next

	For Local x:Int = 0 To pix.width - 2
		For Local y:Int = 0 To pix.height - 2
		
			Local v0:Int = x * pix.height + y
			Local v1:Int = v0 + 1
			Local v2:Int = v1 + pix.height
			Local v3:Int = v0 + pix.height
			
			Local t0:Int = AddTriangle(surf, v0, v1, v2)
			Local t1:Int = AddTriangle(surf, v0, v2, v3)

		Next
	Next

	For Local v:Int = 0 Until CountVertices(surf)
		Local nx:Float = surf.VertexNX(v)
		Local ny:Float = surf.VertexNY(v)
		Local nz:Float = surf.VertexNZ(v)
		Local d:Float = 1.0 / Sqr(nx * nx + ny * ny + nz * nz)
		VertexNormal surf, v, nx * d, ny * d, nz * d
	Next

	UpdateNormals mesh

	Return mesh

End Function





128x128 heightmap gives me:

* Blitz3D: 700 fps approx
* BlitzMax + MiniB3D: 75 fps approx

System Specs: 1.6Ghz Centrino, 512MB ram, 128MB ATI Mobility Radeon X700

Debug and Release display the same speed difference.

Is it a DirectX vs OpenGL issue? MiniB3D’s architecture cannot handle large meshes as efficiently as Blitz3D? Am I doing something stupid?

Probably a vertex buffer thing. MiniB3D doesn't yet use these, B3D does, and Kelpto's version does.

yeah:
128 * 128 = 16 384

that is alot of vertices/polys to render from main mem i guess.
when klepto releases a new version (after easter break) we'll try a VBO version.

as for Terrain LOD algorithms, I think Blitz3D's is based on ROAM ? (i remember that from 4-5 years ago so my memory might be wrong)

http://www.cognigraph.com/ROAM_homepage/

Ah well, I suppose I need to wait then. I did try an earlier version of klepto2's extended version but it kept crashing when loading larger heightmaps (>=256x256), so I suppose some sort of 'chunking' system will be required in any case...

Thanks for the link bradford6.

Actually, I might be able to code a terrain chunking system in MiniB3d as it is now. It handles meshes generated from 64x64 heightmaps without problems. (Not as fast as Blitz3d, but close : both are > 800 fps)... We'll see...

Here are the results of my 'chunking' experiment:


Rem

MiniB3d Chunked Terrain Mesh Test
(Proof of concept / performance test)

Notes:

	* Faster than non-chunked large terrain meshes, but probably not fast enough 
	  for any practical purposes. 512 x 512 heightmaps will reduce minib3d to a crawl.
	
	* The heightmap must have equal dimensions that are power of two (eg 128x128, 256x256, etc).

	* KEYS: WASD, Arrows.
	
	* MOUSE: Right Button
		
EndRem

SuperStrict

Import "c:\temp\minib3d\minib3d.bmx"			' <- Change this to your minib3d path / module

Graphics3D 800, 600

Text 5, 5, "Generating sample height map..."
Flip

createSampleHeightMap("temp_hmap.png", 128, 128, 1000, 32, False)	 

Text 5, 25, "Setting up world and loading terrain..."
Flip

AmbientLight 255, 255, 255

Global Camera:TCamera = CreateCamera()
CameraRange Camera, 0.1, 4000.0
CameraClsColor Camera, 64, 164, 196
	
'**** Terrain Stuff
Local Terrain:TChunkedTerrain = New TChunkedTerrain
Terrain.Load("temp_hmap.png")
Terrain.TextureFromFile("temp_hmap.png", 1) 		' <- Using the heightmap image for the texture also.
Terrain.Scale 40.0, 1000.0, 40.0
Terrain.Position 0.0, 0.0, 0.0
'**** 

PositionEntity Camera, Terrain.X, 1000.0, Terrain.Z

Global MoveSpeed:Float = 2.5

Local old_ms:Int = MilliSecs()
Local renders:Int
Local fps:Int

Global mxs:Float
Global mys:Float

'WireFrame True

While Not KeyDown(KEY_ESCAPE)		

	Cls

	HandleInput
	UpdateWorld
	RenderWorld

	renders :+ 1

	If MilliSecs() -old_ms >= 1000
		old_ms = MilliSecs()
		fps = renders
		renders = 0
	EndIf

	Text 5, 5, "FPS: " + String(fps)
	text 5, 25, EntityX(Camera) + ", " + EntityZ(Camera)

	Flip False
	
	'Delay 1

Wend

FreeEntity Camera
Camera = Null

Terrain.Destroy
Terrain = Null

GCCollect

End


'********** CHUNKED TERRAIN CLASS **********


Type TChunkedTerrain


	Field Size:Int			
	Field ChunkSize:Int
	Field Cols:Int
	Field Rows:Int
	
	Field Pivot:TPivot
	Field Chunks:TMesh[,]
	
	Field X:Float
	Field Y:Float
	Field Z:Float
	
	Field XScale:Float
	Field YScale:Float
	Field ZScale:Float
	
	
	Method New()
	
		X = 0.0
		Y = 0.0
		Z = 0.0
				
		XScale = 1.0
		YScale = 1.0
		ZScale = 1.0
	
	EndMethod
	
	
	Method Destroy()
	
		For Local col:Int = 0 To Cols - 1
			For Local row:Int = 0 To Rows - 1
				FreeEntity Chunks[col, row]
				Chunks[col, row] = Null
			Next
		Next	
	
		FreeEntity Pivot
		
		Pivot = Null
	
		Chunks = Null
		
		GCCollect		
	
	EndMethod
	
	
	'Chunk Size should be less than the heightmap size and a power of two (eg, 16, 32, 64, etc)
	
	Method Load:Int(hmap_url:String, chunk_size:Int = 64)
	
		Local pix:TPixmap = LoadPixmap(hmap_url)
		If Not pix Return False
		
		Size = pix.height
		ChunkSize = chunk_size
		
		Cols = (Size / ChunkSize)
		Rows = (Size / ChunkSize)

		Pivot = CreatePivot()
		
		PositionEntity Pivot, X, Y, Z, True
	
		Chunks = New TMesh[Cols, Rows]
		
		For Local col:Int = 0 To Cols - 1
			For Local row:Int = 0 To Rows - 1
		
				Local px:Int = (col * ChunkSize) - col
				Local pz:Int = (row * ChunkSize) - row
		
				Local hmappm:TPixmap = PixmapWindow(pix, px, pz, ChunkSize + 1, ChunkSize + 1)
				Chunks[col, row] = CreateChunk(hmappm, Pivot)
		
				Local wx:Float = Float(px)
				Local wz:Float = Float(((rows - 1) * chunk_size) - pz)
				PositionEntity Chunks[col, row], wx, 0.0, wz, False

			Next
		Next

		Scale XScale, YScale, ZScale
		
		Return True

	EndMethod


	Method TextureFromFile:Int(tex_url:String = "", flags:Int = 1, frame:Int = 0, index:Int = 0)

		If Not pivot Return False
	
		Local tex:TPixmap = LoadPixmap(tex_url)
		If Not tex Then Return False

		tex = ResizePixmap(tex, Size, Size)

		For Local col:Int = 0 To Cols - 1
			For Local row:Int = 0 To Rows - 1
		
				Local px:Int = (col * ChunkSize) - col
				Local pz:Int = (row * ChunkSize) - row
		
				Local fname:String = "temp_chunk_tex_" + col + "_" + row + ".png"
				Local pm:TPixmap = PixmapWindow(tex, px, pz, ChunkSize + 1, ChunkSize + 1)
				SavePixmapPNG(pm, fname, 9)
				
				Local tex_chunk:TTexture = LoadTexture(fname, flags)
				ScaleTexture tex_chunk, pm.width, pm.height
				EntityTexture chunks[col, row], tex_chunk, frame, index
				
				DeleteFile fname
				
			Next
		Next
		
		Return True

	EndMethod
	

	Method Scale(sx:Float, sy:Float, sz:Float)
	
		If Not Pivot Return
	
		XScale = sx
		YScale = sy
		ZScale = sz
	
		ScaleEntity Pivot, XScale, YScale, ZScale
	
	EndMethod
	
	
	Method Position(wx:Float, wy:Float, wz:Float)
	
		If Not Pivot Return
	
		X = wx
		Y = wy
		Z = wz
		
		PositionEntity Pivot, X, Y, Z		
	
	EndMethod
	
	
	Function CreateChunk:TMesh(pix:TPixmap, parent:TEntity = Null)
	Rem
		* Ported And slightly modified from Blitz3D code:
  		  <a href="http://www.blitzbasic.com/codearcs/codearcs.php?code=1609" target="_blank">http://www.blitzbasic.com/codearcs/codearcs.php?code=1609</a>
	EndRem

		If Not pix Return Null

		Local pixw:Int = PixmapWidth(pix)
		Local pixh:Int = PixmapHeight(pix)

		Local mesh:TMesh = CreateMesh(parent)
		Local surf:TSurface = CreateSurface(mesh)

		For Local ly:Int = 0 To pixh
			For Local lx:Int = 0 To pixw
				AddVertex surf, lx, 0, ly, (1.0 / lx), (1.0 / ly)
			Next
		Next
	
		For Local ly:Int = 1 To (pixw - 2)
			For Local lx:Int = 1 To (pixh - 2)
				AddTriangle surf, lx + ((pixw + 1) * ly), lx + ((pixw + 1) * ly) + (pixw + 1), (lx + 1) + ((pixw + 1) * ly)
				AddTriangle surf, (lx + 1) + ((pixw + 1) * ly), lx + ((pixw + 1) * ly) + (pixw + 1), (lx + 1)+((pixw + 1) * ly) + (pixw + 1)
			Next
		Next

		For Local lx:Int = 0 To (pixw - 1)
			For Local ly:Int = 0 To (pixh - 1)
				Local r:Byte = ReadPixel(pix, lx, pixh - 1 - ly)
				Local index:Int = lx + ((pixw + 1) * ly)
				VertexCoords surf, index, VertexX(surf, index), Float(r) / 255.0, VertexZ(surf, index)
				VertexTexCoords surf, index, lx, -ly 
			Next
		Next

		UpdateNormals mesh
		
		Return Mesh
	
	End Function
	
EndType


'******************************




'****** MISCELLANEOUS FUNCTIONS (NOT required for Chunked Terrain class) *********


Function handleInput()
	
	mxs :+ (MouseXSpeed() / 3.0)
	mys :+ (MouseYSpeed() / 3.0)

	RotateEntity Camera, mys, -mxs, 0

	MoveMouse GraphicsWidth() / 2, GraphicsHeight() / 2
		
	MouseXSpeed()
	MouseYSpeed()
		
	If KeyDown(KEY_UP) Or KeyDown(KEY_W) Or MouseDown(2)
		MoveEntity Camera, 0.0, 0.0, MoveSpeed
	EndIf

	If KeyDown(KEY_DOWN) Or KeyDown(KEY_S) 
		MoveEntity Camera, 0.0, 0.0, -MoveSpeed
	EndIf

	If KeyDown(KEY_D) Then MoveEntity Camera, MoveSpeed, 0.0, 0.0

	If KeyDown(KEY_A) Then MoveEntity Camera, -MoveSpeed, 0.0, 0.0
	
EndFunction	


Function createSampleHeightMap(fname:String, width:Int = 128, height:Int = 128, numhills:Int = 1000, maxhsize:Int = 32, island:Int = True)

	'Generate

	Local Map:Float[width, height]

    SeedRnd(MilliSecs())
  
	If island
		numhills :* 0.2
		maxhsize :* 0.5
	EndIf

    For Local n:Int = 1 To numhills

		Local radius:Int = Rand(0, maxhsize)
		Local centrex:Int = Rand(0, Width - 1)
		Local centrez:Int = Rand(0, Height - 1)
		
		If island
			Local theta:Int = Rand(0, 359)
			Local distx:Int = Rand(0, (Width * 0.5) - radius)
			Local distz:Int = Rand(0, (Height * 0.5) - radius)
	  		centrex = (Width * 0.5) + (Cos(theta) * distx)
	  		centrez = (Height * 0.5) + (Sin(theta) * distz)
		EndIf

    	Local startx:Int = centrex - radius
      	If startx < 0 startx = 0

      	Local endx:Int = centrex + radius
	  	If endx > Width endx = Width

      	Local startz:Int = centrez - radius
      	If startz < 0 startz = 0

      	Local endz:Int = centrez + radius
      	If endz > Height endz = Height

      	For Local x:Int = startx To (endx - 1)
       		For Local z:Int = startz To (endz - 1)
	   			Local y:Float = (radius * radius) - (((x - centrex) * (x - centrex)) + ((z - centrez) * (z - centrez)))
	    		If y > 0.0 Map[x, z] :+ y
	    	Next
	  	Next
    
    Next

	'Normalise
	
	Local minv:Float = 10^38, maxv:Float = 10^-38
  
  	For Local x:Int = 0 To (Width - 1)
    	For Local z:Int = 0 To (Height - 1)
 	 		If Map[x, z] < minv minv = Map[x, z] Else If Map[x, z] > maxv maxv = Map[x, z]
     	Next
   	Next

   	For Local x:Int = 0 To (Width - 1)
     	For Local z:Int = 0 To (Height - 1)
			Map[x, z] = (Map[x, z] - minv) / (maxv - minv)
		Next
  	Next

	'Convert to byte Array
	
	Local arr:Byte[,] = New Byte[width, height]

  	For Local x:Int = 0 To (Width - 1)
    	For Local z:Int = 0 To (Height - 1)

			Local v:Int = Floor(Map[x, z] * 255)

			arr[x, z] = Byte(v)

		Next
	Next
		
	'Render to pixmap and save to file
	
	Local pm:TPixmap = CreatePixmap(width, height, PF_RGB888)

    For Local x:Int = 0 To (Width - 1)
    	For Local z:Int = 0 To (Height - 1)
    		WritePixel(pm, x, z, arr[x, z] Shl 16 | arr[x, z] Shl 8 | arr[x, z])
    	Next
    Next

	SavePixmapPNG(pm, fname, 9)	
	
End Function




Nice piece of code impixi :)

I have modified it a bit to get it work with VBOs and thats my results:

Without VBOs: ~350 FPS
With VBO: ~950FPS

If you agree I will add this to my next release, as it is really clean written and it seems very extendable.

Without VBOs: ~350 FPS
With VBO: ~950FPS

Heh, those VBOs seem like useful things! :)

If you agree I will add this to my next release,

Sure, go for it.

Probably the most significant missing items are:

* TerrainY() method - for use in entity placement.
* PreLoadTexture method – for use in real-time texture changes.
* Collision-related methods.

I have also tried to implement some frustum culling, which pushes the fps again up to 1000 fps with VBOs and a tarrain map of 512*512.

If you have my module, you could test the VBOs yourself:

Change your createChunk method abit:
UpdateNormals mesh
mesh.UpdateBuffer()		
		Return Mesh


And it should compile.