3D ray tracing using raycast

Has anyone successfully used the craft physics raycast in 3D with craft rigidbodies? I didn’t manage to get it to work.

By the way, I remember seeing somewhere an undocumented command to draw a 3D line-can someone remind me about that.

@piinthesky Heres something I have. Use the sliders to move the raycast line around. The sphere is at (10,18,40). When you hit the sphere with the raycast line, the word hit will show below the center point. You can rotate the image to get a better view of the ray cast line and the sphere. You can change the size of the center circle and the raycast direction.


viewer.mode=STANDARD

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")
    
    parameter.integer("x",0,60,0)
    parameter.integer("y",0,60,0)
    parameter.integer("z",0,60,0)   
    fill(255)
    scene = craft.scene()
    scene.sun.rotation=quat.eulerAngles(20,45,-30)    
    v=scene.camera:add(OrbitViewer,vec3(0,0,0),100,0,1000)       
    
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(158, 202, 223, 255)
    skyMaterial.horizon=color(98, 166, 114, 255)
    
    createSphere(vec3(10,18,40),2)
end

function draw()
    update(DeltaTime)
    scene:draw()
    ellipse(WIDTH/2,HEIGHT/2,4) -- center point
    scene.debug:line(vec3(0,0,0),vec3(x,y,z),color(255))  -- 3d line
    ta=scene.physics:raycast(vec3(0,0,0),vec3(x,y,z),100)
    text("(10, 18, 40)",WIDTH/2,HEIGHT/2-50)
    if ta~=nil then
        text("hit",WIDTH/2,HEIGHT/2-20)
    end
end

function update(dt)
    scene:update(dt)
end

function createSphere(p,size)
    local sphere=scene:entity()
    local s1=sphere:add(craft.rigidbody,STATIC)
    sphere.position=vec3(p.x,p.y,p.z)
    sphere.model = craft.model.icosphere(size,2)
    sphere:add(craft.shape.sphere,size)
    sphere.material = craft.material(asset.builtin.Materials.Specular)
    sphere.material.map = readImage(asset.builtin.Blocks.Brick_Red)    
end

Exactly what i was trying to do! Thanks @dave1707

@John I was hoping to write a 3D ray tracing program based on the physics:raycast command.

I see that the raycast works nicely when a ray is external to a rigid body and hits its surface.

Unfortunately a raycast starting inside a rigid body does not register a hit on its surface when it exits the rigid body.

Would it be possible to add an option to raycast so that the hit definition includes this ‘internal’ hit case?

If you’re after the x,y,z coords of where the raycast passes thru the center sphere, I think this will give you the info as x1,y1,z1. This assumes the center sphere is at 0,0,0. If it’s not the calculation gets a little more complicated. I think you just need to add the offset coordinates.


viewer.mode=STANDARD

function setup()
    fill(255)
    parameter.integer("x",0,60,20)  -- x,y,z raycast direction
    parameter.integer("y",0,60,20)
    parameter.integer("z",0,60,20)
    parameter.integer("radius",1,50,10) -- radius of center sphere  
end

function draw()
    background(0)
    
    dist=math.sqrt(x^2+y^2+z^2)
    x1=x*radius/dist
    y1=y*radius/dist
    z1=z*radius/dist
    
    text("dist "..dist,WIDTH/2,HEIGHT/2)
    text("x1  "..x1,WIDTH/2,HEIGHT/2-50)    -- coord on center sphere
    text("y1  "..y1,WIDTH/2,HEIGHT/2-80)    --   where raycast passes through
    text("z1  "..z1,WIDTH/2,HEIGHT/2-110)
end

Thanks @dave1707, actually the raycast gives the hit point on the rigid body and the direction of the normal at that point-so ok for that.

I want to be able to handle the case the raycast originates within a body (of any shape) and find its exit point. This would allow me to refract the light ray through the body (I would apply the Snells law at the interfaces). It must be the case that the existing behind the scenes code for the raycast could be easily modified to handle this. Otherwise, I would have loop through all the triangles in the mesh and calculate any intersection point. Sill hoping @John can help me here!

