Adding a new object to ManimRT - 62firelight/manimRT-490 GitHub Wiki

Objects supported by ManimRT are based off existing mobjects in Manim. This tutorial will go through how to add a new object by creating a new RT object that inherits an existing Manim mobject. Completely new RT objects are not currently supported.

An RTPlane will be implemented as part of this tutorial.

Testing the Manim mobject

It's a good idea to get an idea of how the object you're adding works before adding it into ManimRT.

The goal for this section is to create a unit version of the Manim mobject, identifying important parameters from the mobjects in the process.

This test scene can be used to test the Manim mobject.

Replace Object in RTObjectTest with the name of your object (e.g. RTPlaneTest).

RTPlaneTest_ManimCE_v0 18 0

from manim import *

class RTObjectTest(ThreeDScene):
    def construct(self):
        self.set_camera_orientation(phi=45*DEGREES, theta=45*DEGREES)

        axes = ThreeDAxes()
        labels = axes.get_axis_labels()
        
        self.add(axes, labels)

For the plane mobject, Square is the closest mobject equivalent. However, a default Square starts off like this:

RTPlaneTest_ManimCE_v0 18 0

To make this Square resemble an X-Y unit plane, I looked into the Square class in the Manim source code.

The Square has one unique parameter in its constructor -- side_length which is set to 2 by default. We set this parameter to 1 to ensure that the plane is scaled to a unit length when created.

Looking at the Manim Reference manual, Square is descended from the VMobject class and therefore inherits the parameters in VMobject's constructor. We are able to change those parameters when creating a Square object, but we'll need to know which ones are actually useful first.

In this case, there are three parameters: fill_color (setting the plane's colour), fill_opacity (making sure the plane's colour is visible) and stroke_opacity (removing the white outline from the plane).

