[BB] Terrain With The Voxel Space engine!

Started by Filax, March 13, 2025, 12:42:35

Previous topic - Next topic

Filax

Hey! :)

Today I was browsing YouTube and came across this video:

Where the guy explains a very old technique from my teenage years, using games like Comanche Overkill!
https://github.com/BlackCreepyCat/Blitz3D-Terrain-Voxel-Space-engine/tree/main

The guy explains how to replicate this very old effect! In the links to his video, he also provides links to the
original Comanche Overkill maps! Try them with my code! :)

https://github.com/gustavopezzi/voxelspace/blob/main/maps/gif/

So I worked on a super fast blitz3D version! I had to cut back on the Z precision to get more FPS, but the result
with the game maps is really nice!

Arrow key to move / E+D = Up n Down / Z+S = Rotation

; ----------------------------------------
; Name : Terrain The Voxel Space engine
; Date : (C)2025
; Site : https://github.com/BlackCreepyCat
; Inspired from :
; https://www.youtube.com/watch?v=bQBY9BM9g_Y
; ----------------------------------------


; Set the screen resolution to 640x480, 32-bit color depth, and double buffering
Graphics 640, 480, 32, 2
SetBuffer BackBuffer()

; Define constants for screen dimensions and map size
Const SCREEN_WIDTH = 640
Const SCREEN_HEIGHT = 480
Const MAP_N = 1024
Const SCALE_FACTOR# = 70.0

; Declare arrays to store height and color maps
Dim heightmap(MAP_N-1, MAP_N-1)
Dim colormap(MAP_N-1, MAP_N-1)

; Load the height and color images
Global heightImage = LoadImage("map1.height.png")
Global colorImage = LoadImage("map1.color.png")

; Check if the images were successfully loaded
If heightImage = 0 Or colorImage = 0 Then
    RuntimeError "Error: Unable to load heightmap or colormap!"
EndIf

; Check if the images have the correct dimensions
If ImageWidth(heightImage) <> MAP_N Or ImageHeight(heightImage) <> MAP_N Then
    RuntimeError "Error: Incorrect image size!"
EndIf

; Lock the image buffers to read the pixel data
LockBuffer ImageBuffer(heightImage)
LockBuffer ImageBuffer(colorImage)
For x = 0 To MAP_N-1
    For y = 0 To MAP_N-1
        ; Read pixel data for heightmap and colormap
        heightmap(x, y) = ReadPixelFast(x, y, ImageBuffer(heightImage)) And $FF
        colormap(x, y) = ReadPixelFast(x, y, ImageBuffer(colorImage))
    Next
Next
UnlockBuffer ImageBuffer(heightImage)
UnlockBuffer ImageBuffer(colorImage)

; Free the images after reading their data
FreeImage heightImage
FreeImage colorImage

; Define the Camera type with fields for position, height, angle, etc.
Type Camera
    Field x#, y#, height#, horizon#, zfar#, angle#
End Type

; Initialize the camera object with default values
Global cam.Camera = New Camera
cam\x = 512.0
cam\y = 512.0
cam\height = 70.0
cam\horizon = 60.0
cam\zfar = 600.0
cam\angle = 0

; Create a framebuffer image for drawing
Global framebuffer = CreateImage(SCREEN_WIDTH, SCREEN_HEIGHT)

