Writing a Custom Camera Lens Script

The program ships with support for replacing all of the camera lens types with custom lenses defined by Lua scripts. If you dig around in the application folder you'll eventually find the file "render-functions.lua" which provides this support and defines the default cubic camera implementation.

The following example installs as an auto-loaded script and redefines the panoramic camera to be a "dome" camera. This new camera lens type creates an image by rendering a dome around the camera up direction, using the view angle setting as a half angle for the dome. For example a view angle of 90 degrees gives you a 180 degree dome rendering for a full hemisphere. Set the view angle higher or lower to increase or decrease the field of view.

By default this camera lens type is not enabled for use. The auto-loading script adds a menu entry to the Scripting menu for enabling or disabling the dome camera and this setting will be remembered across program launches.

You can download the source file for this camera lens. Install it into the "Autoload" subfolder of the "Scripting" folder. Look here for instructions on how to find these folders on your system.

The Example Dome Camera Script

--

-- This script adds a new "Dome" camera lens type.

-- Install this script into an auto-load folder to make it available.

--

local kPersistentKey = "Use Dome Lens"

-- We'll remember the last user selection via this persistent key.

--local kReplaceCameraType = 'norm'

--local kReplaceCameraType = 'cube'

local kReplaceCameraType = 'pano'

--local kReplaceCameraType = 'sphr'

--local kReplaceCameraType = 'ster'

-- Enable one of these types to replace the corresponding camera type in the program.

-- I don't recommend replacing the "normal" camera, but any of the others seem reasonable.

local kReplaceCameraName = "Panoramic Camera"

-- For use in the scripting menu item text.

local menu_disable_item = 0

local menu_enable_item = 1

local menu_support_check = 2

local menu_do_check = 4

local menu_change_text = 8

-- Constants for menu enabling.

local function RegisterDomeCamera ()

--

-- Camera lenses need to be defined by generator functions in every case so that we can

-- keep local per-rendering state within each function closure and avoid conflicts.

--

assert( s3d.render and s3d.render.cameraLenses and s3d.render.cameraLensFunctionGenerators )

local function GenerateCameraFunction_Dome (lensStyle, frameTime, cameraState, worldExtent)

-- The lensStyle argument will match that used to look up this generator function from the cameraLenses table.

--

-- frameTime -> the time in seconds for the rendering using this lens.

--

-- cameraState -> a table of useful values describing the camera:

--

-- viewOrigin -> a Point3d giving the viewer origin.

-- viewDirection -> a Dir3d giving the normalized direction in front of the viewer.

-- viewUp -> a Dir3d giving the normalized direction up from the viewer.

-- viewRight -> a Dir3d giving the normalized direction to the right of the viewer.

-- viewCenter -> a Point giving the view center of our original source view in terms of our frame.

-- adjustedOrigin -> (like the version above, but offset backward by our camera focal length)

-- adjustedDirection -> (like the version above, but pre-scaled by our frame pixel dimensions)

-- adjustedUp -> (ditto)

-- adjustedRight -> (ditto)

-- pixelAspect -> a Point2d currently always 1.0 in each component.

-- relativeFrame -> an Extents2d structure defining the relationship of this rendering frame

-- relative to the original source view. (-1, -1, +1, +1) means we cover it all.

-- viewAngle -> the full view angle for our camera in radians, measured horizontally.

-- camera -> the Camera for this rendering.

-- cameraToWorld -> the Transformation to get from camera to world coordinates.

-- image -> the image Buffer for this rendering

--

-- rayConeAngleTangent -> picture the ray as a cone spreading through space with the apex

-- at the pixel through which the camera is looking. This is the

-- tangent of (actually) half of the spread angle for the cone.

-- This is a read/write value.

-- rayConeBaseDiameter -> this is the diameter of the circular area covering the source pixel.

-- This is a read/write value.

--

-- Only the rayConeAngleTangent and rayConeBaseDiameter fields are modifiable. Change them

