Skip to content

Add new primitive: TORUS 🍩 #419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: development
Choose a base branch
from

Conversation

munechika-koyo
Copy link
Contributor

Hello,

I would like to create a PR about a new Torus primitive.
Additionally, I implemented required functionalities to solve quartic, cubic equations, etc.

If you agree to this PR, then I will add or modify the documentations written about primitives' sections.
The example rendering of a torus primitive is below, the script of which is demos/primitives/simple_torus.py.

I would appreciate it if you would review my codes and comment this PR.

simple_cupper_torus

@munechika-koyo
Copy link
Contributor Author

munechika-koyo commented Nov 27, 2023

@vsnever
@mattngc
@CnlPepper
This feature is not prioritized over other issues, but how do you feel about adding this?

Copy link
Contributor

@vsnever vsnever left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this is an excellent implementation, @munechika-koyo. I have only minor comments.

Some plasma diagnostics, such as ITER CXRS, use toric lenses and mirrors, so having the Torus primitive in Raysect would be very useful for Cherab as well.

If @CnlPepper approves this PR, could you please update both primitives.rst and api_reference/primitives/geometric_primitives.rst?


f = a.x^4 + b.x^3 + c.x^2 + d.x + e

The quartic equation has 0, 1, 2, 3 or 4 real roots, and this function will return the number of real roots.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, the function returns only 0, 2 or 4. The special cases of 1 and 3 real roots are not treated by this function separately from other cases due to machine epsilon.

Comment on lines 326 to 337
extent = self._major_radius + self._minor_radius + BOX_PADDING
box.lower = new_point3d(-extent, -extent, -self._minor_radius - BOX_PADDING)
box.upper = new_point3d(extent, extent, self._minor_radius + BOX_PADDING)

# obtain local space vertices
points = box.vertices()

# convert points to world space and build an enclosing world space bounding box
# a small degree of padding is added to avoid potential numerical accuracy issues
box = BoundingBox3D()
for point in points:
box.extend(point.transform(self.to_root()), BOX_PADDING)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need to add BOX_PADDING twice here. It's enough to add padding when you expand the bounding box.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, and will remove it here.

@CnlPepper
Copy link
Member

CnlPepper commented Dec 18, 2023

I've not had a chance to look at this, but, assuming you haven't, could you make test demo that checks it works correctly with the CSG operations (tests next intersection)?

@munechika-koyo
Copy link
Contributor Author

@CnlPepper
I tried to calculate csg oprations by:

  • Torus + Sphere(right) - Sphere(left) - Box(front) (Copper matrial)
  • Torus * Cylinder (Glass material)
Here is the script I used:
from matplotlib import pyplot as plt

from raysect.optical import ConstantSF, Point3D, World, d65_white, rotate, translate
from raysect.optical.library.metal import Copper
from raysect.optical.material import Lambert, UniformSurfaceEmitter
from raysect.optical.library import schott
from raysect.optical.observer import PinholeCamera, RGBAdaptiveSampler2D, RGBPipeline2D
from raysect.primitive import (
    Box,
    Cylinder,
    Torus,
    Sphere,
    Union,
    Intersect,
    Subtract,
)


# glass matrial
glass = schott("N-BK7")

world = World()

# Toruses
torus1 = Torus(1.0, 0.5)
torus2 = Torus(1.0, 0.5)

# Spheres
sphere1 = Sphere(0.6, transform=translate(1.0, 0.0, 0.0))
sphere2 = Sphere(0.6, transform=translate(-1.0, 0.0, 0.0))

# Box
sqrt2 = 2 ** 0.5
box = Box(
    Point3D(-1.6, -1.6, -1.6),
    Point3D(1.6, 1.6, 1.6),
    transform=translate(0.0, 2.21, 0.0),
)

# cylinder
cylinder = Cylinder(0.6, 2.0, transform=translate(0.0, 1.0, 0.0))

# Torus1 - Sphere1 + Sphere2 - Box
Subtract(
    Union(Subtract(torus1, sphere1), sphere2),
    box,
    world,
    transform=translate(0.0, 0.0, 0.6),
    material=Copper(),
)

