ianmackenzie / elm-3d-scene / Scene3d

Top-level functionality for rendering a 3D scene.

Note that the way elm-3d-scene is designed, functions in this module are generally 'cheap' and can safely be used in your view function directly. For example, you can safely have logic in your view function that enables and disables lights, moves objects around by translating/rotating/mirroring them, or even changes the material used to render a particular object with.

In contrast, creating meshes using the functions in the Mesh module is 'expensive'; meshes should generally be created once and then stored in your model.

One small limitation to keep in mind: browsers generally don't allow more than 16 active WebGL 'contexts' at a time, so trying to render a hundred different 3D scenes on a single page will almost certainly not work!

unlit : { dimensions : ( Quantity Basics.Int Pixels, Quantity Basics.Int Pixels ), camera : Camera3d Length.Meters coordinates, clipDepth : Length, background : Background coordinates, entities : List (Entity coordinates) } -> Html msg

Render a simple scene without any lighting. This means all objects in the scene should use plain colors - without any lighting, other material types will always be completely black!

Unlit scene

You will need to provide:

The clip depth is necessary because of how WebGL projection matrices are constructed. Generally, try to choose the largest value you can without actually clipping visible geometry. This will improve the accuracy of the depth buffer which in turn reduces Z-fighting.

cloudy : { dimensions : ( Quantity Basics.Int Pixels, Quantity Basics.Int Pixels ), upDirection : Direction3d coordinates, camera : Camera3d Length.Meters coordinates, clipDepth : Length, background : Background coordinates, entities : List (Entity coordinates) } -> Html msg

Render an outdoors 'cloudy day' scene. This adds some soft lighting to the scene (an approximation of the lighting on a cloudy day) so that all surfaces are illuminated but upwards-facing surfaces are more brightly illuminated than downwards-facing ones:

Cloudy scene

Note how, for example, the top of the sphere is more brightly lit than the bottom, and the sides of objects are not as brightly lit as their top. For this to work, you must specify what the global 'up' direction is (usually Direction3d.positiveZ or Direction3d.positiveY). If the wrong up direction is given, the lighting will look pretty weird - here's the same scene with the up direction reversed:

Cloudy scene with reversed up direction

sunny : { upDirection : Direction3d coordinates, sunlightDirection : Direction3d coordinates, shadows : Basics.Bool, dimensions : ( Quantity Basics.Int Pixels, Quantity Basics.Int Pixels ), camera : Camera3d Length.Meters coordinates, clipDepth : Length, background : Background coordinates, entities : List (Entity coordinates) } -> Html msg

Render an outdoors 'sunny day' scene. This adds some directional sunlight to the scene, so you need to specify:

Sunny scene

custom : { lights : Lights coordinates, camera : Camera3d Length.Meters coordinates, clipDepth : Length, exposure : Exposure, toneMapping : ToneMapping, whiteBalance : Light.Chromaticity, antialiasing : Antialiasing, dimensions : ( Quantity Basics.Int Pixels, Quantity Basics.Int Pixels ), background : Background coordinates, entities : List (Entity coordinates) } -> Html msg

Render a scene with custom lighting. In addition to camera, clip depth, dimensions, background and entities as described above, you will need to provide:

When starting out, it's usually easiest to pick a single default chromaticity such as daylight and then use that for both lights and white balance. This will make all light appear white.

Once you're comfortable with that, you can start experimenting with things like warm and cool lights. For example, fluorescent lighting will appear blueish if the white balance is set to incandescent.

Entities


type alias Entity coordinates =
Types.Entity coordinates

An Entity is a shape or group of shapes in a scene.

Basic shapes

elm-3d-scene includes a handful of basic shapes which you can draw directly without having to create and store a separate Mesh. In general, for most of the basic shapes you can specify whether or not it should cast a shadow (assuming there is shadow-casting light in the scene!) and can specify a material to use. However, different shapes support different kinds of materials:

Note that you could render complex shapes by (for example) mapping Scene3d.triangle over a list of triangles, but this would be inefficient; if you have a large number of triangles it is much better to create a mesh using Mesh.triangles or similar, store that mesh either in your model or as a top-level constant, and then render it using Scene3d.mesh. For up to a few dozen individual entities (points, line segments, triangles etc) it should be fine to use these convenience functions, but for much more than that you will likely want to switch to using a proper mesh for efficiency.