By the way @dave1707 do you have any code that can create a mesh for an arbitrary lens? I.e. different radii of curvature for each side of the lens I.e. intersection region of two spheres of different radii.

@piinthesky I had a look into it and couldn’t find any info on how to get Bullet physics (the engine we use) to work for raycasts inside objects. Do your rays begin outside an object and you just want to find the exit point after refraction? Technically you could move the ray origin forward after the first hit and fire it backward after setting a collision mask for that one object. That way you could find the exit point for that one object in the refraction direction

@John, thanks for investigating.

I would like to be to draw the paths of light rays as they pass through an arbitrary 3D object. I would assign a particular refractive index to each object.

One example could be a sphere…in this case I generate a raycast with an origin outside the sphere, then find its intersection with the sphere and its normal (Daves code does that already). Then generate another raycast from that intersection point but with the 3D angle changed to take into account the differences in refractive index so that it passes through the glass sphere until it hits somewhere on the other side of the sphere. Then I would repeat the process to draw the full path of the light ray through all the objects along its path.

Yes, that is a good idea. I calculate a point along the refracted ray that is well outside the object. Then from that point I fire a ray ‘backwards’ until it hits the object-that would be the exit point. Great I will try that!

@piinthesky Instead of a mesh, maybe it would be easier to calculate the tangent to a point on a sphere where the ray hits. Then calculate the angle the next ray takes.

@John, @dave1707, I implemented the backward raycast-all works well.

I notice if I scale the entity, the rendered image changes but the ray trace does not match. So I guess the raycast interacts with the rigid body and not the mesh of the model. As there is a limited choice of rigidbodies I may abandon the raycast and do it manually by testing for an intersection of the ray vector with each triangle of the model?

-- Raycast, 26/8/23

viewer.mode=STANDARD

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")
    
    parameter.number("x",-1,1,0)
    parameter.number("y",-1,1,1)
    parameter.number("z",-1,1,0)   
    
    fill(255)
    scene = craft.scene()
    scene.sun.rotation=quat.eulerAngles(20,45,-30)    
    v=scene.camera:add(OrbitViewer,vec3(0,0,0),100,0,1000)       
    
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(158, 202, 223, 255)
    skyMaterial.horizon=color(98, 166, 114, 255)
    
    n1=1  --index of vacuum
    n2=1.4 --index of sphere
    
    mySphere=createSphere(vec3(0,18,0),10,true)
--    mySphere.scale=vec3(0.75,0.5,0.75)
    
    org=createSphere(vec3(0,0,0),1,false) --origin 

end

function draw()
    update(DeltaTime)
    scene:draw()
    --draw ray
    scene.debug:line(vec3(0,0,0),vec3(x,y,z)*100,color(255))  -- 3d line
    
    --raycast from origin
    ta=scene.physics:raycast(vec3(0,0,0),vec3(x,y,z),100)  
    if ta~=nil then

        ent=ta.entity
        entryPt=ta.point
        entryNor=ta.normal
        frac=ta.fraction
        index=ta.triangleIndex
        
        --draw entry normal
        norStart=entryPt-entryNor
        norEnd=entryPt+entryNor
        scene.debug:line(norStart,norEnd,color(255))  
        
        --make internal ray
        i=entryPt:normalize()
        t=refractedRay(i,-entryNor,n1,n2)
 
        --backwards raycast from outside       
        ta=scene.physics:raycast(entryPt+t*100, -t, 100)
        exitPt=ta.point
        exitNor=ta.normal
        
        --draw exit normal
        norStart=exitPt-exitNor
        norEnd=exitPt+exitNor
        scene.debug:line(norStart,norEnd,color(255))  
        
        --draw internal ray
        scene.debug:line(entryPt,exitPt,color(241, 44, 80))  
        
        --get exit ray
        i=(exitPt-entryPt):normalize()
        t=refractedRay(i,exitNor,n2,n1)
        --draw exit ray
        scene.debug:line(exitPt,exitPt+t*100, color(241, 44, 80))  
        
    end
end


function update(dt)
    scene:update(dt)
end

