Main Menu

Building a basic 3D Engine

Started by stevmjon, December 28, 2016, 03:43:19 AM

Previous topic - Next topic

kevin

#30
 Steve,

 wow, that's a really impressive demo... mind blown !  

 yeah, It could well benefit from some refactoring, like everything else,  but it's interesting picking through it to see how your adventure is unfolding, just wish I had more time to play with it    


  Note: Link to demo on the previous page


kevin


ran into this old StarGlider video and couldn't help but throw out this hint !




stevmjon

#32
questions and explanations about v0.7.1

*** to see mode look at text at bottom of screen > " subdivide= mode/12  (L) "

mode 1:
> standard 2D  :  uses 2D polygon commands

mode 2 - 6:
> experiment with different subdivide methods to improve the look of 2D textures, and drawing using 2D polygon commands
> mode 6 added polygon z sorting using merge sort routine (sorts furthest polygon to closest polygon in order : artists method)
> mode 6 added sun light correctly

mode 7:
> (start preparing for 3D textures by drawing polygons myself)
> manually draw 2D texture only
> use fastdot to draw each pixel of the scanline for each individual polygon
> this just used random color to test routine

mode 8:
> manually draw 2D texture only
> use texturstrip to read from the polygon image to draw whole scanline for each polygon using a command

mode 9:
> manually draw 2D texture only
> use peek & fastdot to manually read the polygon image myself, and manually draw each pixel myself
> this is because i wanted to prepare for the interperlate routine which needs to calculate each individual pixel for a 3D perspective correct texture

mode 10:
> manually draw 2D texture only
> test z buffer method for a more accurate z depth test (this is more accurate than the artists z sort method)

mode 11:
> manually draw 3D texture
> add interperlate 3D method for perspective correct texture, now i have individual texel checking for the polygon texture that was set-up from mode 9
> try with artists z sort method (to compare with next mode)

mode 12:
> manually draw 3D texture
> still interperlated for 3D texture
> try with z buffer sort method (to test speed with max loading, using 3D interperlate calculation combined with z buffer read/write)


QUESTIONS:
i am using type arrays for the polygon points data.
* is it better to use standard array instead?
you mentioned a change in 1.64 to 1.65 with typed arrays in the video you recorded showcasing this demo.

for the z buffer sorting, i am using standard array in memory that is same size as screen in pixels.
* should i use image that is screen sized instead?
i noticed you did this in one of your demo's, UW3D-RENDER-ENGINE using a library #Include "SaveBitMap".
* is writing to or reading from a screen sized image faster?

i am asking these because i am going to re-write the code using just 3D textures, so i want to use any faster methods where possible.
if anybody has tips to improve my code, please comment, and i will look into it.

   thanks stevmjon


p.s. i looked at the video of starglider, looks interesting kev.
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

kevin

#33
Quote
i am using type arrays for the polygon points data.
* is it better to use standard array instead?
you mentioned a change in 1.64 to 1.65 with typed arrays in the video you recorded showcasing this demo.

 164 / 165 are very different.    165 is a complete rewrite / re imaging of how the internal instruction sets work.   So your code is broken down into a sequence of instructions (serialized)  that the VM executes.  

 The only main thing that 1.65 doesn't have, is  type caching,  which is a set of instructions that handle short cutting accesses of the same structure from within the same expression or across various lines, which could be stuff like typed arrays or variable / list.    This allows the code to be turned into raw memory reads/writes when the same structure is being accessed,  where normal read/writes have nanny code in them to project you from yourself, without them you wouldn't get runtime errors, just crashes.

 There's a whole section in the manual on this under the optexpressions and in the various development blogs .  PlayBASIC V1.64N2 & V1.64N3  (Work In Progress) Gallery
 

 To answer your question, you should be using Typed pointers.    You allocate some  memory and just point at it,  type caching effective turns array accesses into typed pointer accesses..  

   This code uses a typed pointer to access the data within an floating point array, effectively treating it as  buffer..   You could write to anything you can get a porinter from..  Array / Bank / Image whatever
PlayBASIC Code: [Select]
      Type Vertex
x#
Y#
z#
EndType

print SizeOf(Vertex)


NumberOfVertex = 10

// Using an array ass a BANK

Dim Buffer#( (SizeOf(Vertex)/4) * NumberOfVertex )