point : { radius : Quantity Basics.Float Pixels } -> Material.Plain coordinates -> Point3d Length.Meters coordinates -> Entity coordinates

Draw a single point as a circular dot with the given radius in pixels.

Single point

lineSegment : Material.Plain coordinates -> LineSegment3d Length.Meters coordinates -> Entity coordinates

Draw a single line segment.

Singe line segment

triangle : Material.Plain coordinates -> Triangle3d Length.Meters coordinates -> Entity coordinates

Draw a single triangle.

Single triangle

facet : Material.Uniform coordinates -> Triangle3d Length.Meters coordinates -> Entity coordinates

Like Scene3d.triangle, but also generates a normal vector so that matte and physically-based materials (materials that require lighting) can be used.

quad : Material.Textured coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Entity coordinates

Draw a 'quad' such as a rectangle, rhombus or parallelogram by providing its four vertices in counterclockwise order.

Single quad

Normal vectors will be automatically computed at each vertex which are perpendicular to the two adjoining edges. (The four vertices should usually be coplanar, in which case all normal vectors will be the same.) The four vertices will also be given the UV (texture) coordinates (0,0), (1,0), (1,1) and (0,1) respectively; this means that if you specify vertices counterclockwise from the bottom left corner of a rectangle, a texture will map onto the rectangle basically the way you would expect:

Textured quad

block : Material.Uniform coordinates -> Block3d Length.Meters coordinates -> Entity coordinates

Draw a rectangular block using the Block3d type from elm-geometry.

Single block

sphere : Material.Textured coordinates -> Sphere3d Length.Meters coordinates -> Entity coordinates

Draw a sphere using the Sphere3d type from elm-geometry.

Single sphere

The sphere will have texture (UV) coordinates based on an equirectangular projection where positive Z is up. This sounds complex but really just means that U corresponds to angle around the sphere and V corresponds to angle up the sphere, similar to the diagrams shown here except that V is measured up from the bottom (negative Z) instead of down from the top (positive Z).

Note that this projection, while simple, means that the texture used will get 'squished' near the poles of the sphere.

cylinder : Material.Uniform coordinates -> Cylinder3d Length.Meters coordinates -> Entity coordinates

Draw a cylinder using the Cylinder3d type from elm-geometry.

Single cylinder

cone : Material.Uniform coordinates -> Cone3d Length.Meters coordinates -> Entity coordinates

Draw a cone using the Cone3d type from elm-geometry.

Single cone

Shapes with shadows

These functions behave just like their corresponding non-WithShadow versions but make the given object cast a shadow (or perhaps multiple shadows, if there are multiple shadow-casting lights in the scene). Note that no shadows will appear if there are no shadow-casting lights!

triangleWithShadow : Material.Plain coordinates -> Triangle3d Length.Meters coordinates -> Entity coordinates

Triangle with shadows

facetWithShadow : Material.Uniform coordinates -> Triangle3d Length.Meters coordinates -> Entity coordinates

quadWithShadow : Material.Textured coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Entity coordinates

Quad with shadows

blockWithShadow : Material.Uniform coordinates -> Block3d Length.Meters coordinates -> Entity coordinates

Block with shadows

sphereWithShadow : Material.Textured coordinates -> Sphere3d Length.Meters coordinates -> Entity coordinates

Sphere with shadows

cylinderWithShadow : Material.Uniform coordinates -> Cylinder3d Length.Meters coordinates -> Entity coordinates

Cylinder with shadows

coneWithShadow : Material.Uniform coordinates -> Cone3d Length.Meters coordinates -> Entity coordinates

Cone with shadows

Meshes

mesh : Types.Material coordinates attributes -> Mesh coordinates attributes -> Entity coordinates

Draw the given mesh (shape) with the given material. Check out the Mesh and Material modules for how to define meshes and materials. Note that the mesh and material types must line up, and this is checked by the compiler; for example, a textured material that requires UV coordinates can only be used on a mesh that includes UV coordinates!

Faceted mesh

