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!
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:
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:
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:
Direction3d.negativeZ
if
positive Z is up and the sun is directly overhead).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.
Types.Entity coordinates
An Entity
is a shape or group of shapes in a scene.
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:
quad
s and sphere
s support all materials, including textured ones.block
s, cylinder
s, cone
s and facet
s only support uniform
(non-textured) materials.point
s, lineSegment
s and triangle
s only support plain materials
(solid colors or emissive 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.
lineSegment : Material.Plain coordinates -> LineSegment3d Length.Meters coordinates -> Entity coordinates
Draw a single line segment.
triangle : Material.Plain coordinates -> Triangle3d Length.Meters coordinates -> Entity coordinates
Draw a 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.
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:
block : Material.Uniform coordinates -> Block3d Length.Meters coordinates -> Entity coordinates
Draw a rectangular block using the Block3d
type from elm-geometry
.
sphere : Material.Textured coordinates -> Sphere3d Length.Meters coordinates -> Entity coordinates
Draw a sphere using the Sphere3d
type from elm-geometry
.
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
.
cone : Material.Uniform coordinates -> Cone3d Length.Meters coordinates -> Entity coordinates
Draw a cone using the Cone3d
type from elm-geometry
.
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
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
blockWithShadow : Material.Uniform coordinates -> Block3d Length.Meters coordinates -> Entity coordinates
sphereWithShadow : Material.Textured coordinates -> Sphere3d Length.Meters coordinates -> Entity coordinates
cylinderWithShadow : Material.Uniform coordinates -> Cylinder3d Length.Meters coordinates -> Entity coordinates
coneWithShadow : Material.Uniform coordinates -> Cone3d Length.Meters coordinates -> Entity coordinates
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!
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).
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
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:
nothing : Entity coordinates
A dummy entity for which nothing will be drawn.
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:
The following examples all use this duckling as the original (untransformed) entity:
rotateAround : Axis3d Length.Meters coordinates -> Angle -> Entity coordinates -> Entity coordinates
Rotate an entity around a given axis by a given angle.
translateBy : Vector3d Length.Meters coordinates -> Entity coordinates -> Entity coordinates
Translate (move) an entity by a given displacement vector.
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).
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.
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.
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.
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 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 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:
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:
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:
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:
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):
Note how this version is brighter overall than the original, non-tone-mapped image but doesn't suffer from the blown-out highlights.
You're unlikely to need these functions right away but they can be very useful when setting up more complex scenes.
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.
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
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:
aspectRatio
(width over height) that the scene is
being rendered at, so that projection matrices can be computed correctly.
(In Scene3d.custom
, aspect ratio is computed from the given dimensions.)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.