🧪 Jelly Sinus Terrain - How It Works (Step-by-Step Guide)

By PlayBASIC Tutor
Created: May 2025

This demo renders a jelly-like, animated sinusoidal terrain using a grid of rotating, Gouraud-shaded polygons. The "wobble" is based on each vertex's distance from the center of the grid and animated in real time.










🔧 1. Setup Constants and Data Types

* `WorldSIZE#` defines how wide the virtual world is.
* `Size` controls the grid resolution.
* A typed array `Terrain()` stores the 3D coordinates and color for each vertex.

PlayBASIC Code:
Type tVertex
    x#, y#, z#      ; Original 3D position
    rx#, ry#, rz#    ; Rotated/projected 3D position
    Colour          ; Vertex color
EndType
COMMANDS USED:




🌈 2. Create a Grayscale Palette

We generate a grayscale palette based on depth. This will be used later to shade the polygons based on how far away they are.

PlayBASIC Code:
for lp = 0 to depth
    Scaler# = (depth - lp) / float(depth)
    ThisRGB = cliprange(Scaler# * 255, 0, 255)
    palette(lp) = rgb(ThisRGB, ThisRGB, ThisRGB)
next
COMMANDS USED: FLOAT | CLIPRANGE | RGB |




🎨 3. Draw the Backdrop

We create a shaded backdrop image using `shadebox`. This gives a nicer visual background behind the terrain.

PlayBASIC Code:
BackDrop = NewImage(...)
rendertoimage BackDrop
shadebox ...
rendertoscreen




🔁 4. Main Loop

Each frame, we:

* Draw the backdrop
* Capture 3D scene to render
* Update tilt and turn angles for camera rotation
* Recalculate the terrain heights
* Rotate and project the terrain
* Render the terrain polygons

PlayBASIC Code:
tilt# += 0.01
turn# += 2.3
RenderGround(Terrain(), WORLDSIZE#)
RotateVerts(Terrain(), Tilt#, Turn#, Roll#)
Render(Terrain())
COMMANDS USED:




### 🏔� 5. **RenderGround() – Build the Heightmap**

This function fills the `Terrain()` array with X/Z positions in a grid, and the Y (height) is calculated using a sinus function based on distance from the center. The result is a radial wave (like ripples in water).

PlayBASIC Code:
Map(xlp,zlp).y# = 1000 - (sin(baseangle# + angle#) * (1000 * (1 - Distance#)))
COMMANDS USED: SIN |


* `baseangle#` is animated each frame to make the wave move.
* `scaler#` affects wave frequency.



🌀 6. RotateVerts() – Rotate and Project Vertices

Each 3D point is rotated using a 3x3 rotation matrix for tilt, turn, and roll. After rotation, it’s projected to 2D screen space using perspective projection.

PlayBASIC Code:
rx# = screenX
ry# = screenY
rz# = depthZ
COMMANDS USED:


This is where the terrain "wobbles" and appears 3D.



🔺 7. **Render() – Draw the Polygons

We loop over the grid of vertices and draw **two triangles** for each quad (square) using `gouraudtri`.

Each vertex's color is affected by depth (z) using the earlier-created palette, giving us a 3D shaded look.

PlayBASIC Code:
Rgb1 = RgbAlphaMult(Rgb1, ShadeRGB)
gouraudtri ...
COMMANDS USED: RGBALPHAMULT | GOURAUDTRI |


There’s also a bonus "depth ray" line drawn for fun.



📌 Notes and Tips

* The terrain “jelly” effect is created using a sinusoidal function that changes each frame, causing the wave to pulse out from the center.
* The terrain rotates and zooms in/out smoothly with perspective.
* This demo uses a single layer of polygons. It's fast because it avoids overdraw and depth-sorting issues.
* Don’t store original and rotated values in the same array in real projects. This is just for demo simplicity.



🧠 What New Programmers Can Learn

* How to use typed arrays for storing 3D vertex data
* How to build and animate a height map
* How to rotate and project 3D points to 2D
* How to shade polygons based on depth
* How sinus functions can be used for cool animation effects




PlayBASIC Code:
; PROJECT : Lerp Ground
; AUTHOR  : PlayBASIC Tutor - https://PlayBASIC.com
; CREATED : 2/05/2025
; EDITED  : 3/05/2025
; ---------------------------------------------------------------------


    WorldSIZE#    = 7500
    Size            = 64

    Type tVertex
            x#,y#,z#
            rx#,ry#,rz#
            Colour 
    EndTYPE

    Dim Terrain(Size,Size) as tVertex

    depth    =15000    

    Camera = newcamera()
    cameracls camera,off
    Scenecachesize 1000*1000 *8
    scenemaxzdepth depth

    dim palette(depth)
    for lp =0 to depth
            Scaler#        = (depth-lp)/float(depth)
            ThisRGB        = cliprange(Scaler#*255,0,255)
            palette(lp) = rgb(ThisRGB,ThisRGB,ThisRGB)
    next
    
    
    BackDrop=NewImage(GetScreenWidth(),GetSCreenHeight())
    rendertoimage BackDrop
        // Draw the shaded backdrop        
        c1=$223344
        c2=c1*2
        shadebox 0,0,getscreenwidth(),getscreenheight(),c1,c1,c2,c2        

        rendertoscreen
    
        
    ;setfps 30
        
    // ------------------------------------------------
    do
    // ------------------------------------------------
    
        drawimage backdrop,0,0,false
        setcursor 0,0
        
        // set PlayBASIC to capture mode for 
        capturetoscene
        clsscene

            tilt#+=0.01
            turn#+=2.3

            RenderGround(Terrain(), WORLDSIZE# )
            RotateVerts(Terrain(),Tilt#,Turn#,Roll#)
            Render(Terrain())
inkmode 1
        drawcamera camera
        
        thisrgb=point(0,0)
        
        Sync
        
    // ------------------------------------------------
    loop spacekey()
    // ------------------------------------------------


    
        explicit true


Function Render(Map().tVertex )

        local Size        =getArrayelements(Map().tVertex,1)    
        local ThisRGB    =point(0,0)

        local sw=getscreenwidth()/2
        local sh=getscreenheight()*0.1
        
        local CLipZ1# = 10
        local xlp,zlp,polygons
        
        local ThisRGB    =$ffffff
        local DepthColour = $223344
        
        local rgb1,rgb2,rgb3,rgb4
        
        for zlp=0 to size-1
            local zlp2=zlp+1

            local x2#    =Map(1,zlp).rx#    
            local y2#    =Map(1,zlp).ry#    
            local z2#    =Map(1,zlp).rz#


            local    x3#    =Map(1,zlp2).rx#    
            local    y3#    =Map(1,zlp2).ry#    
            local    z3#    =Map(1,zlp2).rz#


            local Col1    =map(0,zlp).Colour
            local Col2    =map(1,zlp).Colour
            local Col3    =map(1,zlp2).Colour
            local Col4    =map(0,zlp2).Colour


            for xlp=0 to size-1

                local x1#    =x2#
                local y1#    =y2#
                local z1#    =z2#
                
                local x4#    =x3#
                local y4#    =y3#
                local z4#    =z3#

                local xlp2=xlp+1
                x2#    =Map(xlp2,zlp).rx#    
                y2#    =Map(xlp2,zlp).ry#    
                z2#    =Map(xlp2,zlp).rz#

                x3#    =Map(xlp2,zlp2).rx#    
                y3#    =Map(xlp2,zlp2).ry#    
                z3#    =Map(xlp2,zlp2).rz#

                if (z1#< CLipZ1#) or (z2#< CLipZ1#) then continue
                if (z3#< CLipZ1#) or (z4#< CLipZ1#)  then continue

                local rgb1    =Col2
                local rgb4    =Col3
                
                col2    =map(xlp2,zlp).Colour
                col3    =map(xlp2,zlp2).Colour
            
                local z#=(z1#+z2#+z3#)
                local ShadeRGB = palette((z#+z4#)/4)
                Rgb1=RgbAlphaMult(Rgb1,ShadeRGB)
                Rgb2=RgbAlphaMult(Col2,ShadeRGB)
                Rgb3=RgbAlphaMult(Col3,ShadeRGB)
                Rgb4=RgbAlphaMult(Rgb4,ShadeRGB)
                
                z#=z#/3
                if z#>10
                    capturedepth z#
                    inkmode 1+64
                    linec x1#,y1#,sw,sh,rgb1 and $010203
                    inkmode 1
                    gouraudtri x1#,y1#,rgb1,x2#,y2#,rgb2,x3#,y3#,rgb3
                endif

                z#=(z1#+z3#+z4#)/3
                if z#>10
                    capturedepth z#
                    gouraudtri x1#,y1#,rgb1,x3#,y3#,rgb3,x4#,y4#,rgb4
                endif                

            next xlp

            polygons+=xlp

    next zlp


EndFunction



Function RenderGround(Map().tVertex, WORLDSIZE# )
    
        local Size            =getArrayelements(map())    
        local VertexStep#    =WorldSIZE#/Size
        local CENTER#        =WORLDSIZE#/2
        local xlp,zlp
        local x#,z#
        static baseangle#,scaler#
    

        for zlp=0 to size
            z#=(zlp*VertexStep#)-CENTER#
            for xlp=0 to size
                    x#=(xlp*VertexStep#)-CENTER#

                    local distance#=getdistance2d(0,0,x#,z#)
                    distance#        =distance#/(WorldSIZE#)
                    local Angle#    =(360*Scaler#)*distance#

                    Map(xlp,zlp).x#=x#        
                    Map(xlp,zlp).y#=1000-(sin(baseangle#+angle#)*(1000*(1-Distance#)))
                    Map(xlp,zlp).z#=z#    
                    Map(xlp,zlp).Colour = ((xlp * $321) and $0fff) + ((zlp * $123000))
            next
        next

        Scaler#        =wrapvalue(Scaler#+0.01        ,0,10)
        baseangle#    =wrapvalue(baseangle#+1        ,0,360*10) 

EndFunction

        explicit false



      //  NOTE:  This demo stores the original and rotated
      //        vertex data in a typed array..
      //              See-> Not a good idea     

Function RotateVerts(Map().tVertex,Tilt#,turn#,roll#)

        ProjectionX#=400
        ProjectionY#=400

        cx=GetScreenWidth()/2
        cy=GetScreenHeight()/2
    
    Rem prepare the rotation matrix
        A#=Cos(tilt#):B#=Sin(tilt#)
        C#=Cos(turn#):D#=Sin(turn#)
        E#=Cos(roll#):F#=Sin(roll#)
        AD#=A#*D#
        BD#=B#*D#

    ; Calc Rotation Matrix  
          m11#=C#*E#
        m21#=-1*C#*F#
        m31#=D#
        m12#=BD#*E#+A#*F#
        m22#=-1*BD#*F#+A#*E# 
        m32#=-1*B#*C# 
        m13#=-1*AD#*E#+B#*F# 
        m23#=AD#*F#+B#*E#
        m33#=A#*C# 

        Size=GetArrayElements(Map())

        Static ZoomAngle#
        ZoomAngle#=wrapangle(ZoomAngle#+1)


    Rem rotate all the points using the matrix
    For zlp=0 To Size
        For xlp=0 To Size
            
              pointx#=Map(xlp,zlp).x
              pointy#=Map(xlp,zlp).y
              pointz#=Map(xlp,zlp).z

            rX# = (m11# * pointx#) + (m12# * pointy#) + (m13# * pointz#) 
            rY# = (m21# * pointx#) + (m22# * pointy#) + (m23# * pointz#)
            rZ# = (m31# * pointx#) + (m32# * pointy#) + (m33# * pointz#)
            
            rZ# =rZ#+ 4000 + cos(ZoomAngle#) * 2000
            
            Map(xlp,zlp).rz=rz#
            if (rz#=>10)
                    // Project point
                  Map(xlp,zlp).rx    = cx+ ((rX#*ProjectionX# ) / rz#)
                    Map(xlp,zlp).ry    = cy+ ((rY#*ProjectionY# ) / rz#)
            endif    
        Next 
  Next 
EndFunction