function createSphere(p,size,rigid)
    local sphere=scene:entity()
    if rigid then 
        local s1=sphere:add(craft.rigidbody,STATIC)
        sphere:add(craft.shape.sphere,size)
    end
    sphere.position=vec3(p.x,p.y,p.z)
    sphere.model = craft.model.icosphere(size,2)

    sphere.material = craft.material(asset.builtin.Materials.Specular)
--    sphere.material.map = readImage(asset.builtin.Blocks.Brick_Red)    
    sphere.material.blendMode=NORMAL
    sphere.material.opacity=0.3
    return sphere
end

function refractedRay(i,n,n1,n2)
    ni=n:dot(i)
    mu=n1/n2    
    return math.sqrt(1-mu*mu*(1-ni*ni))*n +  mu*(i-ni*n)
end

Nice demo. I made changes to my copy to make it easier to see the rays.

@John, shouldn’t scale also scale the rigidbody?

@piinthesky Scale should work on rigidbodies however I noticed your using a non-uniform scale on a sphere, which is not supported in Bullet since it can only handle regular spheres and not ellipsoids

Ok, I confirm it works when it is a uniform scale.

Using craft.shape.model I was able to implement the ray tracing for an arbitrary model. See video below

1 Like

@piinthesky Nice video, very interesting. Can you share the code, I’m curious about the calculations. Also, I’m not sure about the code for uniform scale. I can get sprites to scale, but not Craft models, so I’m not doing something right.

Here is the current code. It uses the pseudoMesh class to make the cylinders for the rays.

Raycast2.zip (13.7 KB)

I will be extending this to handle multiple objects, reflections, filters, chromatic dispersion, etc.

Will be using it for work, but maybe also a puzzle game based on bouncing the light around.

1 Like

Thanks for the code. It’s nice to try the different primitive objects and to slow the movement down to get a better view of the rays.

@John @dave1707 as suggested, I have tried to implement the collision mask on the raycast, but I did not succeed to make it work. Have you ever succeeded? Do you have an example code?

Here’s my original code again. The parameter values are set for a raycast hit with the text hit displayed in the center of the screen. In the raycast line of code, the last value (1) is the raycast group. In the createSphere function I have a mask of 3 set. The text hit will show. If you change the mask to 4, it won’t display hit. Mask and Group work with bit values or they should. So any bit that’s on in mask that has a corresponding bit in group that’s on should cause a hit.


viewer.mode=STANDARD

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")
    
    parameter.integer("x",0,60,10)
    parameter.integer("y",0,60,18)
    parameter.integer("z",0,60,40)   
    fill(255)
    scene = craft.scene()
    scene.sun.rotation=quat.eulerAngles(20,45,-30)    
    v=scene.camera:add(OrbitViewer,vec3(0,0,0),100,0,1000)       
    
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(158, 202, 223, 255)
    skyMaterial.horizon=color(98, 166, 114, 255)
    
    createSphere(vec3(10,18,40),2)
end

function draw()
    update(DeltaTime)
    scene:draw()
    ellipse(WIDTH/2,HEIGHT/2,4) -- center point
    scene.debug:line(vec3(0,0,0),vec3(x,y,z),color(255))  -- 3d line
    
    -- mask group = 1, bit 1 on
    ta=scene.physics:raycast(vec3(0,0,0),vec3(x,y,z),100,1)

    text("(10, 18, 40)",WIDTH/2,HEIGHT/2-50)
    if ta~=nil then
        text("hit",WIDTH/2,HEIGHT/2-20)
    end
end

function update(dt)
    scene:update(dt)
end

function createSphere(p,size)
    local sphere=scene:entity()
    local s1=sphere:add(craft.rigidbody,STATIC)
    s1.mask=3  
    sphere.position=vec3(p.x,p.y,p.z)
    sphere.model = craft.model.icosphere(size,2)
    sphere:add(craft.shape.sphere,size)
    sphere.material = craft.material(asset.builtin.Materials.Specular)
    sphere.material.map = readImage(asset.builtin.Blocks.Brick_Red)    
end

thanks @dave1707 that works now- i was confused with the groups and masks.