; Main game loop that runs until the escape key is pressed
While Not KeyHit(1)
    Local moveSpeed# = 2.0
   
    ; Check for movement input and update camera position
    If KeyDown(200) Then
        cam\x = cam\x + Cos(cam\angle) * moveSpeed
        cam\y = cam\y + Sin(cam\angle) * moveSpeed
    EndIf
    If KeyDown(208) Then
        cam\x = cam\x - Cos(cam\angle) * moveSpeed
        cam\y = cam\y - Sin(cam\angle) * moveSpeed
    EndIf
   
    ; Adjust the camera angle and height based on input
    If KeyDown(203) Then cam\angle = cam\angle - 3
    If KeyDown(205) Then cam\angle = cam\angle + 3
    If KeyDown(18) Then cam\height = cam\height + 1
    If KeyDown(32) Then cam\height = cam\height - 1
    If KeyDown(31) Then cam\horizon = cam\horizon + 1.5
    If KeyDown(17) Then cam\horizon = cam\horizon - 1.5
   
    ; Set the buffer for drawing to the framebuffer
    SetBuffer ImageBuffer(framebuffer)
    ClsColor 50, 100, 200
    Cls
   
    ; Switch back to the back buffer and lock the framebuffer buffer
    SetBuffer BackBuffer()
    LockBuffer ImageBuffer(framebuffer)
   
    ; Calculate the camera's direction using sine and cosine
    Local sinangle# = Sin(cam\angle)
    Local cosangle# = Cos(cam\angle)
   
    ; Calculate the four corners of the viewing plane
    Local plx# = cosangle * cam\zfar + sinangle * cam\zfar
    Local ply# = sinangle * cam\zfar - cosangle * cam\zfar
    Local prx# = cosangle * cam\zfar - sinangle * cam\zfar
    Local pry# = sinangle * cam\zfar + cosangle * cam\zfar
   
    ; Loop through each horizontal screen pixel
    For i = 0 To SCREEN_WIDTH-1
        ; Calculate the position for each pixel on the map
        Local deltax# = (plx + (prx - plx) / SCREEN_WIDTH * i) / cam\zfar
        Local deltay# = (ply + (pry - ply) / SCREEN_WIDTH * i) / cam\zfar
       
        ; Initialize camera position and set the tallest height to the screen height
        Local rx# = cam\x
        Local ry# = cam\y
        Local tallestheight# = Float(SCREEN_HEIGHT)
       
        Local z# = 1.0
        Local stepZ# = 1.0
       
        ; Loop through each pixel in the view range, adjusting the z-buffer
        While z < cam\zfar
            rx = rx + deltax
            ry = ry + deltay
           
            ; Get the map coordinates and height value at the current position
            Local mapX = Int(rx) And (MAP_N-1)
            Local mapY = Int(ry) And (MAP_N-1)
            Local h# = Float(heightmap(mapX, mapY)) 
            Local projheight# = (cam\height - h) / (z + 0.0001) * SCALE_FACTOR + cam\horizon 
           
            ; Draw pixels if the projected height is less than the tallest height
            If projheight < tallestheight Then
                For y = projheight To tallestheight-1
                    If y >= 0 And y < SCREEN_HEIGHT Then
                        WritePixelFast i, y, colormap(mapX, mapY), ImageBuffer(framebuffer)
                    EndIf
                Next
                tallestheight = projheight
            EndIf
           
            ; Increase stepZ for faster movement at greater distances
            If z > cam\zfar / 2 Then stepZ = 2.0 Else stepZ = 1.0
           
            ; Increment z to move further into the scene
            z = z + stepZ
        Wend
    Next
   
    ; Unlock the framebuffer buffer
    UnlockBuffer ImageBuffer(framebuffer)
   
    ; Draw the framebuffer to the screen and flip buffers
    DrawImage framebuffer, 0, 0
    Flip
   
Wend

; Free the framebuffer image after use
FreeImage framebuffer
End

Filax

#1
Another version with movement smoothing with mouse and less gfx glitch, and now you can increase the terrain height :

I put some new nice maps to try!

; ----------------------------------------
; Name : Terrain The Voxel Space engine
; Date : (C)2025
; Site : https://github.com/BlackCreepyCat
; Inspired from :
; https://www.youtube.com/watch?v=bQBY9BM9g_Y
; ----------------------------------------
Graphics3D 800, 600, 32, 2
SetBuffer BackBuffer()
Global SCREEN_WIDTH = 800
Global SCREEN_HEIGHT = 600
Const MAP_N = 1024
Const SCALE_FACTOR# = 70.0
Const CAMERA_SMOOTHING# = 0.1
Const HEIGHT_SCALE_FACTOR# = 2.0
Dim heightmap(MAP_N-1, MAP_N-1)
Dim colormap(MAP_N-1, MAP_N-1)
Global heightImage = LoadImage("map17.height.png")
Global colorImage = LoadImage("map17.color.png")
If heightImage = 0 Or colorImage = 0 Then
    RuntimeError "Error: Unable to load heightmap or colormap!"
