Skip to content

Commit fa6628e

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

File tree

7 files changed

+24012
-14
lines changed

7 files changed

+24012
-14
lines changed

Diff for: README.md

+3-2
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

@@ -31,14 +31,15 @@ To export an OBJ file from Blender choose `File - Export - Wavefront (.obj)`. We
3131

3232
- **Include:** only check “Objects as OBJ Objects”;
3333
- **Transform:** use scale `1.00`, “Y Forward” and “Z Up” to match the Blender coordinate system;
34-
- **Geometry:** only check “Apply Modifiers”, check “Write Normals” for `Obj.Decode.faces` and `Obj.Decode.texturedFaces`, “Include UVs” for `Obj.Decode.texturedTriangles` and `Obj.Decode.texturedFaces`, optionally check “Write Materials” if you want to decode material names.
34+
- **Geometry:** only check “Apply Modifiers”, check “Write Normals” for `Obj.Decode.faces`, `Obj.Decode.texturedFaces` and `Obj.Decode.bumpyFaces`, “Include UVs” for `Obj.Decode.texturedTriangles`, `Obj.Decode.texturedFaces` and `Obj.Decode.bumpyFaces`, optionally check “Write Materials” if you want to decode material names.
3535

3636
Blender collections are not preserved in OBJ groups. To decode individual meshes from the same file, you should rely on the `object` filter. The object name, that Blender produces, is a concatenation of the corresponding object and geometry. For example, the “Pod Body” object that contains “Mesh.001” can be decoded with `Obj.Decode.object "Pod_Body_Mesh.001"`.
3737

3838
If you want to use the shadow generation functionality from `elm-3d-scene`, your mesh needs to be watertight. Blender has the 3D Print Toolbox add-on, that lets you detect non manifold edges and fix them by clicking the “Make Manifold” button.
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`;

Diff for: 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
}

Diff for: examples/src/Nefertiti.elm

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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+
, Html.text "."
260+
]
261+
, Html.p []
262+
[ Html.text "The demo is compiled from the unpublished version of "
263+
, Html.a
264+
[ Html.Attributes.href "https://github.com/ianmackenzie/elm-3d-scene"
265+
, Html.Attributes.target "_blank"
266+
]
267+
[ Html.text "elm-3d-scene" ]
268+
, Html.text " and "
269+
, Html.a
270+
[ Html.Attributes.href "https://github.com/w0rm/elm-obj-file/pull/13"
271+
, Html.Attributes.target "_blank"
272+
]
273+
[ Html.text "elm-obj-file" ]
274+
, Html.text "."
275+
]
276+
, Html.p []
277+
[ Html.label []
278+
[ Html.input
279+
[ Html.Attributes.type_ "checkbox"
280+
, Html.Attributes.checked model.useBumpyMaterial
281+
, Html.Events.onCheck UseBumpyMaterialToggled
282+
]
283+
[]
284+
, Html.text " Use bumpy material"
285+
]
286+
, Html.label [ Html.Attributes.style "margin-left" "20px" ]
287+
[ Html.input
288+
[ Html.Attributes.type_ "checkbox"
289+
, Html.Attributes.checked model.useColorTexture
290+
, Html.Events.onCheck UseColorTextureToggled
291+
]
292+
[]
293+
, Html.text " Use color texture"
294+
]
295+
]
296+
]
297+
]
298+
299+
_ ->
300+
Html.text "Loading mesh and textures…"
301+
302+
303+
main : Program () Model Msg
304+
main =
305+
Browser.element
306+
{ init = init
307+
, update = update
308+
, view = view
309+
, subscriptions = subscriptions
310+
}
311+
312+
313+
subscriptions : Model -> Sub Msg
314+
subscriptions model =
315+
if model.orbiting then
316+
Sub.batch
317+
[ Browser.Events.onMouseMove decodeMouseMove
318+
, Browser.Events.onMouseUp (Decode.succeed MouseUp)
319+
]
320+
321+
else
322+
Browser.Events.onMouseDown (Decode.succeed MouseDown)
323+
324+
325+
decodeMouseMove : Decoder Msg
326+
decodeMouseMove =
327+
Decode.map2 MouseMove
328+
(Decode.field "movementX" (Decode.map Pixels.float Decode.float))
329+
(Decode.field "movementY" (Decode.map Pixels.float Decode.float))

0 commit comments

Comments
 (0)