Skip to main content

Tutorial - 3D models

note

Time: 30 minutes
Level: Beginner

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:

  1. Create an empty app
  2. Set up the basis
  3. Drawing the bridge deck
  4. Adding supports
  5. The finishing touches
  6. 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

So, let's get started and learn how to create amazing 3D models in VIKTOR using Python!

tip

You can find the complete code below. We will also add the code at the end of each chapter.

Pre-requisites

Prerequisites

During the tutorial, we added some links to additional information; but don't let them distract you too much. Stay focused on completing the tutorial. After this, you will know everything you need to create an app which includes 3D modelling functionalities.

1. Create an empty app

Let’s first create and start an empty app. If you don't remember how this worked you can check out the first few steps of the Create your first app tutorial. Make sure to give your app a recognisable name, like "3d-models-tutorial".

Here a short summary of the process, in your terminal:

# Create an empty editor-type app
> viktor-cli create-app 3d-models-tutorial --app-type editor
# Navigate to the app directory and install the app
> cd 3d-models-tutorial
> viktor-cli install
# Clear your database just to be sure
> viktor-cli clear
# And start the app!
> viktor-cli start
caution

Please be aware that the create-app function sets the newly created app to use the latest SDK-version While this tutorial was written using SDK version 14.0.0.

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.

  1. Open app.py, and add the relevant fields to your parametrization. It is always good to give people some context using Text elements. Don't forget to import the necessary fields. In the end your app.py file should look like this:

    from viktor import ViktorController
    from viktor.parametrization import ViktorParametrization, NumberField, Text

    class Parametrization(ViktorParametrization):
    intro = Text("# 3D model app 🌉\n This app parametrically generates and visualises a 3D model of a bridge")

    txt_deck = Text('### Deck layout ') # texts can be used for explanation in the user-interface
    length = NumberField("Deck length", min=6, default=100, suffix='m')
    width = NumberField("Deck width", min=2, default=20, suffix='m')
    deck_thickness = NumberField("Deck thickness", min=0.5, default=1, suffix='m')
    height = NumberField("Bridge height", min=2, default=10, suffix='m')

    class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization
  2. 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.

  1. We want to show a 3D model, so we will create a GeometryView inside your controller like this:

    class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization

    @GeometryView("3D", duration_guess=1)
    def visualize_bridge(self, params, **kwargs):

    return GeometryResult(geometry_group)
  2. Inside visualize_bridge, we'll create a empty Group to store all elements we want to show:

    # Create an empty group to hold all the geometry
    geometry_group = Group([])
  3. 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
  4. Then, create a Material so we can make the water blue, and let's make it a bit transparent. Inside visualize_bridge:

    # Create materials
    mat_water = Material('Water', color=Color.blue(), threejs_opacity=0.7)
  5. Next, create the water by drawing a box is using SquareBeam and add it to the Group we made before. Inside visualize_bridge add:

    # Draw water
    water = SquareBeam(water_width, env_width, env_thick, material=mat_water)
  6. The last step is return the geometry, so it is visualized:

    geometry_group.add([water])
  7. Don't forget to import all the necessary parts at the beginning of your code:

    from viktor import Color
    from viktor.views import GeometryView, GeometryResult
    from viktor.geometry import Group, Material, SquareBeam
  8. Update your app, and you will see the river

All code until now

Click here to see all code until now

from viktor import ViktorController, Color
from viktor.parametrization import ViktorParametrization, Text, NumberField
from viktor.views import GeometryView, GeometryResult
from viktor.geometry import Group, Material, SquareBeam


class Parametrization(ViktorParametrization):
intro = Text("# 3D model app 🌉\n This app parametrically generates and visualises a 3D model of a bridge")

txt_deck = Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = NumberField("Deck length", min=6, default=100, suffix='m')
width = NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = NumberField("Bridge height", min=2, default=10, suffix='m')

class Controller(ViktorController):
label = 'My Entity Type'
parametrization = Parametrization

@GeometryView("3D", duration_guess=1)
def visualize_bridge(self, params, **kwargs):