EndIf
If ImageWidth(heightImage) <> MAP_N Or ImageHeight(heightImage) <> MAP_N Then
    RuntimeError "Error: Incorrect image size!"
EndIf
LockBuffer ImageBuffer(heightImage)
LockBuffer ImageBuffer(colorImage)
For x = 0 To MAP_N-1
    For y = 0 To MAP_N-1
        heightmap(x, y) = ReadPixelFast(x, y, ImageBuffer(heightImage)) And $FF
        colormap(x, y) = ReadPixelFast(x, y, ImageBuffer(colorImage))
        heightmap(x, y) = Int(heightmap(x, y) * HEIGHT_SCALE_FACTOR#)
    Next
Next
UnlockBuffer ImageBuffer(heightImage)
UnlockBuffer ImageBuffer(colorImage)
FreeImage heightImage
FreeImage colorImage
Type Camera
    Field x#, y#, height#, horizon#, zfar#, angle#, pitch#
    Field targetX#, targetY#, targetHeight#, targetAngle#, targetPitch#
End Type
Global cam.Camera = New Camera
cam\x = 512.0
cam\y = 512.0
cam\height = 120.0
cam\horizon = 60.0
cam\zfar = 700.0
cam\angle = 0
cam\pitch = 0
cam\targetX = cam\x
cam\targetY = cam\y
cam\targetHeight = cam\height
cam\targetAngle = cam\angle
cam\targetPitch = cam\pitch
Global framebuffer = CreateImage(SCREEN_WIDTH, SCREEN_HEIGHT)
MoveMouse(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
While Not KeyHit(1)
    Local moveSpeed# = 2.0
    Local mouseSensitivity# = 0.2  ; Sensibilité pour la rotation
    Local pitchHeightMultiplier# = 5.0  ; Multiplicateur pour amplifier l'effet du pitch sur la hauteur
   
    ; Rotation horizontale (yaw) avec la souris, inversée
    cam\targetAngle = cam\targetAngle + MouseXSpeed() * mouseSensitivity  ; Inversion : + au lieu de -
   
    ; Inclinaison verticale (pitch) avec la souris
    cam\targetPitch = cam\targetPitch - MouseYSpeed() * mouseSensitivity
   
    ; Limiter le pitch entre -89° et 89° pour éviter de basculer
    If cam\targetPitch > 89 Then cam\targetPitch = 89
    If cam\targetPitch < -89 Then cam\targetPitch = -89
   
    ; Recentrer la souris
    MoveMouse SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2
   
    ; Calculer les composantes de mouvement basées sur l'angle et le pitch
    Local sinPitch# = Sin(cam\pitch)
    Local cosPitch# = Cos(cam\pitch)
    Local sinAngle# = Sin(cam\angle)
    Local cosAngle# = Cos(cam\angle)
   
    ; Mouvement contrôlé par les flèches (gauche/droite non inversé)
    If KeyDown(200) Then  ; Haut (avancer)
        cam\targetX = cam\targetX + cosAngle * cosPitch * moveSpeed
        cam\targetY = cam\targetY + sinAngle * cosPitch * moveSpeed
        cam\targetHeight = cam\targetHeight + sinPitch * moveSpeed * pitchHeightMultiplier
    EndIf
    If KeyDown(208) Then  ; Bas (reculer)
        cam\targetX = cam\targetX - cosAngle * cosPitch * moveSpeed
        cam\targetY = cam\targetY - sinAngle * cosPitch * moveSpeed
        cam\targetHeight = cam\targetHeight - sinPitch * moveSpeed * pitchHeightMultiplier
    EndIf
   
    If KeyDown(203) Then  ; Gauche (strafe vers la gauche)
        cam\targetX = cam\targetX + sinAngle * moveSpeed
        cam\targetY = cam\targetY - cosAngle * moveSpeed
    EndIf
    If KeyDown(205) Then  ; Droite (strafe vers la droite)
        cam\targetX = cam\targetX - sinAngle * moveSpeed
        cam\targetY = cam\targetY + cosAngle * moveSpeed
    EndIf
   
    ; Ajuster l'horizon en fonction du pitch pour l'effet visuel
    cam\horizon = 60.0 + (cam\pitch * 5.0)  ; 60 est la valeur de base, ajustée par le pitch
   
    ; Limiter la hauteur pour éviter de descendre sous le sol
    If cam\targetHeight < 0 Then cam\targetHeight = 0
   
    ; Lissage des mouvements et rotations
    cam\x = cam\x + (cam\targetX - cam\x) * CAMERA_SMOOTHING
    cam\y = cam\y + (cam\targetY - cam\y) * CAMERA_SMOOTHING
   
    cam\height = cam\height + (cam\targetHeight - cam\height) * CAMERA_SMOOTHING
    cam\angle = cam\angle + (cam\targetAngle - cam\angle) * CAMERA_SMOOTHING
    cam\pitch = cam\pitch + (cam\targetPitch - cam\pitch) * CAMERA_SMOOTHING
   
    SetBuffer ImageBuffer(framebuffer)
    ClsColor 50, 100, 200
    Cls
   
    SetBuffer BackBuffer()
    LockBuffer ImageBuffer(framebuffer)
   
    sinAngle# = Sin(cam\angle)
    cosAngle# = Cos(cam\angle)
   
    Local plx# = cosAngle * cam\zfar + sinAngle * cam\zfar
    Local ply# = sinAngle * cam\zfar - cosAngle * cam\zfar
    Local prx# = cosAngle * cam\zfar - sinAngle * cam\zfar
    Local pry# = sinAngle * cam\zfar + cosAngle * cam\zfar
   
    For i = 0 To SCREEN_WIDTH-1
        Local deltax# = (plx + (prx - plx) / SCREEN_WIDTH * i) / cam\zfar
        Local deltay# = (ply + (pry - ply) / SCREEN_WIDTH * i) / cam\zfar
       
        Local rx# = cam\x
        Local ry# = cam\y
        Local tallestheight# = Float(SCREEN_HEIGHT)
       
        Local z# = 1.0
        Local stepZ# = 1.0
       
        While z < cam\zfar
            rx = rx + deltax
            ry = ry + deltay
           
            Local mapX = Int(rx) And (MAP_N-1)
            Local mapY = Int(ry) And (MAP_N-1)
            Local h# = Float(heightmap(mapX, mapY))
            Local projheight# = (cam\height - h) / (z + 0.0001) * SCALE_FACTOR + cam\horizon
           
            If projheight < tallestheight Then
                For y = projheight To tallestheight-1
                    If y >= 1 And y < SCREEN_HEIGHT Then
                        WritePixelFast i, y-1, colormap(mapX, mapY), ImageBuffer(framebuffer)
                        WritePixelFast i, y, colormap(mapX, mapY), ImageBuffer(framebuffer)
                    EndIf
                Next
                tallestheight = projheight
            EndIf
           
            If z > cam\zfar / 2 Then stepZ = 2.0 Else stepZ = 1.0
            z = z + stepZ
        Wend
    Next
   
    UnlockBuffer ImageBuffer(framebuffer)
   
    DrawImage framebuffer, 0, 0
   
    Color 0, 0, 0
    Text 10, 10, "ARROWS TO MOVE / MOUSE FOR YAW (INVERTED) & PITCH"
    Flip
Wend
FreeImage framebuffer
End

Dan

#2
Nice examples.

The second example gives an occasional MAV. It can be fixed by changing the line to

If y >= 1 And y < SCREEN_HEIGHT Then

or

If y >0 And y < SCREEN_HEIGHT Then
65536 GOTO Back2Basic

Filax

Quote from: Dan on March 13, 2025, 17:09:42Nice examples.

The second example gives an occasional MAV. It can be fixed by changing the line to

If y >= 1 And y < SCREEN_HEIGHT Then

or

If y >0 And y < SCREEN_HEIGHT Then
Fixed! Thanks for the bug! :)