If you want to also draw the shadow of a given object, you'll need to use meshWithShadow.

meshWithShadow : Types.Material coordinates attributes -> Mesh coordinates attributes -> Mesh.Shadow coordinates -> Entity coordinates

Draw a mesh and its shadow (or possibly multiple shadows, if there are multiple shadow-casting lights in the scene).

Mesh with shadows

To render an object with a shadow, you would generally do something like:

-- Construct the mesh/shadow in init/update and then
-- save them in your model:

objectMesh =
    -- Construct a mesh using Mesh.triangles,
    -- Mesh.indexedFaces etc.

objectShadow =
    Mesh.shadow objectMesh

-- Later, render the mesh/shadow in your view function:

objectMaterial =
    -- Construct a material using Material.color,
    -- Material.metal etc.

entity =
    Scene3d.meshWithShadow
        objectMesh
        objectMaterial
        objectShadow

Grouping and toggling

group : List (Entity coordinates) -> Entity coordinates

Group a list of entities into a single entity. This combined entity can then be transformed, grouped with other entities, etc. For example, you might combine two different-colored triangles into a single group, then draw several different rotated copies of that group:

Rotated triangles

nothing : Entity coordinates

A dummy entity for which nothing will be drawn.

Transformations

These transformations are 'cheap' in that they don't actually transform the underlying mesh; under the hood they use a WebGL transformation matrix to change where that mesh gets rendered.

You can use transformations to animate objects over time, or render the same object multiple times in different positions/orientations without needing to create a separate mesh. For example, you could draw a single entity and then draw several more translated versions of it:

Translation of 3D entities

The following examples all use this duckling as the original (untransformed) entity:

Duckling with no transformation

rotateAround : Axis3d Length.Meters coordinates -> Angle -> Entity coordinates -> Entity coordinates

Rotate an entity around a given axis by a given angle.

Rotated duckling

translateBy : Vector3d Length.Meters coordinates -> Entity coordinates -> Entity coordinates

Translate (move) an entity by a given displacement vector.

Translated duckling

translateIn : Direction3d coordinates -> Length -> Entity coordinates -> Entity coordinates

Translate an entity in a given direction by a given distance.

scaleAbout : Point3d Length.Meters coordinates -> Basics.Float -> Entity coordinates -> Entity coordinates

Scale an entity about a given point by a given scale. The given point will remain fixed in place and all other points on the entity will be stretched away from that point (or contract towards that point, if the scale is less than one).

Scaled duckling

elm-3d-scene tries very hard to do the right thing here even if you use a negative scale factor, but that flips the mesh inside out so I don't really recommend it.

mirrorAcross : Plane3d Length.Meters coordinates -> Entity coordinates -> Entity coordinates

Mirror an entity across a plane.

Mirrored duckling

Background


type Background coordinates

Specifies the background used when rendering a scene. Currently only constant background colors are supported, but eventually this will be expanded to support more fancy things like skybox textures or backgrounds based on the current environmental lighting.

transparentBackground : Background coordinates

A fully transparent background.

backgroundColor : Color -> Background coordinates

A custom background color.

Antialiasing


type Antialiasing

An Antialiasing value defines what (if any) kind of antialiasing is used when rendering a scene. Different types of antialiasing have different tradeoffs between quality and rendering speed. If you're not sure what to use, Scene3d.multisampling is generally a good choice.

noAntialiasing : Antialiasing

No antialiasing at all. This is the fastest to render, but often results in very visible jagged/pixelated edges.

multisampling : Antialiasing

Multisample antialiasing. This is generally a decent tradeoff between performance and image quality. Using multisampling means that edges of objects will generally be smooth, but jaggedness inside objects resulting from lighting or texturing may still occur.

supersampling : Basics.Float -> Antialiasing

Supersampling refers to a brute-force version of antialiasing: render the entire scene at a higher resolution, then scale down. For example, using Scene3d.supersampling 2 will render at 2x dimensions in both X and Y (so four times the total number of pixels) and then scale back down to the given dimensions; this means that every pixel in the final result will be the average of a 2x2 block of rendered pixels.

This is generally the highest-quality antialiasing but also the highest cost. For simple cases supersampling is often indistinguishable from multisampling, but supersampling is also capable of handling cases like small bright lighting highlights that multisampling does not address.

Lights


type Lights coordinates

A Lights value represents the set of all lights in a scene. There are a couple of current limitations to note in elm-3d-scene:

The reason there is a separate Lights type, instead of just using a list of Light values, is so that the type system can be used to guarantee these constraints are satisfied.

noLights : Lights coordinates

No lights at all! You don't need lights if you're only using materials like color or emissive (since those materials don't react to external light anyways). But in that case it might be simplest to use Scene3d.unlit instead of Scene3d.custom so that you don't have to explicitly provide a Lights value at all.

oneLight : Light coordinates a -> Lights coordinates

twoLights : Light coordinates a -> Light coordinates b -> Lights coordinates

threeLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Lights coordinates

fourLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Light coordinates d -> Lights coordinates

fiveLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Light coordinates d -> Light coordinates Basics.Never -> Lights coordinates

sixLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Light coordinates d -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Lights coordinates

sevenLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Light coordinates d -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Lights coordinates

eightLights : Light coordinates a -> Light coordinates b -> Light coordinates c -> Light coordinates d -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Light coordinates Basics.Never -> Lights coordinates

Exposure


type Exposure

Exposure controls the overall brightness of a scene; just like a physical camera, adjusting exposure can lead to a scene being under-exposed (very dark everywhere) or over-exposed (very bright, potentially with some pure-white areas where the scene has been 'blown out').

exposureValue : Basics.Float -> Exposure

Set exposure based on an exposure value for an ISO speed of 100. Typical exposure values range from 5 for home interiors to 15 for sunny outdoor scenes; you can find some reference values here.

maxLuminance : Luminance -> Exposure

Set exposure based on the luminance of the brightest white that can be displayed without overexposure. Scene luminance covers a large range of values; some sample values can be found here.

photographicExposure : { fStop : Basics.Float, shutterSpeed : Duration, isoSpeed : Basics.Float } -> Exposure

Set exposure based on photographic parameters: F-stop, shutter speed and ISO film speed.

Tone mapping


type ToneMapping

Tone mapping is, roughly speaking, a way to render scenes that contain both very dark and very bright areas. It works by mapping a large range of brightness (luminance) values into a more limited set of values that can actually be displayed on a computer monitor.

noToneMapping : ToneMapping

No tone mapping at all! In this case, the brightness of every point in the scene will simply be scaled by the overall scene exposure setting and the resulting color will be displayed on the screen. For scenes with bright reflective highlights or a mix of dark and bright portions, this means that some parts of the scene may be underexposed (nearly black) or overexposed (pure white). For example, look at how this scene is in general fairly dim but still has some overexposed lighting highlights such as at the top of the gold sphere:

No tone mapping

That said, it's often best to start with noToneMapping for simplicity, and only experiment with other tone mapping methods if you end up with very bright, harsh highlights that need to be toned down.

reinhardToneMapping : Basics.Float -> ToneMapping

Apply Reinhard tone mapping given the maximum allowed overexposure. This will apply a non-linear scaling to scene luminance (brightness) values such that darker colors will not be affected very much (meaning the brightness of the scene as a whole will not be changed dramatically), but very bright colors will be toned down to avoid 'blowout'/overexposure.

In this example, note how the overall scene brightness is pretty similar to the example above using no tone mapping, but the bright highlights on the gold and white spheres have been softened considerably:

Reinhard tone mapping

The given parameter specifies how much 'extra range' the tone mapping gives you; for example,

Scene3d.reinhardToneMapping 5

will mean that parts of the scene can be 5x brighter than normal before becoming 'overexposed' and pure white. (You could also accomplish that by changing the overall exposure parameter to Scene3d.custom, but then the entire scene would appear much darker.)

reinhardPerChannelToneMapping : Basics.Float -> ToneMapping

A variant of reinhardToneMapping which applies the scaling operation to red, green and blue channels separately instead of scaling overall luminance. This will tend to desaturate bright colors, but this can end up being looking realistic since very bright colored lights do in fact appear fairly white to our eyes:

Reinhard per channel tone mapping

hableFilmicToneMapping : ToneMapping

A popular 'filmic' tone mapping method developed by John Hable for Uncharted 2 and documented here. This is a good default choice for realistic-looking scenes since it attempts to approximately reproduce how real film reacts to light. The results are fairly similar to reinhardPerChannelToneMapping 5, but will tend to have slightly deeper blacks:

Hable filmic tone mapping

Note that applying tone mapping can cause the scene to look slightly dark (compare the above to the example using no tone mapping). However, this can be compensated for by adjusting exposure (for example, by reducing the exposure value by 1 or 2):

Hable filmic tone mapping, brightened

Note how this version is brighter overall than the original, non-tone-mapped image but doesn't suffer from the blown-out highlights.

Advanced

You're unlikely to need these functions right away but they can be very useful when setting up more complex scenes.

Coordinate conversions

placeIn : Frame3d Length.Meters coordinates { defines : localCoordinates } -> Entity localCoordinates -> Entity coordinates

Take an entity that is defined in a local coordinate system and convert it to global coordinates. This can be useful if you have some entities which are defined in some local coordinate system like inside a car, and you want to render them within a larger world.

relativeTo : Frame3d Length.Meters coordinates { defines : localCoordinates } -> Entity coordinates -> Entity localCoordinates

Take an entity that is defined in global coordinates and convert into a local coordinate system. This is even less likely to be useful than placeIn, but may be useful if you are (for example) rendering an office scene (and working primarily in local room coordinates) but want to incorporate some entity defined in global coordinates like a bird flying past the window.

Standalone shadows

In some cases you might want to render the shadow of some object without rendering the object itself. This can let you do things like render a high-poly door while rendering its shadow using a simpler approximate shape like a quad or rectangular block to reduce rendering time (rendering shadows of complex meshes can be expensive).

Note that if you do something like this then you will need to be careful to make sure that the approximate object fits inside the actual mesh being rendered - otherwise you might end up with the object effectively shadowing itself.

triangleShadow : Triangle3d Length.Meters coordinates -> Entity coordinates

quadShadow : Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Point3d Length.Meters coordinates -> Entity coordinates

blockShadow : Block3d Length.Meters coordinates -> Entity coordinates

sphereShadow : Sphere3d Length.Meters coordinates -> Entity coordinates

cylinderShadow : Cylinder3d Length.Meters coordinates -> Entity coordinates

coneShadow : Cone3d Length.Meters coordinates -> Entity coordinates

meshShadow : Mesh.Shadow coordinates -> Entity coordinates

Customized rendering

composite : { camera : Camera3d Length.Meters coordinates, clipDepth : Length, antialiasing : Antialiasing, dimensions : ( Quantity Basics.Int Pixels, Quantity Basics.Int Pixels ), background : Background coordinates } -> List { lights : Lights coordinates, exposure : Exposure, toneMapping : ToneMapping, whiteBalance : Light.Chromaticity, entities : List (Entity coordinates) } -> Html msg

Render a 'composite' scene where different subsets of entities in the scene can use different lighting. This can let you do things like:

toWebGLEntities : { lights : Lights coordinates, camera : Camera3d Length.Meters coordinates, clipDepth : Length, exposure : Exposure, toneMapping : ToneMapping, whiteBalance : Light.Chromaticity, aspectRatio : Basics.Float, supersampling : Basics.Float, entities : List (Entity coordinates) } -> List WebGL.Entity

This function lets you convert a list of elm-3d-scene entities into a list of plain elm-explorations/webgl entities, so that you can combine objects rendered with elm-3d-scene with custom objects you render yourself.

Note that the arguments are not exactly the same as custom; there are no background, dimensions or antialiasing arguments since those are properties that must be set at the top level, so you will have to handle those yourself when calling WebGL.toHtml. However, there are a couple of additional arguments:

This function is called internally by custom but has not actually been tested in combination with other custom WebGL code, so there is a high chance of weird interaction bugs. (In particular, if you use the stencil buffer you will likely want to clear it explicitly after rendering elm-3d-scene entities.) If you encounter bugs when using toWebGLEntities in combination with your own custom rendering code, please open an issue or reach out to @ianmackenzie on the Elm Slack.