Dim P as Vertex Pointer
P = GetArrayPtr(Buffer#(),true)

for lp=0 to NumberOfVertex-1

// Write some data to memory
P.X = 100 +lp
P.Y = 200 +lp
P.Z = 300 +lp

// Move to the next vertex
P=int(p)+SizeOf(Vertex)

next


for lp =0 to NumberOfVertex-1
Index = Lp * (SizeOF(Vertex)/4)
print "XYZ"
print Buffer#(Index)
print Buffer#(Index+1)
print Buffer#(Index+2)

next


sync
waitkey






Quote
for the z buffer sorting, i am using standard array in memory that is same size as screen in pixels.
* should i use image that is screen sized instead?
i noticed you did this in one of your demo's, UW3D-RENDER-ENGINE using a library #Include "SaveBitMap".

 In save bitmap it creates a strip the size of the source image and that's one pixel high.   This strip is forced to be 32bit, so what its doing is pixel colour depth conversion using the Pb graphic library instead of trying to manually do this stuff in PB code.    once the strip is copied from the source image, it grabs a pointer to the strip image (which is known to be 32bit) and dump that directly to disc.   Avoiding the whole pixel  by pixel thing so it's basically batching it.


Quote
* is writing to or reading from a screen sized image faster?

   Memory access in your computer rewards near or localize accesses, this means the CPU is writing/reading from cached memory and now actual memory.    So having all the data your operating upon in one place is faster than having it spread all over the place.   In particular if you want to convert it to machine code, where you'll run head first into this type of problem.  

   What you could do is store the depths and colours together, so make z buffer image twice as wide, then even align cords are Z's and ODD's a re pixels

 
PlayBASIC Code: [Select]
   Type zPixel
Depth#
Colour
EndType

Width=GetScreenWidth()
Height=GetScreenHeight()


Surface = MakeZscreen(GetScreenWidth(),GetScreenHeight())

RenderToImage Surface
LockBuffer
ThisRgb=Point(0,0)
For Ylp=0 to height-1

Dim P as zPixel Pointer

; init pointer toi the start of this row
P = GetImagePtr(Surface) + Ylp * GetImagePitch(Surface)

For Xlp=0 to Width-1
P.Depth = 10000.0/Xlp
P.Colour = Xlp*YLP
// Move to the next pixel along this row
P = int(p) + SizeOf(ZPixel)
next
Next
unLockBuffer
rendertoscreen
drawimage Surface,0,0,false
sync
waitkey




Function MakeZscreen(Width,Height)

ThisSurface=GetFreeimage()

;compute width the number of 32bit pixels (4 bytes per pixel)
ActualWidth = Width * (SizeOf(zPixel) / 4)

CreateFXImageex ThisSurface,ActualWidth,Height,32

EndFunction ThisSurface








  Ran into another classic VIRUS on the ARCH




stevmjon

this update converts/reduces the code to display 3D textures only.

V0.7.3

thanks for the info on pointers in the above post kev. the second code snippet displays an interesting image on screen.
i will impliment this later, as i need to change a fair bit of code.

;--------------------------------------------------------------
i have been experimenting with data for a faster display:

Draw > line 211 > array() = PeekInt
Draw > line 214 > PokeInt dest , PeekInt()
Draw > line 215 > CopyMemory source , dest , size

*only have any 1 above line active at a time, and these all run at the same speed.

;-----------------------------------------------------------------------
i also experimented with drawing the screen for a faster display:

Draw > line 303 > FastDot x , y , col
Draw > line 329 > CopyMemory source , dest , size

*change line 296 set = 1 , or set = 2 to test, no speed difference.

;------------------------------------------------------------------------

i have noticed that if i disable the interperlating calculation , comment out Main > line 175 , the game runs at normal speed (set to be 50 fps) but no polygons displayed.
to interperlate, each pixel gets many floating point calculations as the polygon scanline gets calculated across the polygon surface. i think i may need to calc smaller floats?

any comments are welcome.

  stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

kevin

#35
 
 Hi Steve,

      Sorry for my late reply..     Just fired this up on my laptop  and it's amazing to me that even classic runtime is quick enough to even get this in motion,  there's so much work to chew through per pixel...  wow...  the code is generally excellent too..  



      In your conversion routine,

PlayBASIC Code: [Select]
     For lp=0 To max
col = z_buffer4_rgb(lp)
If col>=0
FastDot x,y,col ; **slightly slower**
EndIf

Inc x
If x>=width Then x=0 : Inc y

Next lp





    Should be better expressed as a nested loop,  that will make the cost per pixel lower.   Lowest cost per pixel ='s faster.



PlayBASIC Code: [Select]
     For ylp=0 To height-1
For xlp=0 To Width-1
col = z_buffer4_rgb(RowIndex+Xlp)
If col=0
FastDot xlp,ylp,col ; **slightly slower**
EndIf
Next xlp

RowIndex += Width
Next ylp





    This should give you a 6 instruction (at worst) inner loop..   (6 PB VM instructions that is)


     You can loop through the rows which would get rid one add pixel

PlayBASIC Code: [Select]
         RowStart=0
RowEnd = Width-1

For ylp=0 To height-1
For xlp=RowStart To RowEnd
col = z_buffer4_rgb(Xlp)
If col=0
FastDot xlp,ylp,col ; **slightly slower**
EndIf
Next xlp
RowStart += Width
RowEnd += Width
Next ylp




   
  EDIT:  - That's not going to work with FastPoint as the XLP isn't a coordinate.   I guess you could poke it, you'd still need to add the offset, but the addition would be inside IF/ENDIF.    So pixels that are transparent cost less to draw than solid pixels




  Now your other loop is copying the buffer to the target via copy memory,  which of course is the quickestt option as there's not VM overhead and it's just pure machine code .. So the bugger the chuck the better the reward.


   What you could do, which I think is safer (works on all display depths) and would allow you to mask the src image to the destination...  Would be much like I mentioned above, where I create a temp image, where you copy the rows to the temp buffer which is forced to be 32bit regardless of the current machines display depth,  then we jusr draw that image to the target.   This way you shift the work load from pixel by pixel through the PBVM to machine code routines.  So the cost per pixel is tiny in comparison.



PlayBASIC Code: [Select]
      Width= 800
Height=600

Dim ScreenArray(Width*Height)

Do
cls 255

WidthByHeightMinus1 =Width*Height-1
For lp=0 to rnd(50)

Index1 = floor(Rnd#(WidthByHeightMinus1))

Index2 = ClipRange(Index1+100,0 ,WidthByHeightMinus1)

For Index = Index1 To index2
ScreenArray(index) = rndrgb()
next
next


if (frames and $ff)=0

ClearArray ScreenArray(),0
Endif

RenderScreen(Width,Height)
print frames
frames++

sync
loop



Function RenderScreen(Width,Height)

// This has no clipping

local ThisSurface= GetSurface()


static Strip32BitImage
if Strip32BitImage=0

Strip32BitImage= GetFreeimage()
CreateFxImageEx Strip32BitImage,Width,Height, 32
endif


local SrcPtr = GetArrayPtr(ScreenArray(),true)
local DestPtr = GetImagePtr(Strip32BitImage)


WidthBy4 = Width*4

For Ylp=0 to Height-1

CopyMemory SrcPtr,DestPtr,WidthBy4

SrcPtr += (WidthBy4)
DestPtr += (WidthBy4)

next

rendertoimage ThisSurface
lockbuffer
drawimage Strip32BitImage,0,0,true
unlockbuffer

EndFunction






    I'll post a copy of this in the source board also..  as things tend to get lost inside big threads..







stevmjon

thanks for the tips kev

my program is running 20% faster now. i used the ex image like a mask layer, to only draw the foreground (visible polygons).
the background image (sky) is seperate, and is drawn to screen first, as a sort of clear old data from backbuffer.

the issue is still the calculating the perspective correct interperlating of each pixel in each polygon. 32 bit floats take time to calculate as there are lots of them.
i need to interperlate the start slope point and end slope point, along polygon edges, then interperlate between these 2 points for perspective correct texel from uv map.
i wonder if you could modify the TextureStripH command to add z component? instead of the linear calc it does to get texel, an extra z component interperlates texel ...?  when i used this command in v0.7.1 it ran fast (no perspective interperlate along polygon edges though, just linear steps along slope).

*have you thought of adding command Float / Double ?
i only need 3 or 6 decimal places after the point for the calculations. this may speed things up too?

anyway, i will keep experimenting. i was thinking of making an .obj loader, so i can make objects with my 3D software (lightwave 3D) and bring these into the game. i need to add collisions too, so player can't just walk through everything. even though playbasic is a 2D language, i still enjoy doing 3D in it.

   stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

kevin

#37
 Steve,

Quote
my program is running 20% faster now. i used the ex image like a mask layer, to only draw the foreground (visible polygons).
the background image (sky) is seperate, and is drawn to screen first, as a sort of clear old data from backbuffer.

 That's good news..  


Quote
the issue is still the calculating the perspective correct interperlating of each pixel in each polygon. 32 bit floats take time to calculate as there are lots of them.

 I know :)


Quote
i need to interperlate the start slope point and end slope point, along polygon edges, then interperlate between these 2 points for perspective correct texel from uv map.
i wonder if you could modify the TextureStripH command to add z component? instead of the linear calc it does to get texel, an extra z component interperlates texel ...?  when i used this command in v0.7.1 it ran fast (no perspective interperlate along polygon edges though, just linear steps along slope).


  Yeah what's normally done is you only compute perspectively correct UV's at fixed intervals like 8 pixels.   So the divides can be wrapped around the integer inner loops,  which are pure linear interpolations.   To do they means writing the loop in assembly as the floating point divide needs to interleaved with integer operations as to not stall execution.  Clearly this is not something a runtime can do or even modern high level C/C++ compilers struggle with such concepts.   None the less, in term of whats built into PB today you could use draw strip to render blocks of 8 or 16 pixels linearly, but you'd have to step the z yourself I guess.  Which should be more optimal than raw brute force per pixel approach.




PlayBASIC Code: [Select]
            ;--------------------------------------------------
;--- setup array data for interperlate scanline ---
;--------------------------------------------------
For lps=0 To distance

t# = lps/distance# : t2# = 1-t#

;--- 3D texel u & v ---
topu# = t2#*a1# + t#*b1#
bottu# = t2#*c1# + t#*d1#
pixelU# = topu# / bottu# ; 3D U (texel)

topv# = t2#*a2# + t#*b2#
bottv# = t2#*c1# + t#*d1#
pixelV# = topv# / bottv# ; 3D V (texel)

;--- calc memory address for texel in array ---
pixelV = pixelV# : pixelU = pixelU#
pixel = address + pixelV*pitch + pixelU*4 ; integer = 4 bytes
;NOTE: polygon scanline can range between 1 pixel to screen width

If sub=10
;-----------------------------------------
;--- draw directly to screen (option) ---
;-----------------------------------------

;scanline(lps) = PeekInt(pixel) ; faster*
col = PeekInt(pixel) : x=interpl+lps
FastDot x,scanY,col ; **slower than copy memory**
; NOTE: get data directly from memory then draw to screen

Else

;---------------------------------------------------------------------------------
;--- save data in z buffer array (draw to screen in draw_z_buffer() Function) ---
;---------------------------------------------------------------------------------

;;col = PeekInt(pixel)
arraypos = scany * width + interpl+lps
z_buffer4_rgb(arraypos) = PeekInt(pixel) ; **slightly slower than above set, but more accurate z sort**
; NOTE: get data directly from memory then save in Z buffer array

;PokeInt dest_address3+arraypos*4,PeekInt(pixel) ; no faster here*
;CopyMemory pixel,dest_address3+arraypos*4,4 ; no faster here*
; NOTE: get data directly from memory then write to destination directly to memory

EndIf

Next lps







      There's a huge amount of work per pixel, a lot of Mult + Divides,  even in the VM the cost a mult and divide is high as there's a stall waiting for the FPu to finish the computation.   To make CPU's quicker they like to stack opcodes and execute them in 2 pipes.  So instructions are executed out of order when they're not dependent.   Which is cool in assembly programming where you can manually lay out every opcode to try and get it all to pair and miss as many potential stalls as possible.   But in a runtime we can't really do this, or at least I visualize a way to do this from C

     
      If you want to use test code inside time critical loops, then define some type of MODE constant and use #IF   #ENDIF blocks.   So the code can still be within the function can be removed at compile time depending upon the value of the your mode constant.


PlayBASIC Code: [Select]
               #If sub=10
;-----------------------------------------
;--- draw directly to screen (option) ---
;-----------------------------------------

;scanline(lps) = PeekInt(pixel) ; faster*
col = PeekInt(pixel) : x=interpl+lps
FastDot x,scanY,col ; **slower than copy memory**
; NOTE: get data directly from memory then draw to screen

#Else

;---------------------------------------------------------------------------------
;--- save data in z buffer array (draw to screen in draw_z_buffer() Function) ---
;---------------------------------------------------------------------------------

;;col = PeekInt(pixel)
arraypos = scany * width + interpl+lps
z_buffer4_rgb(arraypos) = PeekInt(pixel) ; **slightly slower than above set, but more accurate z sort**
; NOTE: get data directly from memory then save in Z buffer array

;PokeInt dest_address3+arraypos*4,PeekInt(pixel) ; no faster here*
;CopyMemory pixel,dest_address3+arraypos*4,4 ; no faster here*
; NOTE: get data directly from memory then write to destination directly to memory

#EndIf




           Depend upon the version of PB, this saves a couple of instructions per pixel, which really adds up of course.     I've got some optimization ideas on the to do list, that might help your inner loops here, but I suspect your best results will be found by rearranging the expressions and try and get to less instructions per pixel.   

       
           When reading the texture, what you could do is a type pointer that pointing at the top left pixexl of the texture. 

           So this,
PlayBASIC Code: [Select]
           pixel = address + pixelV*pitch + pixelU*4  




          Becomes
PlayBASIC Code: [Select]
           pixel = Teature.Pixels(pixelV * pitch + pixelU)  




           Which is 2 instructions (at least) per pixel quicker.   Pitch would need to be divided by 4 tho.   




Quote
*have you thought of adding command Float / Double ?
i only need 3 or 6 decimal places after the point for the calculations. this may speed things up too?

  PB is 32bit internally,  adding doubles (64bit floats & integers) is on the agenda but I doubt you'd get faster.   The FPU is 80 bit interally, all floats / integers are imported and recast to 80 bit,  then the operation is done and the result it's recast back to the target accuracy.   64 gives a wider accuracy but it'd twice the memory bandwidth..    


Quote
anyway, i will keep experimenting. i was thinking of making an .obj loader, so i can make objects with my 3D software (lightwave 3D) and bring these into the game. i need to add collisions too, so player can't just walk through everything. even though playbasic is a 2D language, i still enjoy doing 3D in it.

  Ahh yeah.. I  wrote an OBJ loader way back in the Amiga days,  but I doubt the formats are the same today.  I think that code was even in Amos ..   Will have a look later on my desk top.  



stevmjon

thanks kev

after the imageEX tip, and the perspective calc every 8 pixels tip, now i get double the frame rate i was getting. yay.
still, i would like to get it faster.

for this i was looking at pointers next, like the code clip you posted above ( pixel = Texture.Pixels(pixelV * pitch + pixelU) ).
*i was wondering, can you make a video to explain pointers?

an explanation about how pointers work, compared to standard array access would be great. like, how does an array access data, compared to how a pointer access the same data. i assume standard array access must have overhead (costs time), but pointer gets around this???
what is the difference between pointer and type pointer? some info about how these are stored/accessed in memory would be great.
i haven't really used pointers/type pointers as i don't fully understand how to use them properly.

any advice would be great, stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

kevin


    if you can wrap your head around peek & poke, then pointers are the same thing.   Primitive pointers such as Byte/Word/Integer/Float are for reading/write that data type.   Typed pointers allow us to use the type structure as a series of fixed offsets from the pointer.   Think of the pointer as a base address and the fields are like adding an offset to the address your going to poke into.    Eg.    PokeInt RowAddress+(PixelOffset * 4 ) , ThisColour

   From Poke to Pointers (screen fill examples)  (2018-12-11)

kevin

#40
   UpScale

     What you could is render in %50 of the resolution and then scale that up.   I've used this in a few demos that do mass pixel writing.   So a lot less drawing overhead, mixed with some bilinear filtering on the up scaling..   Depending on the image it can actually look ok


PlayBASIC Code: [Select]
   PixelScaleX#= 2
PixelScaleY#= 2

Width=getSCreenwidth()/PixelScaleX#
Height=GetScreenHeight()/PixelScaleY#

Screen = NewImage(Width, Height,2)


do
rendertoimage Screen

inkmode 1+64
for lp =0 to 500
x1=rnd(width)
y1=rnd(height)
x2=rnd(width)
y2=rnd(height)

linec x1,y1,x2,y2,rndrgb()

next

circle mousex()/PixelScaleX#,Mousey()/PixelScaleY#,20,1
inkmode 1

refresh Screen

loop spacekey()
end



Function Refresh(VirtualScreen)
oldsurface=getsurface()

rendertoscreen
sw=getscreenwidth()
sh=getscreenheight()

iw=GetimageWidth(VirtualScreen)
ih=GetimageHeight(VirtualScreen)

ScaleX#=float(sw)/iw
ScaleY#=float(sh)/ih


drawRotatedImage VirtualScreen,0,0,0,ScaleX#,ScaleY#,0,0,false+8

blurimage VirtualScreen,12.1

Sync

rendertoimage oldSurface


EndFunction







  Also,  Ran into this today and instantly thought of you!   :)  



  Robotron (PS1 Version)

    This is the exact type of thing that comes to mind when doing retro 3D,  simple worlds / objects and no need for 3d physics since it's an arcade shooter,  the object render could do this out of the box.

   






  Tomb Raider (PS1 Version)

    It's amazing looking back at the complexity of the scenes in TR1,  it was big deal back in day to have full screen 3D even at 320*200 resolution in the DOS versions.  The tool sets for content creators and lack of fill rates really push the designers to a result ...   but it's all linear mapping on Playstation versions.   They really did some interest stuff on that system over the years. 

 


