PDA

View Full Version : Generating tobogan mesh in ThinBASIC



Petr Schreiber
17-03-2019, 19:50
A friend of mine approached me with a question, how would I model mesh of tobogan.

Normal person would answer "extrude profile along curve in Blender", but coding it is more interesting task, right?

Friend needs it done in Python, but thinBASIC has better built in graphics, so I prototype it in TB.

The project started with generation of tobogan profile, the "U" shape.

How to model profile?
The profile could be approached as arc, part of circle. As circle can be generated via SIN/COS, we will take these helpers to build the part of the mesh.

Some step needs to be decided. As thinBASIC does not allow redim of UDT arrays yet, the step is hardcoded to 16, will update it in future thinBASIC versions to something more tasteful:


uses "math"

type Point3D
x as float32
y as float32
z as float32
end type

type ToboganProfile

point(16) as point3D ' -- Will be converted to dynamic UDT array once TB supports it
point_count as int32

function _create(ellipsis_width as float32, ellipsis_height as float32, arc_angle as float32)

me.point_count = ubound(me.point)

float32 angle

float32 min_angle = degToRad(-arc_angle/2) ' Conversion of degrees to radians is offered by math module
float32 max_angle = degToRad( arc_angle/2)
float32 angle_step= (max_angle - min_angle) / (me.point_count-1)

int32 i
for i = 1 to me.point_count
angle = min_angle + (i-1) * angle_step

me.point(i).x = sin(angle) * ellipsis_width/2
me.point(i).y = -cos(angle) * ellipsis_height/2

me.point(i).y += ellipsis_height/2 ' shift it up to 0,0,0
next

end function

end type


Now we can get list of points from the ToboganProfile. In order to simplify rendering, we can add dedicated function to connect the points of the U profile:


function render()
int32 i

tbgl_beginPoly %GL_LINE_STRIP ' This primitive allows to just supply list of vertices and they will get connected
for i = 1 to me.point_count
tbgl_vertex me.point(i).x, me.point(i).y, me.point(i).z
next
tbgl_endPoly

tbgl_pushStateProtect %TBGL_DEPTH ' Ignoring depth allows to draw over the previously rendered line strip
tbgl_pushColor 255, 0, 0 ' PushColor allows to enable drawing color just until PopColor is called
tbgl_pushPointSize 5 ' Again, limited scope for point size change

tbgl_beginPoly %GL_POINTS
for i = 1 to me.point_count
tbgl_vertex me.point(i).x, me.point(i).y, me.point(i).z
next
tbgl_endPoly

tbgl_popPointSize ' Returning the states back to previous state
tbgl_popColor
tbgl_popStateProtect
end function


Grid with 1x1 cells would come in handy, to see the profile size
We can build a Grid2D type to help us draw grid easily:


type grid2d

top_x as float32
top_y as float32
grid_step as float32

function _create(top_x as float32, top_y as float32, optional grid_step as float32 = 1)
me.top_x = top_x
me.top_y = top_y
me.grid_step = grid_step
end function

function render()
tbgl_pushColor 128, 128, 128
tbgl_line(-me.top_x, 0, me.top_x, 0)
tbgl_line(0, -me.top_y, 0, me.top_y)
tbgl_popColor

float32 i
tbgl_pushColor 64, 64, 64
for i = -me.top_x to me.top_x
tbgl_line(i, -me.top_y, i, me.top_y)
next

for i = -me.top_y to me.top_y
tbgl_line(-me.top_x, i, me.top_x, i)
next
tbgl_popColor

end function

end type


Once we have these helpers, let's draw it to the screen


uses "tbgl", "console"


#include "ToboganProfile.tbasicu"
#include "Grid2d.tbasicu"


function tbMain()

' -- Create and show window
uint32 hWnd = TBGL_createWindowEx("Tobogan profile - press ESC to quit", 512, 512, 32, %TBGL_WS_WINDOWED or %TBGL_WS_CLOSEBOX)
tbgl_showWindow

dim grid as grid2d(10, 10) ' Create new grid
dim profileA as toboganProfile(2, 1, 180) ' Create wide profile, 180°
dim profileB as toboganProfile(1, 1, 270) ' Create profile with 270° U shape

float32 frameRate

' -- Print the calculated points
uint32 i
printl "Profile A" in 15
for i = 1 to profileA.point_count
printl format$(profileA.point(i).x, "#.000"), format$(profileA.point(i).y, "#.000")
next

printl

printl "Profile B" in 15
for i = 1 to profileB.point_count
printl format$(profileB.point(i).x, "#.000"), format$(profileB.point(i).y, "#.000")
next

' -- Main loop
while tbgl_isWindow(hWnd)
FrameRate = tbgl_getFrameRate

tbgl_clearFrame

' Set the camera position
tbgl_camera 0, 0, 5, 0, 0, 0

' Render all, in order dependent fashion
tbgl_pushStateProtect %TBGL_DEPTH
grid.render()

profileA.render()

profileB.render()
tbgl_popStateProtect

tbgl_drawFrame

' -- ESCAPE key to exit application
if tbgl_getWindowKeyState(hWnd, %VK_ESCAPE) then exit while

