From bfa54dc8cb1c716e5d7326f2f3d334e4625db38a Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 19 Jul 2021 20:55:18 +0100 Subject: [PATCH] Skybox feature --- Implemented new Skybox feature, and added an API for loading and setting the skybox texture. --- .gitignore | 3 + examples/Skybox.elm | 201 +++++++++++++++++++++++++++++++ src/Scene3d.elm | 81 ++++++++++--- src/Scene3d/Skybox.elm | 47 ++++++++ src/Scene3d/Skybox/Protected.elm | 162 +++++++++++++++++++++++++ 5 files changed, 478 insertions(+), 16 deletions(-) create mode 100644 examples/Skybox.elm create mode 100644 src/Scene3d/Skybox.elm create mode 100644 src/Scene3d/Skybox/Protected.elm diff --git a/.gitignore b/.gitignore index ac45283..d2664ba 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ elm.js .AppleDouble .LSOverride +# VSCode specific files +.vscode + # Thumbnails ._* diff --git a/examples/Skybox.elm b/examples/Skybox.elm new file mode 100644 index 0000000..2f44f30 --- /dev/null +++ b/examples/Skybox.elm @@ -0,0 +1,201 @@ +module Skybox exposing (main) + +{-| This example shows how to load and set a skybox texture. The exmple also +implements camera rotation, and the skybox can be previewed from different +angles. +-} + +import Angle exposing (Angle) +import Block3d +import Browser +import Browser.Dom +import Browser.Events +import Camera3d +import Color +import Direction3d +import Frame3d +import Json.Decode as Decode exposing (Decoder) +import Length +import Pixels exposing (Pixels) +import Point3d exposing (Point3d) +import Quantity exposing (Quantity) +import Scene3d +import Scene3d.Material as Material +import Scene3d.Skybox as Skybox exposing (Skybox, loadEquirectangular) +import Task +import Viewpoint3d +import WebGL.Texture + + +type alias Model = + { width : Quantity Int Pixels -- Width of the browser window + , height : Quantity Int Pixels -- Height of the browser window + , orbiting : Bool + , azimuth : Angle + , elevation : Angle + , skybox : Maybe Skybox + } + + +type Msg + = MouseUp + | MouseDown + | Resize (Quantity Int Pixels) (Quantity Int Pixels) + | MouseMove (Quantity Float Pixels) (Quantity Float Pixels) + | SkyboxLoaded (Result WebGL.Texture.Error Skybox) + + +type WorldCoordinates + = WorldCoordinates + + +main : Program () Model Msg +main = + Browser.document + { init = init + , update = update + , view = view + , subscriptions = subs + } + + +init : () -> ( Model, Cmd Msg ) +init _ = + ( { width = Quantity.zero + , height = Quantity.zero + , orbiting = False + , azimuth = Angle.degrees 135 + , elevation = Angle.degrees 5 + , skybox = Nothing + } + , Cmd.batch + [ Task.perform + (\{ viewport } -> + Resize + (Pixels.int (round viewport.width)) + (Pixels.int (round viewport.height)) + ) + Browser.Dom.getViewport + + -- Load equirectangular texture + , "https://ianmackenzie.github.io/elm-3d-scene/examples/skybox/umhlanga_sunrise_8k.jpg" + |> Skybox.loadEquirectangular + |> Task.attempt SkyboxLoaded + ] + ) + + +mouseMoveDecoder : Decoder Msg +mouseMoveDecoder = + Decode.map2 MouseMove + (Decode.field "movementX" (Decode.map Pixels.float Decode.float)) + (Decode.field "movementY" (Decode.map Pixels.float Decode.float)) + + +subs : Model -> Sub Msg +subs model = + Sub.batch + [ Browser.Events.onResize + (\w h -> Resize (Pixels.int w) (Pixels.int h)) + + -- + , if model.orbiting then + Sub.batch + [ Browser.Events.onMouseMove mouseMoveDecoder + , Browser.Events.onMouseUp (Decode.succeed MouseUp) + ] + + else + Browser.Events.onMouseDown (Decode.succeed MouseDown) + ] + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + Resize w h -> + ( { model | width = w, height = h }, Cmd.none ) + + MouseDown -> + ( { model | orbiting = True }, Cmd.none ) + + MouseUp -> + ( { model | orbiting = False }, Cmd.none ) + + MouseMove dx dy -> + if model.orbiting then + let + rotationRate = + Angle.degrees -0.05 |> Quantity.per Pixels.pixel + + newAzimuth = + model.azimuth + |> Quantity.minus (dx |> Quantity.at rotationRate) + + newElevation = + model.elevation + |> Quantity.plus (dy |> Quantity.at rotationRate) + |> Quantity.clamp (Angle.degrees -60) (Angle.degrees 60) + in + ( { model + | orbiting = True + , azimuth = newAzimuth + , elevation = newElevation + } + , Cmd.none + ) + + else + ( model, Cmd.none ) + + SkyboxLoaded (Ok texture) -> + ( { model | skybox = Just texture }, Cmd.none ) + + SkyboxLoaded (Err err) -> + ( model, Cmd.none ) + + +view : Model -> Browser.Document Msg +view model = + let + camera = + Camera3d.perspective + { viewpoint = + Viewpoint3d.orbitZ + { focalPoint = Point3d.centimeters 0 0 20 + , azimuth = model.azimuth + , elevation = model.elevation + , distance = Length.meters 3 + } + , verticalFieldOfView = Angle.degrees 30 + } + in + { title = "Skybox" + , body = + [ Scene3d.sunny + { upDirection = Direction3d.z + , sunlightDirection = Direction3d.xyZ (Angle.degrees -135) (Angle.degrees -45) + , shadows = False + , dimensions = ( model.width, model.height ) + , camera = camera + , clipDepth = Length.centimeters 10 + , background = + model.skybox + |> Maybe.map Scene3d.backgroundSkybox + |> Maybe.withDefault (Scene3d.backgroundColor Color.lightBlue) + , entities = + [ Scene3d.block + (Material.matte Color.lightBrown) + (Block3d.centeredOn + (Frame3d.atPoint + (Point3d.centimeters 0 0 20) + ) + ( Length.centimeters 0 + , Length.centimeters 0 + , Length.centimeters 0 + ) + ) + ] + } + ] + } diff --git a/src/Scene3d.elm b/src/Scene3d.elm index 52f5801..338d624 100644 --- a/src/Scene3d.elm +++ b/src/Scene3d.elm @@ -6,7 +6,7 @@ module Scene3d exposing , mesh, meshWithShadow , group, nothing , rotateAround, translateBy, translateIn, scaleAbout, mirrorAcross - , Background, transparentBackground, backgroundColor + , Background, transparentBackground, backgroundColor, backgroundSkybox , Antialiasing , noAntialiasing, multisampling, supersampling , Lights @@ -114,7 +114,7 @@ entity: # Background -@docs Background, transparentBackground, backgroundColor +@docs Background, transparentBackground, backgroundColor, backgroundSkybox # Antialiasing @@ -213,6 +213,8 @@ import Scene3d.Entity as Entity import Scene3d.Light as Light exposing (Chromaticity, Light) import Scene3d.Material as Material exposing (Material) import Scene3d.Mesh as Mesh exposing (Mesh) +import Scene3d.Skybox +import Scene3d.Skybox.Protected import Scene3d.Transformation as Transformation exposing (Transformation) import Scene3d.Types as Types exposing (Bounds, DrawFunction, LightMatrices, LinearRgb(..), Material(..), Node(..)) import Sphere3d exposing (Sphere3d) @@ -937,6 +939,7 @@ current environmental lighting. -} type Background coordinates = BackgroundColor Color + | BackgroundSkybox Scene3d.Skybox.Skybox {-| A fully transparent background. @@ -953,6 +956,36 @@ backgroundColor color = BackgroundColor color +toBackgroundColorString : Background coordinates -> Maybe String +toBackgroundColorString bkg = + case bkg of + BackgroundColor color -> + Just (Color.toCssString color) + + _ -> + Nothing + + +{-| Provides a way to set a skybox background! + +Before the skybox can be set, a skybox texture must be loaded first. At the +moment, only equirectangular skybox textures are supported. On how to load a +skybox texture, please refer to the `Scene3d.Skybox` module. + +Once the skybox texture is ready, it can be set as the background when +initialising a `Scene3d` scene. For example: + + Scene3d.sunny + { background = Scene3d.backgroundSkybox loadedSkyboxTexture + , ... + } + +-} +backgroundSkybox : Scene3d.Skybox.Skybox -> Background coordinates +backgroundSkybox texture = + BackgroundSkybox texture + + ----- RENDERING ----- @@ -1698,12 +1731,6 @@ composite arguments scenes = heightInPixels = Pixels.toInt height - (BackgroundColor givenBackgroundColor) = - arguments.background - - backgroundColorString = - Color.toCssString givenBackgroundColor - commonWebGLOptions = [ WebGL.depth 1 , WebGL.stencil 0 @@ -1753,14 +1780,36 @@ composite arguments scenes = Html.Keyed.node "div" [ Html.Attributes.style "padding" "0px", widthCss, heightCss ] <| [ ( key , WebGL.toHtmlWith webGLOptions - [ Html.Attributes.width (round (toFloat widthInPixels * scalingFactor)) - , Html.Attributes.height (round (toFloat heightInPixels * scalingFactor)) - , widthCss - , heightCss - , Html.Attributes.style "display" "block" - , Html.Attributes.style "background-color" backgroundColorString - ] - webGLEntities + ([ Html.Attributes.width (round (toFloat widthInPixels * scalingFactor)) + , Html.Attributes.height (round (toFloat heightInPixels * scalingFactor)) + , widthCss + , heightCss + , Html.Attributes.style "display" "block" + ] + ++ (case toBackgroundColorString arguments.background of + Just colorStr -> + [ Html.Attributes.style "background-color" colorStr + ] + + Nothing -> + [] + ) + ) + (List.concat + [ case arguments.background of + BackgroundSkybox skybox -> + [ Scene3d.Skybox.Protected.quad + { camera = arguments.camera + , aspectRatio = aspectRatio + , skybox = skybox + } + ] + + _ -> + [] + , webGLEntities + ] + ) ) ] diff --git a/src/Scene3d/Skybox.elm b/src/Scene3d/Skybox.elm new file mode 100644 index 0000000..3fbaccc --- /dev/null +++ b/src/Scene3d/Skybox.elm @@ -0,0 +1,47 @@ +module Scene3d.Skybox exposing + ( Skybox + , loadEquirectangular + ) + +{-| + +@docs Skybox + + +# Loading a Skybox texture + +@docs loadEquirectangular + +-} + +import Scene3d.Skybox.Protected exposing (Skybox(..)) +import Task exposing (Task) +import WebGL.Texture + + +{-| Loaded and ready to use equirectangular `Skybox` texture. +-} +type alias Skybox = + Scene3d.Skybox.Protected.Skybox + + + +-- LOADING EQUIRECTANGULAR SKYBOX TEXTURE + + +{-| Function which defines a task for loading an equirectangular texture, +which can then be applied as a `Scene3d` skybox background. + +The first argument is the path (absolute or relative) to the texture. + +-} +loadEquirectangular : String -> Task WebGL.Texture.Error Skybox +loadEquirectangular = + Task.map EquirectTexture + << WebGL.Texture.loadWith + { magnify = WebGL.Texture.linear + , minify = WebGL.Texture.nearest + , horizontalWrap = WebGL.Texture.clampToEdge + , verticalWrap = WebGL.Texture.clampToEdge + , flipY = True + } diff --git a/src/Scene3d/Skybox/Protected.elm b/src/Scene3d/Skybox/Protected.elm new file mode 100644 index 0000000..a5e9b80 --- /dev/null +++ b/src/Scene3d/Skybox/Protected.elm @@ -0,0 +1,162 @@ +module Scene3d.Skybox.Protected exposing (Skybox(..), SkyboxQuad, quad) + +import Camera3d exposing (Camera3d) +import Geometry.Interop.LinearAlgebra.Point3d as Point3d +import Length exposing (Meters) +import Math.Matrix4 exposing (Mat4) +import Math.Vector2 exposing (Vec2, vec2) +import Math.Vector3 exposing (Vec3) +import Point3d +import Quantity exposing (Quantity(..)) +import Viewpoint3d +import WebGL +import WebGL.Matrices +import WebGL.Texture as WebGL + + + +-- SKYBOX + + +type Skybox + = EquirectTexture WebGL.Texture + + +toTexture : Skybox -> WebGL.Texture +toTexture (EquirectTexture texture) = + texture + + + +-- SKYBOX QUAD + + +type alias SkyboxQuad = + WebGL.Entity + + +quad : + { camera : Camera3d Meters coordinates + , aspectRatio : Float + , skybox : Skybox + } + -> SkyboxQuad +quad { camera, aspectRatio, skybox } = + let + eyeCamera = + camera + |> Camera3d.viewpoint + |> Viewpoint3d.eyePoint + |> Point3d.toVec3 + + inverseViewProjectionMatrix = + WebGL.Matrices.viewProjectionMatrix camera + { aspectRatio = aspectRatio + , nearClipDepth = Quantity 0.01 + , farClipDepth = Quantity 1.01 + } + |> Math.Matrix4.inverse + |> Maybe.withDefault Math.Matrix4.identity + in + WebGL.entity + skyboxVertexShader + skyboxFragmentShader + mesh + { skyboxTexture = toTexture skybox + , eyePoint = eyeCamera + , inverseViewProjectionMatrix = inverseViewProjectionMatrix + } + + + +-- SKYBOX SHADERS + + +type alias SkyboxVertex = + { position : Vec2 + } + + +type alias SkyboxVarying = + { vposition : Vec2 + } + + +type alias SkyboxUniforms = + { skyboxTexture : WebGL.Texture + , eyePoint : Vec3 + , inverseViewProjectionMatrix : Mat4 + } + + +skyboxVertexShader : WebGL.Shader SkyboxVertex SkyboxUniforms SkyboxVarying +skyboxVertexShader = + [glsl| + attribute vec2 position; + varying vec2 vposition; + + void main() { + vposition = position; + gl_Position = vec4(position.x, position.y, 0, 1.0); + } + |] + + +skyboxFragmentShader : WebGL.Shader {} SkyboxUniforms SkyboxVarying +skyboxFragmentShader = + [glsl| + precision mediump float; + + const float PI = 3.1415926535897932384626433832795; + const float M_PI = 1.0 / PI; + const float M_2PI = 1.0 / (2.0 * PI); + + uniform vec3 eyePoint; + uniform sampler2D skyboxTexture; + uniform mat4 inverseViewProjectionMatrix; + + varying vec2 vposition; + + void main() { + vec2 textureCoordinate; + + vec4 projPos = inverseViewProjectionMatrix * vec4(vposition.x, vposition.y, 0.0, 1.0); + vec3 skyboxPoint = projPos.xyz/projPos.w; + + vec3 skyboxRay = normalize(skyboxPoint - eyePoint); + + textureCoordinate.x = 0.5 + atan(skyboxRay.x, skyboxRay.y) * M_2PI; + textureCoordinate.y = 0.5 + asin(skyboxRay.z) * M_PI; + + gl_FragColor = texture2D(skyboxTexture, textureCoordinate); + } + |] + + + +-- SKYBOX MESH + + +type alias SkyboxMesh = + WebGL.Mesh SkyboxVertex + + +mesh : SkyboxMesh +mesh = + let + bottomLeft = + SkyboxVertex (vec2 -1 -1) + + bottomRight = + SkyboxVertex (vec2 1 -1) + + topLeft = + SkyboxVertex (vec2 -1 1) + + topRight = + SkyboxVertex (vec2 1 1) + in + WebGL.triangles + [ ( bottomLeft, bottomRight, topLeft ) + , ( topLeft, bottomRight, topRight ) + ]