The Cube class was also important, as it is essentially just six planes. It creates six Squares and passes in the parameter shade_in_3d=True for each Square it creates. I found this parameter useful for making sure that the plane was actually in 3D (i.e. it doesn't jump in front of other 3D objects because it is actually rendered as a 3D object).

Using the information I've gathered, I added the plane to the scene using this line of code:

plane = Square(side_length=2, fill_opacity=1, fill_color=BLUE, stroke_opacity=0, shade_in_3d=True)

RTPlaneTest_ManimCE_v0 18 0

Make sure to test the stretch (scaling), rotate (rotation) and shift (translation) methods on the object to test that it transforms as expected.

For example, if we want to transform the above plane:

# Stretch plane on X, Y and Z axis respectively
plane.stretch(2, 0) 
plane.stretch(2, 1) 
plane.stretch(2, 2) 

# Rotate plane by 90 degrees on the X axis
# Use UP for Y axis and OUT for Z axis
plane.rotate(90 * DEGREES, RIGHT)

# Translate plane to (1, 2, 3)
plane.shift([1, 2, 3])

Creating a ManimRT object

Create a new Python file and name it RTObject.py, replacing Object with the name of the object to be implemented.

As we are implementing a Plane, this new file will be named RTPlane.py.

Start off with the following code:

from manim import *
from typing import Sequence

# TODO: Replace Object and Mobject with the relevant objects
class RTObject(Mobject):
    def __init__(
        self,
        translation: Sequence[float] = [0, 0, 0],
        x_scale: float = 1,
        y_scale: float = 1,
        z_scale: float = 1,
        x_rotation: float = 0,
        y_rotation: float = 0,
        z_rotation: float = 0,
        refractive_index: float = 1,
        color=WHITE,
        opacity=1,
        **kwargs
    ):
        # get 3x3 scale matrix
        self.scale_matrix = np.array([
            [x_scale, 0, 0],
            [0, y_scale, 0],
            [0, 0, z_scale]
        ])
        
        # get 3x3 rotation matrix
        self.rot_matrix = np.identity(3)
        self.rot_matrix = np.matmul(rotation_matrix(x_rotation, RIGHT), self.rot_matrix)
        self.rot_matrix = np.matmul(rotation_matrix(y_rotation, UP), self.rot_matrix)
        self.rot_matrix = np.matmul(rotation_matrix(z_rotation, OUT), self.rot_matrix)
        
        # get 4x4 translation vector 
        # (only last column changes)
        self.translation = np.array([
            [1, 0, 0, translation[0]],  
            [0, 1, 0, translation[1]],
            [0, 0, 1, translation[2]],
            [0, 0, 0, 1],
        ])
        
        # combine scale and rotation matrices
        self.transform = np.matmul(self.rot_matrix, self.scale_matrix)
        
        # ensure the linear transformation is stored as homogeneous coordinates
        self.transform = np.c_[self.transform, np.array([0, 0, 0])]
        self.transform = np.r_[self.transform, np.array([0, 0, 0, 1](/62firelight/manimRT-490/wiki/0,-0,-0,-1))]
        
        # combine translation matrix with scale and rotation matrices
        self.transform = np.matmul(self.translation, self.transform)
        
        # calculate inverse
        self.inverse = np.linalg.inv(self.transform)
        
        # TODO: keep track of the unit version of this for display purposes
        self.unit_form
        
        # TODO: Add relevant parameters for parent class
        super().__init__(
            **kwargs
        )
        
        self.set_color(color)
        self.set_opacity(opacity)

        # Scale by X, Y and Z axis (in that order)
        self.stretch(x_scale, 0)
        self.stretch(y_scale, 1)
        self.stretch(z_scale, 2)
        
        # Rotate by X, Y and Z axis (in that order)
        self.rotate(x_rotation, RIGHT)
        self.rotate(y_rotation, UP)
        self.rotate(z_rotation, OUT)
        
        # Perform a translation
        self.shift(translation)

The above code will initialize the transformation matrix for the object, and then apply the transformation to the object.

Note the comments that start with "TODO:". You should replace the TODO comments with the required code.

For a plane, the replacement code would be:

Class name:

class RTPlane(Square):

Unit version of the object:

        # keep track of the unit version of this
        # for display purposes
        self.unit_form = Square(side_length=1, fill_color=BLUE, fill_opacity=1, stroke_opacity=0)

Calling parent constructor:

        super().__init__(
            side_length=2,
            fill_color=BLUE,
            fill_opacity=1,
            stroke_opacity=0,
            shade_in_3d=True,
            **kwargs
        )

Test your object implementation in a Manim scene (like the one that was created in the previous section) to check if it works.

Implementing ray intersections

The object has been implemented in ManimRT, but rays that intersect the object still won't know how to calculate the intersection points with the object.

Add the get_intersection method below to the class for your ManimRT object.

    def get_intersection(
        self,
        ray: Ray3D
    ):      
        # apply inverse transformation to the ray
        start_inverse = np.matmul(self.inverse, ray.homogeneous_start)
        direction_inverse = np.matmul(self.inverse, ray.homogeneous_direction)
        
        inhomogeneous_start_inverse = start_inverse[:3]
        inhomogeneous_direction_inverse = direction_inverse[:3]
        
        # TODO: Calculate intersection point(s) and add them to hit_locations
        
        hit_points = []
        normals = []
        for hit_location in hit_locations:
            # find hit point for the transformed ray
            hit_point = start_inverse + hit_location * direction_inverse
            
            # apply original transformation to find actual hit point
            hit_point = np.matmul(self.transform, hit_point)
            
            hit_point = hit_point[:3]
            
            hit_points.append(hit_point)
            
            # TODO: calculate normals
    
        ray.hit_points = hit_points
        ray.normals = normals
    
        return hit_points

You will need to replace the TODO components with the required code.

It may be helpful to refer to the get_intersection method in RTSphere.py to help with calculating the intersection point(s).

For example, the plane will be implemented in the following way:

Calculating intersection point(s):

        z0 = start_inverse[2]
        dz = direction_inverse[2]
        
        hit_location = -z0 / dz
        hit_locations = [hit_location]

Calculating normals:

        normal = [0, 0, 1, 0]
        if np.dot(normal[:3], ray.direction) > 0:
             normal = [0, 0, -1, 0]
         
        inverse_transform_transpose = np.linalg.inv(np.transpose(self.transform))
        
        normal_transformed = np.matmul(inverse_transform_transpose, normal)
            
        normals.append(normal_transformed[:3])

Test the intersection point(s) in a Manim scene. Start with a unit object centered at the origin, and then if that works, try applying more and more transformations to make sure that the intersection point(s) work as expected.

For the plane, I created the following scene:

RTPlaneTest_ManimCE_v0 18 0

from manim import *

from manim_rt.RTPlane import RTPlane
from manim_rt.Ray3D import Ray3D

class RTPlaneTest(ThreeDScene):
    def construct(self):
        self.set_camera_orientation(phi=45*DEGREES, theta=45*DEGREES)
        
        axes = ThreeDAxes()
        labels = axes.get_axis_labels()
        
        plane_centre = [0.5, 0.5, -0.5]
        plane = RTPlane(plane_centre, 2, 4, 6, 45 * DEGREES, 45 * DEGREES, 45 * DEGREES)
        
        ray_start = [1, -1, 1]
        ray = Ray3D(ray_start, np.subtract(plane_centre, ray_start), 5, color=RED)
        
        hit_points = plane.get_intersection(ray)
        
        first_hit_point = hit_points[0]
        self.add(Dot3D(first_hit_point))
        print(first_hit_point)
        
        unit_normal = Ray3D(first_hit_point, ray.get_unit_normal(0), color=GREEN)
        
        self.add(axes, labels, plane, ray, unit_normal)

From here, you should test the methods for the lighting vectors (get_reflected_light_vector, get_light_vector, get_viewer_vector, get_shadow_ray, etc).