wend

tbgl_destroyWindow
end function


The result of the drawing can be seen in the attachment. You can also download the complete source code for this phase.


Petr

Petr Schreiber
24-03-2019, 20:20
The original goal of the project is to generate tobogan mesh, so the next step was to organize set of profiles to a collection and generate triangle mesh.

As the original ToboganProfile is always created at the origin of coordinates, we need to add way to place them in the space.

This can be done by adding a simple method to the ToboganProfile type:


function add_pos(x as float32, y as float32, z as float32)
int32 i
for i = 1 to me.point_count
me.point(i).x += x
me.point(i).y += y
me.point(i).z += z
next
end function


Next, we need to manage the multiple profiles somehow. I created a type called ToboganMesh for it:


#include once "ToboganProfile.tbasicu"


type Triangle3D
vertices(3) as Point3D
end type


type ToboganMesh


pProfiles as uint32 ' -- Pointer to profile data in memory
profile_count as uint32 ' -- The total count of the profiles
profile_capacity as uint32 ' -- The capacity of the profile buffer, must be >= as total count of the profiles

point_detail as uint32 ' -- Overall point detail, detected from the first profile added

pTriangles as uint32 ' -- Pointer to triangles in memory
triangle_count as uint32 ' -- The total count of triangles


function _create()
me.profile_count = 0
me.profile_capacity = 8 ' -- Some default value to avoid reallocation after first addition
me.pProfiles = heap_alloc(sizeof(ToboganProfile) * me.profile_capacity)
end function

function add_profile(profile as ToboganProfile)
me.profile_count += 1

if me.profile_count = 1 then
me.point_detail = profile.point_count ' -- The first profile determines the expected detail level for all subsequent profiles
elseif profile.point_count <> me.point_detail then
msgbox 0, "New profile detail does not match existing profile" ' -- Should the detail differ from the existing one, we don't support this operation
stop
end if

' -- Reallocation of memory buffer to bigger size, if needed
if me.profile_count > me.profile_capacity then
me.profile_capacity *= 2
me.pProfiles = heap_realloc(me.pProfiles, sizeof(ToboganProfile) * me.profile_capacity)
end if

' -- Copy the profile binary signature to our buffer
string memory_block = peek$(varptr(profile), sizeof(ToboganProfile))
poke$(me.pProfiles + (me.profile_count-1) * sizeof(ToboganProfile), memory_block)
end function

function build()
if me.profile_count < 2 then exit function ' -- In order to build a mesh, at least 2 profiles are needed

dim profiles(me.profile_count) as ToboganProfile at me.pProfiles ' -- Overlay profile memory

me.triangle_count = (me.profile_count-1) * (me.point_detail-1) * 2
me.pTriangles = heap_alloc(sizeof(Triangle3D) * me.triangle_count)

dim triangles(me.triangle_count) as Triangle3D at me.pTriangles ' -- Overlay triangle memory
int32 index


' Mesh building approach:
'
' PROFILE i PROFILE i+1
' POINT j A----B POINT j
' |\ |
' | \ |
' | \ |
' | \| POINT j+1
' POINT j+1 D----C POINT j+1

int32 i, j
for i = 1 to me.profile_count-1
for j = 1 to me.point_detail-1
index += 1
' A
triangles(index).vertices(1).x = profiles(i).point(j).x
triangles(index).vertices(1).y = profiles(i).point(j).y
triangles(index).vertices(1).z = profiles(i).point(j).z


' B
triangles(index).vertices(2).x = profiles(i+1).point(j).x
triangles(index).vertices(2).y = profiles(i+1).point(j).y
triangles(index).vertices(2).z = profiles(i+1).point(j).z


' C
triangles(index).vertices(3).x = profiles(i+1).point(j+1).x
triangles(index).vertices(3).y = profiles(i+1).point(j+1).y
triangles(index).vertices(3).z = profiles(i+1).point(j+1).z


index += 1
' A
triangles(index).vertices(1).x = profiles(i).point(j).x
triangles(index).vertices(1).y = profiles(i).point(j).y
triangles(index).vertices(1).z = profiles(i).point(j).z


' C
triangles(index).vertices(2).x = profiles(i+1).point(j+1).x
triangles(index).vertices(2).y = profiles(i+1).point(j+1).y
triangles(index).vertices(2).z = profiles(i+1).point(j+1).z


' D
triangles(index).vertices(3).x = profiles(i).point(j+1).x
triangles(index).vertices(3).y = profiles(i).point(j+1).y
triangles(index).vertices(3).z = profiles(i).point(j+1).z
next
next

end function

function render()
if me.triangle_count = 0 then exit function
dim triangles(me.triangle_count) as Triangle3D at me.pTriangles ' -- Overlay array over memory block

