Volumetric Model to Isometric Sprite

BlitzMax Forums/BlitzMax Programming/Volumetric Model to Isometric Sprite

zoqfotpik(Posted 2014) [#1]
Here's a program I wrote back in February that creates 3/4 perspective isometric sprites from 32x32x32 volumetric images.

It does this by rendering 32 horizontal slices of the volumetric model with Y scaled by half. Then it renders the next slice up, but a pixel higher vertically on the screen.

This was originally for a Syndicate-like game I was working on, to make it feasible to draw sprites of human figures with lots of directional facings, otherwise prohibitive in terms of time. I should really do something with this because it's kinda neat but I want to put it out there for other people to enjoy. I'm also putting this in the code archives.

This even has some rudimentary lighting. It wouldn't be hard to add direction-sourced raymarching for ambient light or even specular to this.

Ideally you would use this for an isometric image editor to produce spritesheets using grabimage, I don't think it's fast enough to do realtime voxel-model display, but it might be. I understand this was how Red Alert II did things but I understand the display was realtime and used hand-tuned assembly, so maybe it's possible, that ran nice on some really shabby hardware. You could also try the SMAC/Alpha Centauri method where they raymarch straight lines through the volumetric data. That's how old-style medical volumetric imaging was possible in real time in the 80s and there are a lot of things you could do with it today for constrained-perspective games, like maybe platformers...

Type TColor
	'color by RGB value
	Field r:Int, g:Int, b:Int
	Method Setrgb(myr:Int,myg:Int, myb:Int)
		r=myr
		g = myg
		b = myb
	End Method
	
	Method getr()
		Return myr
	End Method
	
	Method getg()
		Return g
	End Method
	
	Method getb()
		Return b
	End Method
	
	Method printrgb()
	' For debug.
		Print r + " " + g + " " + b
	End Method
	
	Method setcolorfromtcolor()
	' Set the global draw color according to tcolor rgb.
		SetColor r, g, b
	End Method
	
	Method addrgb(nr:Int, ng:Int, nb:Int)
	' Add given rgb value to this rgb.
		r = r + nr
		g = g + ng
		b = b + nb
	End Method
	
End Type

Type imagegrid  ' This is the voxel model structure.

	'imagegrid - array of RGB values.  Z is positive upward.
	
	Field colorarray : TColor [32,32,32] ' Define the voxel array
	Field imagearray : TImage [32]
	
	Method New()
	' constructor initializes entire array-- we have to do this because
	' Max doesn't ordinarily initialize an array of objects and it produces
	' bizarre errors if you don't.
		For Local i:Int = 0 To 31
			For Local j:Int = 0 To 31
				For Local k:Int = 0 To 31
					colorarray[i,j,k]=New tcolor
				Next
			Next
		Next
		
		For i:Int = 0 To 31
			imagearray[i]=CreateImage(45,69, 1, DYNAMICIMAGE|MASKEDIMAGE)
		Next
	End Method
	
	Method setvoxel(x:Int, y:Int, z: Int, r:Int, g:Int, b:Int)
		Assert invoxel(x,y,z) Else "Voxel out of bounds."
		colorarray[x,y,z].Setrgb(r, g, b)
	End Method
	
	Method drawslice(x:Int, y:Int, z:Int) ' draws a square slice z at screencoords x and y
		Assert testconstraint(x,0,31) Else "Slice out of bounds in drawslice()"
		For Local i:Int = 0 To 31
			For Local j:Int = 0 To 31
				colorarray[i,j,z].setcolorfromtcolor()
				Plot x+j, y+i
			Next
		Next
	End Method
	
	Method fill(r:Int, g:Int, b:Int)
		For Local i:Int = 0 To 31
			For Local j:Int = 0 To 31
				For Local k:Int = 0 To 31
					setpoint(i,j,k, r,g,b)
				Next
			Next
		Next 
	End Method
	
	Method fillrect(ux:Int, uy:Int, lx:Int, ly:Int, z:Int, r:Int, g:Int, b:Int)
		Assert invoxel(ux, uy, z) And invoxel(lx, ly, z) Else "Rect out of bounds in fillrect()"
		For Local i:Int = ux To lx
			For Local j:Int = uy To ly
				setpoint(i,j,z, r,g,b)
			Next
		Next
	End Method
	
	
	Method fillbox(ux:Int, uy:Int, lx:Int, ly:Int, lz:Int, uz:Int, r:Int, g:Int, b:Int) ' coordinates of box, then bottom to top extents
		Assert invoxel(ux, uy, uz) And invoxel(lx, ly, lz) Else "Box out of bounds in fillbox"
		For Local k:Int = lz To uz
			fillrect(ux, uy, lx, ly, k, r, g, b)
		Next
	End Method
	
	Method setpoint(i:Int, j:Int, k:Int, r:Int, g:Int, b:Int)
		Assert invoxel(i,j,k) Else "Voxel out of bounds in setpoint()"
		colorarray[i,j,k].setrgb(r,g,b)
	End Method
	
	Method isoccupied(x:Int, y:Int, z:Int)
		Assert invoxel(x,y,z) Else "Voxel out of bounds in isoccupied()"
		Local tempcolor:tcolor = colorarray[x,y,z]
		If tempcolor.r + tempcolor.g + tempcolor.b > 0
			Return True
		Else
			Return False
		EndIf
	End Method
	
	Method toplight(lightr:Int, lightg:Int, lightb:Int) ' toplights entire boxel
		For Local i:Int = 0 To 31
			For Local j:Int = 0 To 31
				Local k:Int = 31
				' descend from top of boxel to first occupied voxel
				While k > 0 And isoccupied(i,j,k)=False
					k = k - 1
				Wend
				If k > -1 And isoccupied(i,j,k)=True
					colorarray[i,j,k].addrgb(lightr:Int, lightg:Int, lightb:Int)
				EndIf
			Next
		Next
	End Method
	
	Method leftlight(lightr:Int, lightg:Int, lightb:Int)
		For Local j:Int = 0 To 31
			For Local k:Int = 0 To 31
				Local i:Int = 31
				'enter from the left and move toward first occupied voxel
				While i>0 And isoccupied(i,j,j)=False
					i=i-1
				Wend
				If i>-1 And isoccupied(i,j,k)=True
					colorarray[i,j,k].addrgb(lightr:Int, lightg:Int, lightb:Int)
				EndIf
			Next
		Next
	End Method
	
	Method grabsliceimages()
	Local tempimage:TImage = CreateImage(45,69, 1, DYNAMICIMAGE|MASKEDIMAGE)
		
		For Local i:Int = 0 To 31
			Cls
			'SetRotation 45
			'SetScale 1,.5
			SetColor 255,255,255
			drawslice (100,100,i)
			GrabImage tempimage, 100,100, 0
			Cls 
			SetRotation 45
			DrawImage tempimage, 100,100
			GrabImage imagearray[i],77,77,0
		Next
		SetRotation 0
	End Method
	
	Method drawimagestack(x, y)
	Local tempimage:TImage = CreateImage(45,69, 1, DYNAMICIMAGE|MASKEDIMAGE)
		SetScale 1, .5
		For Local i:Int = 0 To 31
		tempimage = imagearray[i]
			DrawImage tempimage,x,y-i
		Next
	End Method
	
End Type

Type voxgrid
	 ' A grid of 32 x 32 x 32 points, and associated isometric sprite images
	Field image:TImage = CreateImage(45,69, 1, DYNAMICIMAGE|MASKEDIMAGE)
	Field imagestack:TImage = CreateImage(45,69,32,DYNAMICIMAGE|MASKEDIMAGE)
	
	Method setimage(myimage:TImage)
		image = myimage
	End Method
	
	Method setimageslice(i:Int, myimage:TImage)
		Assert testconstraint(i,0,31) Else "Slice out of bounds in setimageslice"
	End Method
	
	Method drawgrid(x:Int, y:Int)
		For Local i:Float = 1 To 31
			DrawImage image, x,y-i,0
		Next
	End Method	
	
End Type

Function testconstraint(x:Int, lowerbound:Int, upperbound:Int)
' Test whether an input integer x is between lowerbound and upperbound.
	If x < lowerbound Or x > upperbound 
		Return False
	Else 
		Return True
	EndIf
End Function

Function invoxel(x:Int, y:Int, z:Int)
' Test whether an input point x, y, z is within a 32 by 32 by 32 voxel.
	If testconstraint(x, 0, 31) And testconstraint(y, 0, 31) And testconstraint (z, 0, 31)
		Return True
	Else
		Return False
	EndIf
End Function

Function boxinvoxel(ux:Int,uy:Int, lx:Int, ly:Int, lz:Int, uz:Int)
	If invoxel(ux, uy, uz) And invoxel (lx, ly, lz)
		Return True
	Else
		Return False
	EndIf
End Function

Global testcolor:tcolor = New tcolor
Global testgrid:imagegrid = New imagegrid
Global testgrid2:imagegrid = New imagegrid

testcolor.Setrgb(100,101,102)
testcolor.printrgb()

Graphics 640, 480

For i = 1 To 1000
testgrid.setvoxel (jRand(32),jRand(32),jRand(32), jRand(255),Rand(255),jRand(255))
Next

testgrid2.fill(75,75,100)
'testgrid2.fillrect(10, 10, 20, 20, 5, 0, 0, 0)	
testgrid2.toplight(45,45,45)
testgrid2.leftlight(100,100,100)

Cls
testgrid2.grabsliceimages()


While Not KeyDown (KEY_ESCAPE)

Cls
testgrid.drawslice(100,100,10)
testgrid2.drawslice(140,100,10)
testgrid2.drawslice(180,100,5)
testgrid2.drawslice(240,100,31)
testgrid2.drawimagestack(300,100)
testgrid2.drawimagestack(344,100)
testgrid2.drawimagestack(322,120)
Flip

Wend

Function jrand(x:Int)
	Local retval:Int = Rand(x)-1
	Return retval
End Function