stevmjon

using TextureStripH to fill in the scanline chunks (meaning faster)

V0.7.6

this version runs at a much better speed now, compared to the older version.
if you launched the game and didn't move the mouse:
> v0.7.3 = 5 fps        (use z buffer array & calc every interporlated polygon pixel)
> v0.7.4 = 10 fps        (use z buffer array & calc every 8th interperlated polygon pixel and linear interperlate between, and draw to EX screensize image) * thanks kev for this tip
> v0.7.6 = 25 fps        (calc every 64 interperlated polygon pixel and use TextureStripH to interperlate between, and draw to EX screensize image)

i am excited about this increase in speed. the only setback is i am no longer using the z buffer array in v0.7.6, so i can't get pixel perfect polygon overlap accuracy, but i do get polygon z sorting with back to front drawing order.

upscale demo kev, this looks good too. i will check it out.

* press i key to change the texture on the polygons, to see grid lines (total 4 textures)

    enjoy stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

kevin

  ran in this article today...  interesting

 Pushing Polygons on the Mega Drive




 

stevmjon

hey kev
the above demo is interesting. left to right drawing without overdraw.

how my 3D engine v0.7.6 is currently rendering, i do get overdraw of pixels. i did this because i used TextureStripH to speed up interperlating textures for the polygons. if i don't use this i get a slow framerate.

funny, i was thinking about how to reduce overdraw lately, so now i have more homework...

at the moment i have been writing collision detections in the 3D engine. 2D collisions are easier to setup, but 3D are more work, but i am getting there.
detecting collisions is the easier part, it is what to do next that is more involved.
eg. if a player walked into a wall, but was facing slightly left/right, you need to be able to push the player back away from the wall AND allow the move left/right movement also, so the player will slide along the wall. i came up with an idea for this and am in the final testing at the moment. can't wait to post the next demo.

  stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.

stevmjon

hello

i just worked out the collision routine, and thought i would share how i done it (see pic).
i used Seperated Axis Theorem (SAT) to detect the collision, then i came up with my own routine to respond to the collision.
it took me several attempts to come up with this, to work in the way i wanted. i wanted the player to 'slide' along walls, rather than just stop.

in game, the player can now collide with objects and the ground, and even jump on top of objects and use them to get higher up.
i am currently adding in gravity, so player falls off edges etc, and i am modifying the scene to have fun with this.
i am also adding some more graphics to have some variety. must keep it under 5 meg to upload on the forums though.

i will post a new scene as soon as this is done.

   stevmjon
It's easy to start a program, but harder to finish it...

I think that means i am getting old and get side tracked too easy.