# Create an empty group to hold all the geometry
geometry_group = 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 = Material('Water', color=Color.blue(), threejs_opacity=0.7)

# Draw water
water = SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])

return 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.

  1. Create the deck, using the SquareBeam function as well, and give the deck a width, length and thickness.

    deck = SquareBeam(params.length, params.width, params.deck_thickness)
  2. 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 use translate

    deck.translate((0, 0, params.height))
  3. 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 = Material('Deck top', color=Color(60, 60, 60))
  4. Again, use SquareBeam and translate it in the z-direction:

    deck_top = 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))
  5. Add the created elements to the geometry Group so they get visualized:

    geometry_group.add([deck, deck_top])
  6. Refresh your application, you should see the water and the bridge in 3D view.

4. 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.

  1. We will add 2 input fields to our app: support_amount, support_piles_amount. These inputs will define how many supports we want. Inside the parametrization class add the following three lines of code:

     txt_support = Text('### Support layout', flex=100)
    support_amount = NumberField("Number of supports", default=2, min=1)
    support_piles_amount = NumberField("Number of piles per support", min=2, default=3)
  2. 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

  1. Inside your GeometryView before the return, 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)
  2. Draw one column and use mirror_object to get the V-shape. We draw the column by extruding a circle along a line using CircularExtrusion:

    # Draw columns

    top_point = Point(-grid_size_x/2, 0, params.height)
    bottom_point = Point(0, 0, 0)
    column_line = Line(top_point, bottom_point)

    column_1 = CircularExtrusion(pile_diameter, column_line)
    column_2 = mirror_object(column_1, Point(0, 0, 0), (1, 0, 0))
  3. Let's now add the box at the base to complete our support:

    # Draw base

    column_base = SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter)
  4. For the next step, it is convenient to put the 3 elements we just made in a new Group:

    support = Group([column_1, column_2, column_base])
  5. Don't forget to import:

    from viktor.geometry import Point, Line, CircularExtrusion, mirror_object
  6. Let's see our progress by temporarily adding the support to our geometry_group and updating your app:

    geometry_group.add([support])
  7. 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.

Improve the speed of your app

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.

  1. First, place the support in the corner of the grid using translate:

    support.translate((-water_width/2 + grid_size_x/2, -params.width/2 + pile_diameter/2, 0))
  2. 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 = 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
    )
  3. Let's add all_supports to geometry_group so we visualize them

    # Adding geometry group to geometry result
    geometry_group.add(all_supports)
  4. As always, don't forget to import:

    from viktor.geometry import BidirectionalPattern
  5. Refesh your app, and look how amazing your bridge is looking!

All code until now

Click here to see all code until now

from viktor import ViktorController, Color
from viktor.parametrization import ViktorParametrization, Text, NumberField
from viktor.views import GeometryView, GeometryResult
from viktor.geometry import Group, Material, SquareBeam, Point, Line, CircularExtrusion, mirror_object, BidirectionalPattern

class Parametrization(ViktorParametrization):
intro = Text("# 3D model app 🌉\n This app parametrically generates and visualises a 3D model of a bridge")

txt_deck = Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = NumberField("Deck length", min=6, default=100, suffix='m')
width = NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = NumberField("Bridge height", min=2, default=10, suffix='m')

txt_support = Text('### Support layout', flex=100)
support_amount = NumberField("Number of supports", default=2, min=1)
support_piles_amount = NumberField("Number of piles per support", min=2, default=3)

class Controller(ViktorController):
label = 'My Entity Type'
parametrization = Parametrization

@GeometryView("3D", duration_guess=1)
def visualize_bridge(self, params, **kwargs):

# Create an empty group to hold all the geometry
geometry_group = Group([])

# ===============================
# Draw Enviroment
# ===============================

# 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 = Material('Water', color=Color.blue(), threejs_opacity=0.7)
mat_deck_top = Material('Deck top', color=Color(60, 60, 60))

# Draw water
water = SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])

# ===============================
# Create Bridge
# ===============================

# Draw deck