int32 i
tbgl_pushLineWidth 2
tbgl_pushPolygonLook %GL_LINE
tbgl_beginpoly %TBGL_TRIANGLES
' -- Render triangle one by one
' -- Each triangle defined via 3 points
for i = 1 to me.triangle_count
tbgl_vertex triangles(i).vertices(1).x, triangles(i).vertices(1).y, triangles(i).vertices(1).z
tbgl_vertex triangles(i).vertices(2).x, triangles(i).vertices(2).y, triangles(i).vertices(2).z
tbgl_vertex triangles(i).vertices(3).x, triangles(i).vertices(3).y, triangles(i).vertices(3).z
next
tbgl_endpoly
tbgl_popPolygonLook
tbgl_popLineWidth
end function

function _destroy()
' -- Free the memory, if necessary
if me.pProfiles then heap_free(me.pProfiles)
if me.pTriangles then heap_free(me.pTriangles)
end function


end type


This object allows us to group profiles together and to build and render mesh for them, just like this:


dim profileA as toboganProfile(2, 1, 180) ' Create wide profile, 180°
dim profileB as toboganProfile(1, 1, 270) ' Create profile with 270° U shape
dim profileC as toboganProfile(2, 1, 200) ' Create wide profile, 200°

' -- Adjust profile position
profileA.add_pos(0, 0, -1)
profileB.add_pos(0, 0, 0)
profileC.add_pos(0, 0, 1)

' -- Create mesh object and add profiles to it
dim mesh as ToboganMesh()
mesh.add_profile(profileA)
mesh.add_profile(profileB)
mesh.add_profile(profileC)

' -- Build the mesh
mesh.build()

' -- Draw the mesh
mesh.render()


The complete code and illustration how it looks attached to this post again.

The next step? Add profile rotation.


Petr

Petr Schreiber
25-04-2019, 15:00
In order to rotate the profiles, we have to dive into the math a bit - to matrix calculus, to be specific.


There are well know matrices for rotation along x, y and z axis, documented for example here, in section Rotation matrices:
https://www.mathworks.com/help/phased/ref/roty.html


The transformation is then applied like:
final_point_column_vector = rotation_matrix * current_point_as_column_vector


ThinBASIC offers native support for matrix calculus, so we can implement these by using the MAT statement:


function rotate_by_matrix(angle_deg as float32, rotation_matrix() as float32)


float32 column_vector_pos(3, 1)
float32 column_vector_result(3, 1)

int32 i
for i = 1 to me.point_count
column_vector_pos(1, 1) = me.point(i).x,
me.point(i).y,
me.point(i).z


mat column_vector_result() = rotation_matrix() * column_vector_pos()

me.point(i).x = column_vector_result(1, 1)
me.point(i).y = column_vector_result(2, 1)
me.point(i).z = column_vector_result(3, 1)
next


end function


function rotate_x(angle_deg as float32)
float32 angle_rad = degToRad(angle_deg)


float32 matrix_rotation_x(3, 3) = [ 1, 0, 0,
0, cos(angle_rad),-sin(angle_rad),
0, sin(angle_rad), cos(angle_rad)]


me.rotate_by_matrix(angle_deg, matrix_rotation_x)
end function


function rotate_y(angle_deg as float32)
float32 angle_rad = degToRad(angle_deg)


float32 matrix_rotation_y(3, 3) = [ cos(angle_rad), 0, sin(angle_rad),
0, 1, 0,
-sin(angle_rad), 0, cos(angle_rad)]


me.rotate_by_matrix(angle_deg, matrix_rotation_y)
end function


function rotate_z(angle_deg as float32)
float32 angle_rad = degToRad(angle_deg)


float32 matrix_rotation_y(3, 3) = [ cos(angle_rad),-sin(angle_rad), 0,
sin(angle_rad), cos(angle_rad), 0,
0, 0, 1]


me.rotate_by_matrix(angle_deg, matrix_rotation_y)
end function



In order to construct the tobogan itself, we might prepare some function. It will take as input:
- mesh to manipulate
- general height of the tobogan
- radius of the turns
- number of turns
- detail level of angular precision




function build_tobogan(byRef mesh as ToboganMesh, height as float32 = 5, radius as float32 = 5, turns as float32 = 1, angle_deg_detail as float32 = 10)
dim main_profile as ToboganProfile(1, 1, 180) ' Create wide profile, 180°
int32 i


float32 angle_rad
for i = 0 to 360*turns step angle_deg_detail
angle_rad = degToRad(i)


main_profile.reset_to(1, 1, 180) ' -- Recycle the same profile, this will reset the transformation to default state again


main_profile.rotate_z(45) ' -- Roll rotation
main_profile.rotate_y(-i) ' -- Left/Right rotation

' -- Placing the profiles along the spiral
main_profile.add_pos(cos(angle_rad) * radius, height-i/(360*turns)*height, sin(angle_rad) * radius)

' -- Appending the profile to mesh
mesh.add_profile(main_profile)
next

' -- Build the mesh
mesh.build()
end function



The complete solution can be downloaded from the attachement.




Petr

ErosOlmi
28-04-2019, 10:02
Great Petr.
Hope to give you very soon dynamic arrays inside UDTs

Petr Schreiber
08-05-2019, 19:30
Thank you Eros,

it will make the code much easier to read :) Yet - being able to demonstrate thinBasic can work around the limitation by custom memory allocation is also cool and not so common in interpreter land :)


Petr