Skip to content

Commit f69b107

Browse files
committed
Support tangents for bumpy materials
1 parent 7f30832 commit f69b107

File tree

7 files changed

+23995
-13
lines changed

7 files changed

+23995
-13
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ An Elm package to decode 3D models from the [OBJ file format](https://en.wikiped
66

77
_The “Pod” model by [@01k](https://mobile.twitter.com/01k) rendered with `elm-3d-scene`. [See it live here](https://unsoundscapes.com/elm-obj-file/examples/pod/)._
88

9-
Make sure to check [the viewer example](https://unsoundscapes.com/elm-obj-file/examples/viewer/) that lets you preview OBJ files.
9+
Make sure to check [the viewer example](https://unsoundscapes.com/elm-obj-file/examples/viewer/) that lets you preview OBJ files. [The Nefertiti example](https://unsoundscapes.com/elm-obj-file/examples/nefertit/) demonstrates support for loading bumpy faces with a normal map texture.
1010

1111
The examples source code [can be found here](https://github.com/w0rm/elm-obj-file/tree/main/examples).
1212

@@ -39,6 +39,7 @@ If you want to use the shadow generation functionality from `elm-3d-scene`, your
3939

4040
## OBJ Format Support
4141

42+
- [x] support for tangents needed for bumpy materials
4243
- [x] different combinations of positions, normal vectors and UV (texture coordinates);
4344
- [x] face elements `f`;
4445
- [x] line elements `l`;

examples/elm.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"type": "application",
33
"source-directories": [
44
"src",
5-
"../src"
5+
"../src",
6+
"../../elm-3d-scene/src"
67
],
78
"elm-version": "0.19.1",
89
"dependencies": {
@@ -14,22 +15,21 @@
1415
"elm/html": "1.0.0",
1516
"elm/http": "2.0.0",
1617
"elm/json": "1.1.3",
18+
"elm-explorations/linear-algebra": "1.0.3",
1719
"elm-explorations/webgl": "1.1.3",
20+
"ianmackenzie/elm-1d-parameter": "1.0.1",
1821
"ianmackenzie/elm-3d-camera": "3.1.0",
19-
"ianmackenzie/elm-3d-scene": "1.0.1",
22+
"ianmackenzie/elm-float-extra": "1.1.0",
2023
"ianmackenzie/elm-geometry": "3.6.0",
21-
"ianmackenzie/elm-triangular-mesh": "1.0.4",
24+
"ianmackenzie/elm-geometry-linear-algebra-interop": "2.0.2",
25+
"ianmackenzie/elm-triangular-mesh": "1.1.0",
2226
"ianmackenzie/elm-units": "2.6.0"
2327
},
2428
"indirect": {
2529
"elm/bytes": "1.0.8",
2630
"elm/time": "1.0.0",
2731
"elm/url": "1.0.0",
2832
"elm/virtual-dom": "1.0.2",
29-
"elm-explorations/linear-algebra": "1.0.3",
30-
"ianmackenzie/elm-1d-parameter": "1.0.1",
31-
"ianmackenzie/elm-float-extra": "1.1.0",
32-
"ianmackenzie/elm-geometry-linear-algebra-interop": "2.0.2",
3333
"ianmackenzie/elm-interval": "2.0.0",
3434
"ianmackenzie/elm-units-interval": "1.1.0"
3535
}

examples/src/Nefertiti.elm

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
module Nefertiti exposing (main)
2+
3+
{-| The following example demonstrates loading bumpy faces to render
4+
elm-3d-scene bumpy materials.
5+
6+
You can use Blender to reduce the size of a mesh by baking details
7+
into the normal map: <https://www.katsbits.com/codex/bake-normal-maps/>
8+
9+
The OBJ file was derived from the original “Bust of Nefertiti”
10+
scan by Staatliche Museen zu Berlin – Preußischer Kulturbesitz,
11+
under the CC BY-NC-SA license.
12+
13+
Toggle bumpy material to see if it makes the difference!
14+
15+
-}
16+
17+
import Angle exposing (Angle)
18+
import Browser
19+
import Browser.Events
20+
import Camera3d
21+
import Color exposing (Color)
22+
import Direction3d
23+
import Html exposing (Html)
24+
import Html.Attributes
25+
import Html.Events
26+
import Http
27+
import Json.Decode as Decode exposing (Decoder)
28+
import Length exposing (Meters)
29+
import Obj.Decode exposing (ObjCoordinates)
30+
import Pixels exposing (Pixels)
31+
import Point3d exposing (Point3d)
32+
import Quantity exposing (Quantity, Unitless)
33+
import Scene3d
34+
import Scene3d.Material exposing (Texture)
35+
import Scene3d.Mesh exposing (Bumpy)
36+
import SketchPlane3d
37+
import Task
38+
import TriangularMesh exposing (TriangularMesh)
39+
import Vector3d exposing (Vector3d)
40+
import Viewpoint3d
41+
import WebGL.Texture
42+
43+
44+
type alias Model =
45+
{ azimuth : Angle
46+
, elevation : Angle
47+
, zoom : Float
48+
, orbiting : Bool
49+
, colorTexture : Maybe (Texture Color)
50+
, normalMap : Maybe Scene3d.Material.NormalMap
51+
, mesh : Maybe (Bumpy ObjCoordinates)
52+
, useBumpyMaterial : Bool
53+
, useColorTexture : Bool
54+
}
55+
56+
57+
type Msg
58+
= LoadedColorTexture (Result WebGL.Texture.Error (Texture Color))
59+
| LoadedNormalMap (Result WebGL.Texture.Error Scene3d.Material.NormalMap)
60+
| LoadedMesh
61+
(Result
62+
Http.Error
63+
(TriangularMesh
64+
{ position : Point3d Meters ObjCoordinates
65+
, normal : Vector3d Unitless ObjCoordinates
66+
, uv : ( Float, Float )
67+
, tangent : Vector3d Unitless ObjCoordinates
68+
, tangentBasisIsRightHanded : Bool
69+
}
70+
)
71+
)
72+
| MouseDown
73+
| MouseUp
74+
| MouseMove (Quantity Float Pixels) (Quantity Float Pixels)
75+
| MouseWheel Float
76+
| UseBumpyMaterialToggled Bool
77+
| UseColorTextureToggled Bool
78+
79+
80+
init : () -> ( Model, Cmd Msg )
81+
init () =
82+
( { colorTexture = Nothing
83+
, normalMap = Nothing
84+
, mesh = Nothing
85+
, azimuth = Angle.degrees -50
86+
, elevation = Angle.degrees 15
87+
, orbiting = False
88+
, useBumpyMaterial = False
89+
, useColorTexture = True
90+
, zoom = 0
91+
}
92+
, Cmd.batch
93+
[ Task.attempt LoadedColorTexture (Scene3d.Material.load "NefertitiColor.png")
94+
, Task.attempt LoadedNormalMap (Scene3d.Material.loadNormalMap "NefertitiNormalMap.png")
95+
, Http.get
96+
{ url = "Nefertiti.obj.txt" -- .txt is required to work with `elm reactor`
97+
, expect =
98+
Obj.Decode.expectObj LoadedMesh
99+
Length.meters
100+
Obj.Decode.bumpyFaces
101+
}
102+
]
103+
)
104+
105+
106+
update : Msg -> Model -> ( Model, Cmd Msg )
107+
update msg model =
108+
case msg of
109+
LoadedColorTexture result ->
110+
( { model | colorTexture = Result.toMaybe result }
111+
, Cmd.none
112+
)
113+
114+
LoadedNormalMap result ->
115+
( { model | normalMap = Result.toMaybe result }
116+
, Cmd.none
117+
)
118+
119+
LoadedMesh result ->
120+
( { model
121+
| mesh =
122+
result
123+
|> Result.map Scene3d.Mesh.bumpyFaces
124+
|> Result.map Scene3d.Mesh.cullBackFaces
125+
|> Result.toMaybe
126+
}
127+
, Cmd.none
128+
)
129+
130+
MouseDown ->
131+
( { model | orbiting = True }, Cmd.none )
132+
133+
MouseUp ->
134+
( { model | orbiting = False }, Cmd.none )
135+
136+
MouseMove dx dy ->
137+
if model.orbiting then
138+
let
139+
rotationRate =
140+
Quantity.per Pixels.pixel (Angle.degrees 1)
141+
in
142+
( { model
143+
| azimuth =
144+
model.azimuth
145+
|> Quantity.minus (Quantity.at rotationRate dx)
146+
, elevation =
147+
model.elevation
148+
|> Quantity.plus (Quantity.at rotationRate dy)
149+
|> Quantity.clamp (Angle.degrees -90) (Angle.degrees 90)
150+
}
151+
, Cmd.none
152+
)
153+
154+
else
155+
( model, Cmd.none )
156+
157+
MouseWheel deltaY ->
158+
( { model | zoom = clamp 0 1 (model.zoom - deltaY * 0.002) }, Cmd.none )
159+
160+
UseBumpyMaterialToggled bumpy ->
161+
( { model | useBumpyMaterial = bumpy }, Cmd.none )
162+
163+
UseColorTextureToggled color ->
164+
( { model | useColorTexture = color }, Cmd.none )
165+
166+
167+
view : Model -> Html Msg
168+
view model =
169+
let
170+
viewpoint =
171+
Viewpoint3d.orbitZ
172+
{ focalPoint = Point3d.meters 0 0 0.2
173+
, azimuth = model.azimuth
174+
, elevation = model.elevation
175+
, distance = Length.meters (1.2 - model.zoom * 0.5)
176+
}
177+
178+
sunlightDirection =
179+
Direction3d.fromAzimuthInAndElevationFrom SketchPlane3d.xy
180+
model.azimuth
181+
model.elevation
182+
|> Direction3d.reverse
183+
184+
camera =
185+
Camera3d.perspective
186+
{ viewpoint = viewpoint
187+
, verticalFieldOfView = Angle.degrees 30
188+
}
189+
in
190+
case ( model.colorTexture, model.normalMap, model.mesh ) of
191+
( Just colorTexture, Just normalMapTexture, Just mesh ) ->
192+
let
193+
material =
194+
case ( model.useBumpyMaterial, model.useColorTexture ) of
195+
( True, True ) ->
196+
Scene3d.Material.bumpyNonmetal
197+
{ baseColor = colorTexture
198+
, roughness = Scene3d.Material.constant 0.5
199+
, ambientOcclusion = Scene3d.Material.constant 1
200+
, normalMap = normalMapTexture
201+
}
202+
203+
( False, True ) ->
204+
Scene3d.Material.texturedNonmetal
205+
{ baseColor = colorTexture
206+
, roughness = Scene3d.Material.constant 0.5
207+
}
208+
209+
( True, False ) ->
210+
Scene3d.Material.bumpyNonmetal
211+
{ baseColor = Scene3d.Material.constant Color.blue
212+
, roughness = Scene3d.Material.constant 0.5
213+
, ambientOcclusion = Scene3d.Material.constant 1
214+
, normalMap = normalMapTexture
215+
}
216+
217+
( False, False ) ->
218+
Scene3d.Material.texturedNonmetal
219+
{ baseColor = Scene3d.Material.constant Color.blue
220+
, roughness = Scene3d.Material.constant 0.5
221+
}
222+
in
223+
Html.figure
224+
[ Html.Attributes.style "display" "block"
225+
, Html.Attributes.style "width" "640px"
226+
, Html.Attributes.style "margin" "auto"
227+
, Html.Attributes.style "padding" "20px"
228+
, Html.Events.preventDefaultOn "wheel"
229+
(Decode.map
230+
(\deltaY -> ( MouseWheel deltaY, True ))
231+
(Decode.field "deltaY" Decode.float)
232+
)
233+
]
234+
[ Scene3d.sunny
235+
{ upDirection = Direction3d.z
236+
, sunlightDirection = sunlightDirection
237+
, shadows = True
238+
, camera = camera
239+
, dimensions = ( Pixels.int 640, Pixels.int 640 )
240+
, background = Scene3d.backgroundColor Color.darkGrey
241+
, clipDepth = Length.meters 0.01
242+
, entities = [ Scene3d.mesh material mesh ]
243+
}
244+
, Html.figcaption [ Html.Attributes.style "font" "14px/1.5 sans-serif" ]
245+
[ Html.p []
246+
[ Html.text "This is a simplified version of the "
247+
, Html.a
248+
[ Html.Attributes.href "https://www.thingiverse.com/thing:3974391"
249+
, Html.Attributes.target "_blank"
250+
]
251+
[ Html.text "Bust of Nefertiti"
252+
]
253+
, Html.text " by Staatliche Museen zu Berlin – Preußischer Kulturbesitz, under the "
254+
, Html.a
255+
[ Html.Attributes.href "https://creativecommons.org/licenses/by-nc-sa/4.0/"
256+
, Html.Attributes.target "_blank"
257+
]
258+
[ Html.text "CC BY-NC-SA license" ]
259+
]
260+
, Html.p []
261+
[ Html.label []
262+
[ Html.input
263+
[ Html.Attributes.type_ "checkbox"
264+
, Html.Attributes.checked model.useBumpyMaterial
265+
, Html.Events.onCheck UseBumpyMaterialToggled
266+
]
267+
[]
268+
, Html.text " Use bumpy material"
269+
]
270+
, Html.label [ Html.Attributes.style "margin-left" "20px" ]
271+
[ Html.input
272+
[ Html.Attributes.type_ "checkbox"
273+
, Html.Attributes.checked model.useColorTexture
274+
, Html.Events.onCheck UseColorTextureToggled
275+
]
276+
[]
277+
, Html.text " Use color texture"
278+
]
279+
]
280+
]
281+
]
282+
283+
_ ->
284+
Html.text "Loading mesh and textures…"
285+
286+
287+
main : Program () Model Msg
288+
main =
289+
Browser.element
290+
{ init = init
291+
, update = update
292+
, view = view
293+
, subscriptions = subscriptions
294+
}
295+
296+
297+
subscriptions : Model -> Sub Msg
298+
subscriptions model =
299+
if model.orbiting then
300+
Sub.batch
301+
[ Browser.Events.onMouseMove decodeMouseMove
302+
, Browser.Events.onMouseUp (Decode.succeed MouseUp)
303+
]
304+
305+
else
306+
Browser.Events.onMouseDown (Decode.succeed MouseDown)
307+
308+
309+
decodeMouseMove : Decoder Msg
310+
decodeMouseMove =
311+
Decode.map2 MouseMove
312+
(Decode.field "movementX" (Decode.map Pixels.float Decode.float))
313+
(Decode.field "movementY" (Decode.map Pixels.float Decode.float))

0 commit comments

Comments
 (0)