deck = SquareBeam(params.length, params.width, params.deck_thickness)
deck.translate((0, 0, params.height))

deck_top = 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 = Point(-grid_size_x/2, 0, params.height)
bottom_point = Point(0, 0, 0)
column_line = Line(top_point, bottom_point)

column_1 = CircularExtrusion(pile_diameter, column_line)
column_2 = mirror_object(column_1, Point(0, 0, 0), (1, 0, 0))

# Draw base

column_base = SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter)

# Pattern supports

support = 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 = 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 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.

  1. In your GeometryView, let's add a new Material:

    mat_grass = Material('Grass', color=Color.green())
  2. Then, we will create, rotate, and translate the embankment, and make a copy of it :

    # Draw embankment (assuming embankment at 45 deg)

    embankment_1 = 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 = mirror_object(embankment_1, Point(0, 0, 0), (1, 0, 0))
  3. As allways, add the objects to geometry_group:

    geometry_group.add([embankment_1, embankment_2])
  4. Dont't forget to import:

    import math
  5. Reaload 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:

  1. Create an ColorField:

    bridge_color = ColorField('Bridge Color', default=Color(200, 200, 200))
  2. Create a material

    mat_bridge = Material('Bridge', color=params.bridge_color)
  3. Assign this material to any (or all) of deck, column_1 and column_base. By adding material=mat_bridge to the SquareBeam and CircularExtrusion functions that generate these elements.

  4. Don’t forget to import

    from viktor.parametrization import ColorField
  5. Reload and play with you app

6. The complete app code

from viktor import ViktorController, Color
from viktor.parametrization import ViktorParametrization, Text, NumberField, ColorField
from viktor.views import GeometryView, GeometryResult
from viktor.geometry import Group, Material, SquareBeam, Point, Line, CircularExtrusion, mirror_object, BidirectionalPattern
import math

class Parametrization(ViktorParametrization):
intro = Text("# 3D model app 🌉\n This app parametrically generates and visualises a 3D model of a bridge")

txt_deck = Text('### Deck layout ') # texts can be used for explanation in the user-interface
length = NumberField("Deck length", min=6, default=100, suffix='m')
width = NumberField("Deck width", min=2, default=20, suffix='m')
deck_thickness = NumberField("Deck thickness", min=0.5, default=1, suffix='m')
height = NumberField("Bridge height", min=2, default=10, suffix='m')

txt_support = Text('### Support layout', flex=100)
support_amount = NumberField("Number of supports", default=2, min=1)
support_piles_amount = NumberField("Number of piles per support", min=2, default=3)
bridge_color = ColorField('Bridge Color', default=Color(200, 200, 200))


class Controller(ViktorController):
label = 'My Entity Type'
parametrization = Parametrization

@GeometryView("3D", duration_guess=1)
def visualize_bridge(self, params, **kwargs):

# Create an empty group to hold all the geometry
geometry_group = 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 = Material('Water', color=Color.blue(), threejs_opacity=0.7)
mat_deck_top = Material('Deck top', color=Color(60, 60, 60))
mat_grass = Material('Grass', color=Color.green())
mat_bridge = Material('Bridge', color=params.bridge_color)

# ===============================
# Draw environment
# ===============================

# Draw water
water = SquareBeam(water_width, env_width, env_thick, material=mat_water)
geometry_group.add([water])

# Draw embankment (at 45 deg)

embankment_1 = 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 = mirror_object(embankment_1, 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 = SquareBeam(params.length, params.width, params.deck_thickness, material=mat_bridge)
deck.translate((0, 0, params.height))

deck_top = 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 = Point(-grid_size_x/2, 0, params.height)
bottom_point = Point(0, 0, 0)
column_line = Line(top_point, bottom_point)

column_1 = CircularExtrusion(pile_diameter, column_line, material=mat_bridge)
column_2 = mirror_object(column_1, Point(0, 0, 0), (1, 0, 0))

# Draw base

column_base = SquareBeam(3 * pile_diameter, pile_diameter, 2*pile_diameter, material=mat_bridge)

support = 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 = 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 GeometryResult(geometry_group)

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.