# Torus2 * Cylinder
Intersect(
    torus2,
    cylinder,
    world,
    transform=translate(0.0, 0.3, 0.6),
    material=glass,
)


# floor
Box(
    Point3D(-100, -100, -10),
    Point3D(100, 100, 0),
    parent=world,
    material=Lambert(ConstantSF(1.0)),
)

# emitter
Cylinder(
    3.0,
    100.0,
    parent=world,
    transform=translate(0, 0, 8) * rotate(90, 0, 0) * translate(0, 0, -50),
    material=UniformSurfaceEmitter(d65_white, 1.0),
)

# camera
rgb = RGBPipeline2D(display_unsaturated_fraction=0.995)
sampler = RGBAdaptiveSampler2D(rgb, min_samples=500, fraction=0.1, cutoff=0.01)
camera = PinholeCamera(
    (512, 512),
    parent=world,
    transform=rotate(0, 45, 0) * translate(0, 0, 5) * rotate(0, -180, 0),
    pipelines=[rgb],
    frame_sampler=sampler,
)
camera.spectral_bins = 21
camera.spectral_rays = 1
camera.pixel_samples = 250
camera.ray_max_depth = 10000
camera.ray_extinction_min_depth = 3
camera.ray_extinction_prob = 0.01


# start ray tracing
plt.ion()
for p in range(0, 1000):
    print(f"Rendering pass {p}...")
    camera.observe()
    print()

plt.ioff()
rgb.display()
plt.show()

simple_torus_csg

Copy link
Contributor

@vsnever vsnever left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked on the hollow toroids

torus1 = Subtract(Torus(1.0, 0.5), Torus(1.0, 0.47))
torus2 = Subtract(Torus(1.0, 0.5), Torus(1.0, 0.47))

that everything works fine when rays hit the surface from the inside.
Hollow_torus

@vsnever
Copy link
Contributor

vsnever commented Apr 15, 2024

I think the implementation of the next_intersection() method is inconsistent with its intended behaviour. The description of this method says:

cpdef Intersection next_intersection(self):
"""
Virtual method - to be implemented by derived classes.
Returns the next intersection of the ray with the primitive along the
ray path.
This method may only be called following a call to hit(). If the ray
has further intersections with the primitive, these may be obtained by
repeatedly calling the next_intersection() method. Each call to
next_intersection() will return the next ray-primitive intersection
along the ray's path. If no further intersections are found or
intersections lie outside the ray parameters then next_intersection()
will return None.

Unlike any other primitive in Raysect, there are up to four possible intersections of a ray with a torus, but the method always stops after the second intersection.

    cpdef Intersection next_intersection(self):

        if not self._further_intersection:
            return None

        # this is the 2nd intersection
        self._further_intersection = False
        return self._generate_intersection(self._cached_ray, self._cached_origin, self._cached_direction, self._next_t)

The method is used only in the CSGPrimitive._identify_intersection() method.

As a result, extra intersections are generated in some cases. The output of this script:

from raysect.optical import Point3D, Vector3D, World, translate, InterpolatedSF
from raysect.optical.material import Dielectric
from raysect.primitive import Union, Torus, Cylinder
from raysect.optical.loggingray import LoggingRay


glass = Dielectric(index=InterpolatedSF([10, 10000], [1., 1.]),
                   transmission=InterpolatedSF([10, 10000], [1., 1.]),
                   transmission_only=True)

world = World()

torus = Torus(1.0, 0.5)

cylinder = Cylinder(1.0, 1.0, transform=translate(0, 0, -0.5))

union = Union(torus, cylinder, material=glass, parent=world)

ray = LoggingRay(origin=Point3D(2., 0, 0), direction=Vector3D(-1, 0, 0))
ray.trace(world)
for intersection in ray.log:
    print(intersection.hit_point, intersection.exiting)
    print()

is

Point3D(1.5, 0.0, 0.0) False

Point3D(1.4999997776219791, 0.0, 0.0) False

Point3D(-1.0, 0.0, 0.0) True

Point3D(-1.5, 0.0, 0.0) True

The intersection at Point3D(-1.0, 0.0, 0.0) is an extra one because the ray is not exciting the primitive at (-1, 0, 0).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants