Tutorial - 3D models
Level: Intermediate
Time: 30 min
Prerequisites:
- You have an account and completed the installation process. No account? Get one here
- You have some experience with reading Python code
Introductionβ
As a designer or engineer, creating 3D models is an essential part of your work. 3D models can help you visualize and communicate complex concepts in a way that is easy to understand. In this tutorial, we will explore how to create 3D models in VIKTOR using Python.
By the end of this tutorial, you will have the skills and knowledge needed to create your own 3D models in VIKTOR using Python. We will start with simple shapes and gradually work our way up to more complex models. Whether you are a beginner or an experienced designer, this tutorial will provide you with a solid foundation in 3D modeling with VIKTOR.
This tutorial consists of six different sections:
- Create an empty app
- Set up the basis
- Drawing the bridge deck
- Adding supports
- The finishing touches
- The complete app code
To give you an idea of what you will be creating, here's a GIF of the application you will me making throughout this tutorial:
What the app looks like once you complete this tutorial
Are you encountering an error? Take a look at the complete app code.
Also know that you can always ask for help at our Community Forum, where our developers are ready to help with any question related to the installation, coding and more.
1. Create, install and start an empty appβ
Let's create, install and start an empty app. This will be the starting point for the rest of the tutorial.
But before we start, make sure to shut down any app that is running (like the demo app) by closing the command-line shell
(for example Powershell) or end the process using Ctrl + C
.
Follow these steps to create, install and start an empty app:
- Go to the App store in your VIKTOR environment to create a new app. After clicking 'Create app' choose the option 'Create blank app' and enter a name and description of your choice. Submit the form by clicking 'Create and setup'.
- Select 'Editor' as app type and click 'Next'.
- Now follow the instructions to run the
quickstart
command to download the empty app template. After entering the command click 'I have run the command' to continue. The CLI will ask you to select your code editor of choice. Use the arrows and press enter to select a code editor. The app will now open in your code editor of choice.
If all went well, your empty app is installed and connected to your development workspace. Do not close the terminal as this will break the connection with your app. The terminal in your code editor should show something like this:
INFO : Connecting to cloud.viktor.ai...
INFO : Connection is established:
INFO :
INFO : https://cloud.viktor.ai/workspaces/XXX/app <--- navigate here to find your app
INFO :
INFO : The connection can be closed using Ctrl+C
INFO : App is ready
- You only need create an app template and install it once for each new app you want to make.
- The app will update automatically once you start adding code in
app.py
, as long as you don't close the terminal or your code editor. - Did you close your code editor? Use
viktor-cli start
to start the app again. No need to install, clear, etc.
Did you encounter any errors?
-
Always make sure to check the spelling of everything you placed in the command-line, a small mistake and the command you are trying to run may not be recognised!
-
If you are encountering:
ERROR:
Exiting because of an error: no requirements.txt file
PS C:\Users\<username>\viktor-apps>Then you are not in the correct folder! check the command-line and navigate to the 3D-model-tutorial folder.
-
If you are encountering:
Error: App definition is not compatible with the data currently stored in the database. Use the command 'viktor-cli clear' to clear the database.
PS C:\Users\<username>\viktor-apps\3D-model-tutorial>That means you have not cleared the database yet! Use the
viktor-cli clear
to clear and then you can useviktor-cli start
to start the app. No need to install it again!
Not seeing any of these errors? Head over to our community! There is a good chance another developer encountered it and solved it too!
2. Set up the basisβ
In this tutorial we will make a 3D model. In this example case you will be modelling a bridge including a simplified version of the river running underneath it, the sloped embankment on the side of the river and the supports underneath the bridge. π
Let's start easy and create a simple bridge deck and a river running underneath it, and some input fields to change the dimensions.
Input fieldsβ
We will add 4 inputfields to define the dimensions of our bridge, our app: length
, width
, height
and deck_thickness
.
We will use a Numberfield
, for each.
-
Open
app.py
, and add the relevant fields to your parametrization. It is always good to give people some context usingText
elements. Don't forget to import the necessary fields. In the end yourapp.py
file should look like this:import viktor as vkt
class Parametrization(vkt.Parametrization):
intro = vkt.Text("# 3D model app π\n This app parametrically generates and visualises a 3D model of a bridge")
txt_deck = vkt.Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = vkt.NumberField("Deck length", min=6, default=100, suffix='m')
width = vkt.NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = vkt.NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = vkt.NumberField("Bridge height", min=2, default=10, suffix='m')
class Controller(vkt.Controller):
parametrization = Parametrization -
Refresh your app, and you should see the input fields there.
Basic set up & the riverβ
We donβt need a bridge if there is no river. So, letβs start with making the river step by step and set up the basis for the next parts, so we can explain some concepts.
-
We want to show a 3D model, so we will create a
GeometryView
inside yourcontroller
like this:class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.GeometryView("3D", x_axis_to_right=True)
def visualize_bridge(self, params, **kwargs):
return vkt.GeometryResult(geometry_group) -
Inside
visualize_bridge
, we'll create a emptyGroup
to store all elements we want to show:# Create an empty group to hold all the geometry
geometry_group = vkt.Group([]) -
Now we'll define the measurements of the river and bridge's surroundings. We donβt want to make new input field for that, so we will just use the bridge measurements for this. Inside
visualize_bridge
:# Measurements environment
env_width = params.length
env_thick = params.deck_thickness # Thickness of water and embankment
water_width = params.length - 2 * params.height # Assuming embankment at 45 deg -
Then, create a
Material
so we can make the water blue, and let's make it a bit transparent. Insidevisualize_bridge
:# Create materials
mat_water = vkt.Material('Water', color=vkt.Color.blue(), opacity=0.7) -
Next, create the water by drawing a box is using
SquareBeam
and add it to theGroup
we made before. Insidevisualize_bridge
add:# Draw water
water = vkt.SquareBeam(water_width, env_width, env_thick, material=mat_water) -
The last step is return the geometry, so it is visualized:
geometry_group.add([water])
-
Update your app, and you will see the river
All code until nowβ
Click here to see all code until now
import viktor as vkt
class Parametrization(vkt.Parametrization):
intro = vkt.Text("# 3D model app π\n This app parametrically generates and visualises a 3D model of a bridge")
txt_deck = vkt.Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = vkt.NumberField("Deck length", min=6, default=100, suffix='m')
width = vkt.NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = vkt.NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = vkt.NumberField("Bridge height", min=2, default=10, suffix='m')
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.GeometryView("3D", x_axis_to_right=True)
def visualize_bridge(self, params, **kwargs):
# Create an empty group to hold all the geometry
geometry_group = vkt.Group([])
# Measurements environment
env_width = params.length
env_thick = params.deck_thickness # Thickness of water and embankment
water_width = params.length - 2 * params.height # Assuming embankment at 45 deg
# Create materials
mat_water = vkt.Material('Water', color=vkt.Color.blue(), opacity=0.7)
# Draw water
water = vkt.SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])
return vkt.GeometryResult(geometry_group)
3. Drawing the bridge deckβ
Now that we have the basis for drawing our 3D models, and a beautiful river π, we can quickly create the bridge:
Adding the deckβ
We'll create the bridge inside the GeometryView
, like we did before.
-
Create the deck, using the
SquareBeam
function as well, and give the deck a width, length and thickness.deck = vkt.SquareBeam(params.length, params.width, params.deck_thickness)
-
SquareBeam
creates a box with its centre at the origin (0, 0, 0). In order to move the deck to the correct position let's usetranslate
deck.translate((0, 0, params.height))
-
Now we will create the deck top, you can interpret this top as the asphalt layer of the bridge. But first, let's define a new material. This time we'll define the
Color
using RGB values:mat_deck_top = vkt.Material('Deck top', color=vkt.Color(60, 60, 60))
-
Again, use
SquareBeam
andtranslate
it in the z-direction:deck_top = vkt.SquareBeam(params.length, params.width, params.deck_thickness / 4, material=mat_deck_top)
deck_top.translate((0, 0, params.height + 5/8*params.deck_thickness)) -
Add the created elements to the geometry
Group
so they get visualized:geometry_group.add([deck, deck_top])
-
Refresh your application, you should see the water and the bridge in 3D view.
5. Adding supportsβ
That's a really beautiful bridge, but something feels wrong, it looks like the bridge is flying, so we are now going to generate some supports for it, parametrically of course.
The planβ
Before going into the code, let's discuss the plan. We will make a V-shaped support and place it on several locations according to a 'grid', as shown here:
Input fieldsβ
With that said, let's jump into the code.
-
We will add 2 input fields to our app:
support_amount
,support_piles_amount
. These inputs will define how many supports we want. Inside theparametrization
class add the following three lines of code:txt_support = vkt.Text('### Support layout', flex=100)
support_amount = vkt.NumberField("Number of supports", default=2, min=1)
support_piles_amount = vkt.NumberField("Number of piles per support", min=2, default=3) -
Refresh your app and you should see the new input fields.
Create one supportβ
Next let's make the bridge supports. The supports each consist of two columns in a V-formation placed on a support box.
As before, we will draw the different element inside the GeometryView
-
Inside your
GeometryView
before thereturn
, we will define the pile diameter and the grid size. Note that we did not create an input field to define the diameter (but feel free), so we will make it equal to Β½ of the deck thickness:pile_diameter = params.deck_thickness / 2
grid_size_x = water_width / params.support_amount
grid_size_y = (params.width - pile_diameter) / (params.support_piles_amount - 1) -
Draw one column and use
mirror_object
to get the V-shape. We draw the column by extruding a circle along a line usingCircularExtrusion
:# Draw columns
top_point = vkt.Point(-grid_size_x/2, 0, params.height)
bottom_point = vkt.Point(0, 0, 0)
column_line = vkt.Line(top_point, bottom_point)
column_1 = vkt.CircularExtrusion(pile_diameter, column_line)
column_2 = vkt.geometry.mirror_object(column_1, vkt.Point(0, 0, 0), (1, 0, 0)) -
Let's now add the box at the base to complete our support:
# Draw base
column_base = vkt.SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter) -
For the next step, it is convenient to put the 3 elements we just made in a new
Group
:support = vkt.Group([column_1, column_2, column_base])
-
Let's see our progress by temporarily adding the
support
to ourgeometry_group
and updating your app:geometry_group.add([support])
-
Before continuing, eliminate the line of code we added in the last step.
Repeating the supportsβ
We will now use the support we draw in the last section and repeat it according to our grid. As before, we will continue
to work inside our GeometryView
.
When we hear repeating, our Python minds immediately think in a for loop. But in VIKTOR, we do not use a for loop for
making repetitive geometries. This will make you app unnecessary slow. For this Pattern
, LinearPattern
, and
BidirectionalPattern
are better suited. This can reduce the loading time from minutes to seconds in larger models.
-
First, place the
support
in the corner of the grid usingtranslate
:support.translate((-water_width/2 + grid_size_x/2, -params.width/2 + pile_diameter/2, 0))
-
Now, we will copy the support using
BidirectionalPattern
. While this may seem like a difficult function at first, the implementation is surprisingly easy:
all_supports = vkt.BidirectionalPattern(
base_object=support,
direction_1=[1, 0, 0],
direction_2=[0, 1, 0],
number_of_elements_1=params.support_amount,
number_of_elements_2=params.support_piles_amount,
spacing_1=grid_size_x,
spacing_2=grid_size_y
)
-
Let's add
all_supports
togeometry_group
so we visualize them# Adding geometry group to geometry result
geometry_group.add(all_supports) -
Refresh your app, and look how amazing your bridge is looking!
All code until nowβ
Click here to see all code until now
import viktor as vkt
class Parametrization(vkt.Parametrization):
intro = vkt.Text("# 3D model app π\n This app parametrically generates and visualises a 3D model of a bridge")
txt_deck = vkt.Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = vkt.NumberField("Deck length", min=6, default=100, suffix='m')
width = vkt.NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = vkt.NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = vkt.NumberField("Bridge height", min=2, default=10, suffix='m')
txt_support = vkt.Text('### Support layout', flex=100)
support_amount = vkt.NumberField("Number of supports", default=2, min=1)
support_piles_amount = vkt.NumberField("Number of piles per support", min=2, default=3)
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.GeometryView("3D", x_axis_to_right=True)
def visualize_bridge(self, params, **kwargs):
# Create an empty group to hold all the geometry
geometry_group = vkt.Group([])
# ===============================
# Draw Environment
# ===============================
# Measurements environment
env_width = params.length
env_thick = params.deck_thickness # Thickness of water and embankment
water_width = params.length - 2 * params.height # Assuming embankment at 45 deg
# Create materials
mat_water = vkt.Material('Water', color=vkt.Color.blue(), opacity=0.7)
mat_deck_top = vkt.Material('Deck top', color=vkt.Color(60, 60, 60))
# Draw water
water = vkt.SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])
# ===============================
# Create Bridge
# ===============================
# Draw deck
deck = vkt.SquareBeam(params.length, params.width, params.deck_thickness)
deck.translate((0, 0, params.height))
deck_top = vkt.SquareBeam(params.length, params.width, params.deck_thickness / 4, material=mat_deck_top)
deck_top.translate((0, 0, params.height + 5/8*params.deck_thickness))
geometry_group.add([deck, deck_top])
# Define grid
pile_diameter = params.deck_thickness / 2
grid_size_x = water_width / params.support_amount
grid_size_y = (params.width - pile_diameter) / (params.support_piles_amount - 1)
# Draw columns
top_point = vkt.Point(-grid_size_x/2, 0, params.height)
bottom_point = vkt.Point(0, 0, 0)
column_line = vkt.Line(top_point, bottom_point)
column_1 = vkt.CircularExtrusion(pile_diameter, column_line)
column_2 = vkt.geometry.mirror_object(column_1, vkt.Point(0, 0, 0), (1, 0, 0))
# Draw base
column_base = vkt.SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter)
# Pattern supports
support = vkt.Group([column_1, column_2, column_base])
support.translate((-water_width/2 + grid_size_x/2, -params.width/2 + pile_diameter/2, 0))
all_supports = vkt.BidirectionalPattern(
base_object=support,
direction_1=[1, 0, 0],
direction_2=[0, 1, 0],
number_of_elements_1=params.support_amount,
number_of_elements_2=params.support_piles_amount,
spacing_1=grid_size_x,
spacing_2=grid_size_y
)
# Adding geometry group to geometry result
geometry_group.add(all_supports)
return vkt.GeometryResult(geometry_group)
5. The finishing touchesβ
You already made an amazing parametric bridge and learned the basics of creating 3D models in VIKTOR. In this chapter we just want to show you some little extras that can be useful during your app development journey.
Drawing the embankmentβ
We already learned that you can move things using translate
. This is because everything you draw is a TransformableObject
.
Besides translate
you can also use other transformations, like rotate
,
mirror
and scale
.
Letβs give this a try drawing the embankments.
-
In your
GeometryView
, let's add a newMaterial
:mat_grass = vkt.Material('Grass', color=vkt.Color.green())
-
Then, we will create,
rotate
, andtranslate
the embankment, and make a copy of it :# Draw embankment (assuming embankment at 45 deg)
embankment_1 = vkt.SquareBeam(1.4 * params.height, env_width, env_thick, material=mat_grass)
embankment_1.rotate(-math.pi / 4, direction=[0, 1, 0])
embankment_1.translate(((water_width+params.height)/2, 0, params.height/2))
embankment_2 = vkt.geometry.mirror_object(embankment_1, vkt.Point(0, 0, 0), (1, 0, 0)) -
As allways, add the objects to
geometry_group
:geometry_group.add([embankment_1, embankment_2])
-
Don't forget to import:
import math
-
Reload you app and see what happened
A colorful bridgeβ
Until now, we did not give the bridge a color, which by is shown grey by default. We already learned that you can make a
Material
and give it a Color
, but did you know that we can ask the user to give it a color too?
To demonstrate this, letβs:
- Create an ColorField:
bridge_color = vkt.ColorField('Bridge Color', default=vkt.Color(200, 200, 200))
- Create a material
mat_bridge = vkt.Material('Bridge', color=params.bridge_color)
-
Assign this material to any (or all) of
deck
,column_1
andcolumn_base
. By addingmaterial=mat_bridge
to theSquareBeam
andCircularExtrusion
functions that generate these elements. -
Reload and play with you app
Complete app codeβ
Were you able to do everything in this tutorial without error? If not, you can always look at the full code:
Complete code
import viktor as vkt
import math
class Parametrization(vkt.Parametrization):
intro = vkt.Text("# 3D model app π\n This app parametrically generates and visualises a 3D model of a bridge")
txt_deck = vkt.Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = vkt.NumberField("Deck length", min=6, default=100, suffix='m')
width = vkt.NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = vkt.NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = vkt.NumberField("Bridge height", min=2, default=10, suffix='m')
txt_support = vkt.Text('### Support layout', flex=100)
support_amount = vkt.NumberField("Number of supports", default=2, min=1)
support_piles_amount = vkt.NumberField("Number of piles per support", min=2, default=3)
bridge_color = vkt.ColorField('Bridge Color', default=vkt.Color(200, 200, 200))
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.GeometryView("3D", x_axis_to_right=True)
def visualize_bridge(self, params, **kwargs):
# Create an empty group to hold all the geometry
geometry_group = vkt.Group([])
# Measurements environment
env_width = params.length
env_thick = params.deck_thickness # Thickness of water and embankment
water_width = params.length - 2 * params.height # Assuming embankment at 45 deg
# Create materials
mat_water = vkt.Material('Water', color=vkt.Color.blue(), opacity=0.7)
mat_deck_top = vkt.Material('Deck top', color=vkt.Color(60, 60, 60))
mat_grass = vkt.Material('Grass', color=vkt.Color.green())
mat_bridge = vkt.Material('Bridge', color=params.bridge_color)
# ===============================
# Draw environment
# ===============================
# Draw water
water = vkt.SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])
# Draw embankment (at 45 deg)
embankment_1 = vkt.SquareBeam(1.4 * params.height, env_width, env_thick, material=mat_grass)
embankment_1.rotate(-math.pi / 4, direction=[0, 1, 0])
embankment_1.translate(((water_width+params.height)/2, 0, params.height/2))
embankment_2 = vkt.geometry.mirror_object(embankment_1, vkt.Point(0, 0, 0), (1, 0, 0))
# Add created geometries to geometry group
geometry_group.add([embankment_1, embankment_2])
# ===============================
# Create Bridge
# ===============================
# Draw deck
deck = vkt.SquareBeam(params.length, params.width, params.deck_thickness, material=mat_bridge)
deck.translate((0, 0, params.height))
deck_top = vkt.SquareBeam(params.length, params.width, params.deck_thickness / 4, material=mat_deck_top)
deck_top.translate((0, 0, params.height + 5/8*params.deck_thickness))
geometry_group.add([deck, deck_top])
# Define grid
pile_diameter = params.deck_thickness / 2
grid_size_x = water_width / params.support_amount
grid_size_y = (params.width - pile_diameter) / (params.support_piles_amount - 1)
# Draw columns
top_point = vkt.Point(-grid_size_x/2, 0, params.height)
bottom_point = vkt.Point(0, 0, 0)
column_line = vkt.Line(top_point, bottom_point)
column_1 = vkt.CircularExtrusion(pile_diameter, column_line, material=mat_bridge)
column_2 = vkt.geometry.mirror_object(column_1, vkt.Point(0, 0, 0), (1, 0, 0))
# Draw base
column_base = vkt.SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter, material=mat_bridge)
support = vkt.Group([column_1, column_2, column_base])
support.translate((-water_width/2 + grid_size_x/2, -params.width/2 + pile_diameter/2, 0))
all_supports = vkt.BidirectionalPattern(
base_object=support,
direction_1=[1, 0, 0],
direction_2=[0, 1, 0],
number_of_elements_1=params.support_amount,
number_of_elements_2=params.support_piles_amount,
spacing_1=grid_size_x,
spacing_2=grid_size_y
)
# Adding geometry group to geometry result
geometry_group.add(all_supports)
return vkt.GeometryResult(geometry_group)
Want to learn how VIKTOR works?β
If you are interested in how VIKTOR works behind the scenes, for example how it processes your input, expand the tabs below!
How does it work?β
How does the Parametrization work?
In the Parameterization class you can add input fields that allow the user to provide input to your app, and there are more than 20 different input fields you can use, including numbers, text, colors, images and files.
Inside the Parametrization class, you can also format the layout of your app by adding sections, tabs, steps and pages.
To show your Parametrization in the app, we need to add the line parametrization = Parametrization
inside the
Controller
class, because it is the controller that determines what is shown and not.
How does the Parametrization get saved?
So you may be wondering, how do you get the information from the parametrization to my controller?
Well, we do this automatically for you. The values of all parameters are stored in a single variable called params
, which is accessible inside the Controller class.
These variables are stored in a Munch
; this is similar to a dictionary, but work with point denotation.
Example:
- Let's say we have a variable called
height
as a NumberField in ourParameterization
. - To use it in a method in the
Controller
, define it as:def my_method(self, params, **kwargs)
- You can now make calculations inside that method using our height parameter as
params.height
!
How does the Controller work?
The Controller class is the place where you add everything you want to calculate and show.
As explained in this tutorial, we show results in a View
and we always add views in our controller.
You can even add several views in a single app by adding them to the controller class... and yes, we have
many Views,for showing graphs, maps, 3D models, reports, images and more.
In the Controller, you also do or call your calculation. Remember that the user input given in the parametrization,
is accessible inside the Controller class in the variable The params
.
To infinity and beyond!β
Well done! You are now able to create an app that can parametrically design a 3D model which includes rectangular as well as circular extrusions with mirror, rotate and pattern possibilities!
Obviously there is way more to discover about VIKTOR and its 3D modelling capabilities. Check those out in our documentation
Finally, the journey doesn't end here. Check out some of our other tutorials or go to the next section where you can see the different paths you can follow in your journey to learn more about VIKTOR.