-- to affect the rays that are cast later through this lens.

--

-- worldExtent -> an Extents3d structure stuffed with the occupied work extents.

--

-- This camera type ignores the relative frame on the assumption that we can't really

-- render just a subset of the frame and have it make sense. The whole field is always rendered.

local size = cameraState.image:GetSize ()

local adjustAngle = 2.0 * cameraState.viewAngle / math.pi

-- I'm re-interpreting the view angle here to define half the dome angle.

-- Use 90 degrees for a full 180 degree dome.

local origin = cameraState.viewOrigin

local dir = cameraState.viewDirection

local up = cameraState.viewUp

local right = cameraState.viewRight

-- It turns out that access involving multiple '.'s has a cost for each access. Do it once here.

return function (ray, pixel_h, pixel_v, step)

-- Return true for points within the hemisphere.

local v = 1 - 2 * pixel_v / size.v

local h = 2 * pixel_h / size.h - 1

local radiusSqr = v * v + h * h

if radiusSqr > 1.0 then

return false

end

local distance = math.sqrt( 1.0 - radiusSqr )

console( "Before -> h:%f, v:%f, d:%f", h, v, distance )

if distance < 1.0 then

local angle = math.acos( distance )

local adjustedDistance = math.cos( angle * adjustAngle )

local adjustVH = math.sqrt( (1.0 - adjustedDistance * adjustedDistance) / radiusSqr )

h = h * adjustVH

v = v * adjustVH

distance = adjustedDistance

console( "After -> h:%f, v:%f, d:%f", h, v, distance )

end

ray.origin = origin

ray.direction:Set( distance * up.x - v * dir.x + h * right.x,

distance * up.y - v * dir.y + h * right.y,

distance * up.z - v * dir.z + h * right.z )

-- Calling the "Set" method is faster than setting each component individually.

-- This version has the hemisphere centered around our up vector.

-- So you would keep the camera pointing at the edge of the visible horizon

-- while modeling.

return true

end

end

s3d.render.cameraLensFunctionGenerators.Dome = GenerateCameraFunction_Dome

-- Replace any existing definition for this camera lens.

local wasLens = s3d.render.cameraLenses[ kReplaceCameraType ]

if s3d.util.PersistentRetrieve( kPersistentKey, false ) == "TRUE" then

s3d.render.cameraLenses[ kReplaceCameraType ] = GenerateCameraFunction_Dome

end

-- Add a scripting menu entry to control the new lens type.

local domeMenuItem =

{

"Enable Dome Camera for " .. kReplaceCameraName,

function (menuItem)

if GenerateCameraFunction_Dome ~= s3d.render.cameraLenses[ kReplaceCameraType ] then

wasLens = s3d.render.cameraLenses[ kReplaceCameraType ]

-- If we remember this each time we can play better with other code swapping it out.

s3d.render.cameraLenses[ kReplaceCameraType ] = GenerateCameraFunction_Dome

s3d.util.PersistentStore( kPersistentKey, "TRUE", false )

else

s3d.render.cameraLenses[ kReplaceCameraType ] = wasLens

s3d.util.PersistentForget( kPersistentKey, false )

end

end,

function (menuItem)

-- We'll just always enable this menu item.

local result = menu_enable_item + menu_support_check

if GenerateCameraFunction_Dome == s3d.render.cameraLenses[ kReplaceCameraType ] then

result = result + menu_do_check

end

return result

end

}

assert( s3d.ui.scripting_menu )

table.insert( s3d.ui.scripting_menu, { "-" } )

table.insert( s3d.ui.scripting_menu, domeMenuItem )

end

RegisterAutoloadFinalizer { RegisterDomeCamera }

To create a new dome camera first create a regular camera object, then set the type of the camera to "Panorama", then set the aspect ratio popup in the camera window to "1.00 : 1" for a square view. Enable the dome camera in the Scripting menu and you're ready to go.