🧪 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.
🌈 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.
🎨 3. Draw the Backdrop
We create a shaded backdrop image using `shadebox`. This gives a nicer visual background behind the terrain.
🔁 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
### 🏔� 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).
* `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.
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.
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
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
🌈 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
🎨 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())
### 🏔� 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#)))
* `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
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 ...
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 ideaFunction 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