This is just something I thought a) you might like to see, and b) something that might be suitable for a later to-do.
I ported a cool terrain generator, then made it multithreaded, just out of interest, to try and reduce the pauses when generating a new terrain. Weirdly, it worked first try! (Latest code is below, in this 'issue'.)
This takes about a second to do the terrain creation (Model.CreateTerrain), in a background thread, but once the thread completes, there's a shorter jerk of around 300-400 ms.
Is it possible to implement this, or maybe have Async versions of slow operations like this (eg. model loaders), so that your Model reference only gets assigned once the upload's complete?
Code's all in one lump, but jump to OnRender -> If Keyboard.KeyHit (Key.G) and the thread function GenerateTerrain_Threaded ().
PlayfulJSTerrain generates a Pixmap-based heightmap, then GenerateTerrain (called by GenerateTerrain_Threaded) turns it into a Model.
Thought it might be something of interest to look at longer term, anyway. Could help with possible streaming of bigger worlds, etc.
#Import "<std>"
#Import "<mojo>"
#Import "<mojo3d>"
#Import "<thread>"
Using std..
Using mojo..
Using mojo3d..
#Rem
Basic usage to generate 2D PNG heightmap (size must be power of 2):
Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed, pixmap_size, roughness) ' eg. 1, 1024, 0.5
Local heightmap:Pixmap = terrain.RenderPixmap ()
heightmap.Save (DesktopDir () + "\mx2__rendered__heightmap.png")
#End
' -----------------------------------------------------------------------------
' Start of PlayfulJSTerrain
' -----------------------------------------------------------------------------
Class PlayfulJSTerrain
' Hunter Loftis
' http://www.playfuljs.com/realistic-terrain-in-130-lines/
' https://github.com/hunterloftis/playfuljs-demos/blob/gh-pages/terrain/index.html
Field size:Int
Field max:Int
Field map:Float[]
Field roughness:Float
Field upper:Float
Field lower:Float
Method New (seed:ULong, pixmap_size:Int = 1024, in_roughness:Float = 0.5)
Assert (IsPow2 (pixmap_size), "PlayfulJSTerrain.New (pixmap_size) must be power of 2!")
SeedRnd (seed)
roughness = in_roughness
size = pixmap_size + 1 'Pow (2, Log2 (pixmap_size)) + 1
max = size - 1
map = New Float[size * size]
' Local ticks:Int = Millisecs ()
Generate ()
' Print Millisecs () - ticks
' About 15 ms on AMD FX6350 @ 4.2 GHz
End
Method GetHeight:Float (x:Int, y:Int)
If (x < 0 Or x > max Or y < 0 Or y > max) Return -1
Return map[x + size * y]
End
Method SetHeight (x:Int, y:Int, val:Float)
map[x + size * y] = val
If val < lower Then lower = val
If val > upper Then upper = val
End
Method Generate ()
SetHeight (0, 0, max)
SetHeight (max, 0, max * 0.5)
SetHeight (max, max, 0)
SetHeight (0, max, max * 0.5)
Divide (max)
End
Method Divide (size:Int)
Local half:Int = size / 2
If half < 1 Then Return
Local scale:Float = roughness * size
Local scale2:Float = scale * 2 ' Quick pre-calc for the two loops below...
Local x:Int
Local y:Int
For y = half To max Step size
For x = half To max Step size
Square (x, y, half, Rnd () * scale2 - scale)
Next
Next
For y = 0 To max Step half
For x = (y + half) Mod size To max Step size
Diamond (x, y, half, Rnd () * scale2 - scale)
Next
Next
' Recursive call until too small (see "If half < 1")...
Divide (size / 2)
End
Method Square (x:Int, y:Int, size:Int, offset:Float)
' Pre-calc gains ~ 0.5 to 1 ms!
Local xms:Int = x - size
Local yms:Int = y - size
Local xps:Int = x + size
Local yps:Int = y + size
Local average:Float = ( GetHeight (xms, yms) + GetHeight (xps, yms) +
GetHeight (xps, yps) + GetHeight (xms, yps)) * 0.25
SetHeight (x, y, average + offset)
End
Method Diamond (x:Int, y:Int, size:Int, offset:Float)
' No pre-calc, nothing repeated...
Local average:Float = ( GetHeight (x, y - size) + GetHeight (x + size, y) +
GetHeight (x, y + size) + GetHeight (x - size, y)) * 0.25
SetHeight (x, y, average + offset)
End
Method RenderPixmap:Pixmap ()
Local pix:Pixmap = New Pixmap (max, max, PixelFormat.I8)
Local rgb:Float
Local color:Color = New Color (0.0, 0.0, 0.0)
' Local ticks:Int = Millisecs ()
For Local y:Int = 0 Until max
For Local x:Int = 0 Until max
If (upper - lower) = 0 ' Make sure range is valid!
rgb = 0.5 ' TODO: Not sure what this should be...
Else
rgb = TransformRange (GetHeight (x, y), lower, upper, 0.0, 1.0)
Endif
' Let's not keep New'ing in heavy loop...
color.R = rgb
color.G = rgb
color.B = rgb
pix.SetPixel (x, y, color)
Next
Next
' Print Millisecs () - ticks ' Around 20 ms
Return pix
End
Method TransformRange:Float (input_value:Float, from_min:Float, from_max:Float, to_min:Float, to_max:Float)
' Algorithm via jerryjvl at https://stackoverflow.com/questions/929103/convert-a-number-range-to-another-range-maintaining-ratio
Local from_delta:Float = from_max - from_min ' Input range, eg. 0.0 - 1.0
Local to_delta:Float = to_max - to_min ' Output range, eg. 5.0 - 10.0
Assert (from_delta <> 0.0, "TransformRange: Invalid input range!")
Return (((input_value - from_min) * to_delta) / from_delta) + to_min
End
Method IsPow2:Long (value:Long)
Return Not (value & (value - 1))
End
End
' -----------------------------------------------------------------------------
' End of PlayfulJSTerrain
' -----------------------------------------------------------------------------
' Helper function to generate checkerboard Pixmap...
Function CheckerPixmap:Pixmap (color0:Color, color1:Color)
Local pixels:Pixmap = New Pixmap (256, 256, PixelFormat.RGBA8)
pixels.Clear (color0)
Local pixel_toggle:Int = 0
For Local gp_y:Int = 0 Until pixels.Height
For Local gp_x:Int = 0 Until pixels.Width
If pixel_toggle Then pixels.SetPixel (gp_x, gp_y, color1)
pixel_toggle = 1 - pixel_toggle
Next
pixel_toggle = 1 - pixel_toggle
Next
Return pixels
End
' Demo...
Class MyWindow Extends Window
Field scene:Scene
Field camera:Camera
Field light:Light
Field seed:Int = 0
Field turn:Bool = True
Field ground:Model
Field ground_thread:Thread
Field render_timer:Int
Method New( title:String="PlayfulJSTerrain to Monkey2!",width:Int=1024,height:Int=768,flags:WindowFlags=WindowFlags.Center )
' Method New( title:String="PlayfulJSTerrain to Monkey2!",width:Int=App.DesktopSize.X,height:Int=App.DesktopSize.Y,flags:WindowFlags=WindowFlags.Fullscreen )
Super.New( title,width,height,flags )
End
Method GenerateTerrain_Threaded ()
Local ticks:Int = Millisecs ()
Local tg:Model = GenerateTerrain (seed, 1024)
ground?.Destroy ()
ground = tg
Print "Terrain rendering thread took " + (Millisecs () - ticks) + " ms"
' About a second...
End
Method OnCreateWindow() Override
scene=New Scene
scene.ClearColor = Color.Sky
scene.FogColor = Color.Sky
scene.FogNear = 256
scene.FogFar = 1024
camera=New Camera( Self )
camera.AddComponent<FlyBehaviour>()
camera.Move( 0,300,0 )
camera.Far = 1024.0
camera.Rotate (45, 0, 0)
light=New Light
light.CastsShadow=True
light.Move (-512, 256, 0)
' GenerateTerrain returns a mojo3d Model...
'ground = GenerateTerrain (seed, 1024)
ground_thread = New Thread (GenerateTerrain_Threaded)
Mouse.PointerVisible = False
render_timer = Millisecs ()
End
Method OnRender( canvas:Canvas ) Override
Local render_time:Int = Millisecs () - render_timer
render_timer = Millisecs ()
If render_time > 17
Print "Rendering exceeded 17 ms, taking " + render_time + " ms"
Endif
If turn Then ground?.Rotate (0.0, 0.125, 0.0)
If Keyboard.KeyHit (Key.Escape) Then App.Terminate ()
If Keyboard.KeyHit (Key.T) Then turn = Not turn ' Toggle turn variable on/off...
If Keyboard.KeyHit (Key.G)
' One at a time, please...
If Not ground_thread.Running
seed = seed + 1
ground_thread = New Thread (GenerateTerrain_Threaded)
Endif
Endif
' Hitting S demonstrates simple non-3D usage to generate a heightmap...
If Keyboard.KeyHit (Key.S)
' Create a terrain with same details as 3D one...
Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed) ' Defaults to size = 1024 x 1024, roughness = 0.5
' Render to Pixmap and save to desktop...
Local heightmap:Pixmap = terrain.RenderPixmap ()
heightmap.Save (DesktopDir () + "\mx2__rendered__heightmap.png")
Endif
RequestRender()
scene.Update()
camera.Render( canvas )
canvas.DrawText( "Seed=" + seed + ", FPS="+App.FPS,20,20 )
canvas.DrawText( "G to generate new terrain",20,40 )
canvas.DrawText( "S to save heightmap to desktop",20,60 )
canvas.DrawText( "T to toggle rotation",20,80 )
canvas.DrawText( "Cursors + A/Z to move (slowly!)",20,100 )
canvas.DrawText( "Esc to exit",20,140 )
If ground_thread.Running
canvas.DrawText( "Generating new terrain...",20,180 )
Endif
End
End
' For demo only...
Function GenerateTerrain:Model (seed:ULong, size:Int = 1024, roughness:Float = 0.5, color0:Color = New Color (0.8, 0.5, 0.1), color1:Color = New Color (0.75, 0.35, 0.05))
Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed, size, roughness)
Local heightmap:Pixmap = terrain.RenderPixmap ()
If Not heightmap Then RuntimeError ("Failed to generate heightmap!")
heightmap.FlipY () ' 2D Y (increases downwards) translates to 3D Z (increases upwards)
Local terrain_material:PbrMaterial = New PbrMaterial ()
terrain_material.ColorTexture = New Texture (CheckerPixmap (color0, color1), TextureFlags.None)
Local terrain_height:Float = 256.0 ' TODO!
Local height_box:Boxf = New Boxf (-heightmap.Width * 0.5, 0.0, -heightmap.Height * 0.5, heightmap.Width * 0.5, terrain_height, heightmap.Height * 0.5)
Return Model.CreateTerrain (heightmap, height_box, New PbrMaterial (terrain_material))
End
Function Main()
New AppInstance
New MyWindow
App.Run()
End
Original Author: DruggedBunny
Hi Mark,
This is just something I thought a) you might like to see, and b) something that might be suitable for a later to-do.
I ported a cool terrain generator, then made it multithreaded, just out of interest, to try and reduce the pauses when generating a new terrain. Weirdly, it worked first try! (Latest code is below, in this 'issue'.)
This takes about a second to do the terrain creation (Model.CreateTerrain), in a background thread, but once the thread completes, there's a shorter jerk of around 300-400 ms.
My best guess would be that this is where the main thread uploads the data to the graphics card, and so I looked it up, and it seems it might be possible to upload the data on a separate thread.
Is it possible to implement this, or maybe have Async versions of slow operations like this (eg. model loaders), so that your Model reference only gets assigned once the upload's complete?
Code's all in one lump, but jump to OnRender ->
If Keyboard.KeyHit (Key.G)
and the thread functionGenerateTerrain_Threaded ()
.PlayfulJSTerrain generates a Pixmap-based heightmap, then GenerateTerrain (called by GenerateTerrain_Threaded) turns it into a Model.
Thought it might be something of interest to look at longer term, anyway. Could help with possible streaming of bigger worlds, etc.