Importing code from previous merged project

This commit is contained in:
Joseph Ferano 2019-02-13 21:27:47 -04:00
commit 33a452ad83
26 changed files with 8193 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
/assg3-distrib/
*.pdf
*.log
*.swp
*.suo
*.html
presentation/
final/
.idea/
*.zip
elm-stuff/**
!elm-stuff/packages/
!elm-stuff/packages/Zinggi/
!elm-stuff/packages/Zinggi/elm-obj-loader/
!elm-stuff/packages/Zinggi/elm-obj-loader/1.0.3/
!elm-stuff/packages/Zinggi/elm-obj-loader/1.0.3/**

39
README.org Normal file
View File

@ -0,0 +1,39 @@
### FPS Scene
You can walk around the scene with simple FPS controls.
Open `scene.html` with an http server, you can use the following;
```python3 -m http.server 8000```
Unfortunately, elm-reactor has issues with loading obj files so that's why the
python server is needed.
![alt text](screenshots/elm-fps.png "FPS Scene")
##### Controls
Mouse - Look Rotation
WASD - Player movement
◀ ▼ ▲ ▶ - Move the Robot
N and M - Rotate robot left and right
Y and H - Rotate robot arm up and down
U and J - Rotate robot hand up and down
### PQTorusknot
Either open up `torus.html`, or use the command `elm-reactor`, if you want to be able to modify the source file and compile;
https://guide.elm-lang.org/install.html
Alternatively, here's the Ellie link
https://ellie-app.com/vVTgpBj77ra1
![alt text](screenshots/elm-knot.png "PQ Torus Knot")

379
Scene.elm Normal file
View File

@ -0,0 +1,379 @@
module Scene exposing (..)
import Html exposing (Html)
import Html.Attributes exposing (width, height, style)
import AnimationFrame
import Time exposing (Time)
import Dict exposing (..)
import Math.Matrix4 as Mat4 exposing (Mat4)
import Math.Vector2 as Vec2 exposing (vec2, Vec2)
import Math.Vector3 as Vec3 exposing (vec3, Vec3)
import WebGL exposing (Mesh, Shader)
import Mouse
import Keyboard exposing (..)
import Task
import WebGL.Texture as Texture exposing (..)
import Color exposing (..)
import Window
import OBJ
import OBJ.Types
import Shaders exposing (..)
type alias ObjVert = OBJ.Types.Vertex
type alias ObjMesh = OBJ.Types.MeshWith ObjVert
main : Program Never Model Msg
main = Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions }
type alias Model =
{ texDict : Dict String Texture
, objDict : Dict String ObjMesh
, rot : Float
, winSize : Window.Size
, keys : Keys
, pos : Vec3
, pitchAndYaw : ( Float , Float )
, lookDir : Vec3
, lastMousePos : Mouse.Position
, robot : Robot }
type alias Robot = { pos : Vec3 , rot : Float , armRot : Float , handRot : Float }
type alias Keys =
{ left : Bool , down : Bool , up : Bool , right : Bool
, w : Bool , a : Bool , s : Bool , d : Bool
, y : Bool , h : Bool , u : Bool , j : Bool
, n : Bool , m : Bool }
type Msg
= TextureLoaded (Result Texture.Error (String , Texture))
| ObjLoaded (Result String (String , ObjMesh))
| Animate Time
| WindowResized Window.Size
| MouseMove Mouse.Position
| KeyChange Bool Keyboard.KeyCode
init: ( Model , Cmd Msg )
init =
( Model
Dict.empty
Dict.empty
0
(Window.Size 1 1)
-- God this is horrendous
(Keys False False False False False False False False False False False False False False)
(vec3 0 0 -10)
(0 , -90)
(vec3 0 0 1)
{ x = 0 , y = 0 }
{ pos = vec3 0 -0.5 -3 , rot = 45 , armRot = 0 , handRot = 0 }
, Cmd.batch
[ loadTex "Thwomp" "textures/thwomp-face.jpg"
, loadTex "UV" "textures/uv_big.png"
, loadTex "Tetra" "textures/tetra.png"
, loadObj "Teapot" "models/suz.obj"
, Task.perform WindowResized Window.size] )
loadTex: String -> String -> Cmd Msg
loadTex id path =
Task.attempt TextureLoaded (Texture.load path |> Task.map (\t -> (id , t) ) )
loadObj: String -> String -> Cmd Msg
loadObj id path =
OBJ.loadMeshWithoutTexture path (\ r -> Result.map (\ o -> (id , o)) r ) |> Cmd.map ObjLoaded
subscriptions: Model -> Sub Msg
subscriptions _ =
Sub.batch
[ AnimationFrame.diffs Animate
, Window.resizes WindowResized
, Mouse.moves MouseMove
, Keyboard.downs (KeyChange True)
, Keyboard.ups (KeyChange False) ]
update: Msg -> Model -> (Model , Cmd Msg)
update msg model =
let m = case msg of
KeyChange b k -> { model | keys = getKeys b k model.keys }
Animate dt ->
{ model
| pos = movePos
(model.keys.a , model.keys.s , model.keys.w , model.keys.d)
model.lookDir
model.pos
0.2
, robot = updateRobot model dt
, rot = model.rot + 0.001 * dt}
WindowResized size -> { model | winSize = size }
TextureLoaded result ->
case result of
Ok (id , tex) -> { model | texDict = Dict.insert id tex model.texDict }
Err e -> model
ObjLoaded result ->
case result of
Ok (id , obj) -> { model | objDict = Dict.insert id obj model.objDict }
Err e -> model |> Debug.log e
MouseMove mp ->
let (ld , py) = getLookPos model.lastMousePos mp model.pitchAndYaw
in { model | lookDir = ld , lastMousePos = mp , pitchAndYaw = py }
in ( m , Cmd.none )
updateRobot: Model -> Time -> Robot
updateRobot { robot , keys } dt =
let rr part = part + dt * 0.005
rl part = part - dt * 0.005
rot = radians robot.rot
in { pos =
movePos
(keys.left, keys.down, keys.up, keys.right)
(vec3 (sin rot) (sin rot) (cos rot))
robot.pos
0.1
, rot = if keys.n then rr robot.rot else if keys.m then rl robot.rot else robot.rot
, armRot =
let angle = if keys.y then rr robot.armRot else if keys.h then rl robot.armRot else robot.armRot
in clamp -0.5 2.5 angle
, handRot =
let angle = if keys.u then rr robot.handRot else if keys.j then rl robot.handRot else robot.handRot
in clamp -1 1 angle}
view: Model -> Html Msg
view model =
WebGL.toHtml
[ width model.winSize.width , height model.winSize.height , style [ ( "display" , "block") ] ]
([ getEntity model wall texturedPlane "Thwomp"
, getEntity model tetraB tetraBasic "UV"
, getEntity model tetra tetraF "Tetra"
, getRobot model
, getModel model (Mat4.makeRotate model.rot Vec3.j) "Teapot"
, getEntity model floor texturedPlane "UV" ] |> List.concat)
getRobot: Model -> List WebGL.Entity
getRobot model =
let body = Mat4.makeTranslate model.robot.pos
|> Mat4.rotate model.robot.rot Vec3.j
arm = Mat4.makeTranslate3 0 0 -0.35
|> Mat4.rotate model.robot.armRot Vec3.i
|> Mat4.inverse |> Maybe.withDefault Mat4.identity
|> Mat4.mul (Mat4.makeTranslate3 0 0.5 0.5)
|> Mat4.mul body
hand = Mat4.makeTranslate3 0 0 -0.25
|> Mat4.rotate model.robot.handRot Vec3.i
|> Mat4.inverse |> Maybe.withDefault Mat4.identity
|> Mat4.mul (Mat4.makeTranslate3 0 0 0.6)
|> Mat4.mul arm
in [ getEntity3 model (body |> Mat4.scale3 0.5 0.5 0.5) cube Color.blue
, getEntity3 model (arm |> Mat4.scale3 0.2 0.2 0.5) cube Color.green
, getEntity3 model (hand |> Mat4.scale3 0.15 0.15 0.45) cube Color.red
] |> List.concat
getModel: Model -> Mat4 -> String -> List WebGL.Entity
getModel model local id =
case Dict.get id model.objDict of
Just mesh ->
case Dict.get "UV" model.texDict of
Just t -> [ WebGL.entity
diffuseVS
diffuseFS
(WebGL.indexedTriangles mesh.vertices mesh.indices)
(DiffuseColor
(projectionMatrix model)
(viewMatrix model)
local
(colorToVec3 Color.blue)
(vec3 1 1 0)
(vec3 1 0 1)
(vec3 0 0 1)
1.0) ]
Nothing -> []
Nothing -> []
getEntity: Model -> Mat4 -> Mesh UTVertex -> String -> List WebGL.Entity
getEntity model local mesh texId =
case Dict.get texId model.texDict of
Just t ->
[ WebGL.entity
unlitTexturedVS
unlitTexturedFS
mesh
(UnlitTextured (projectionMatrix model) (viewMatrix model ) local t) ]
Nothing -> []
getEntity2: Model -> Mat4 -> Mesh ObjVert -> Color -> List WebGL.Entity
getEntity2 model local mesh color =
[ WebGL.entity
unlitColorVS
unlitColorFS
mesh
(UnlitColor (projectionMatrix model) (viewMatrix model ) local (colorToVec3 color) ) ]
getEntity3: Model -> Mat4 -> Mesh ObjVert -> Color -> List WebGL.Entity
getEntity3 model local mesh color =
[ WebGL.entity
diffuseVS
diffuseFS
mesh
(DiffuseColor
(projectionMatrix model)
(viewMatrix model)
local
(colorToVec3 color)
(vec3 1 1 0)
(vec3 1 0 1)
(vec3 0 0 1)
1.0 ) ]
projectionMatrix: Model -> Mat4
projectionMatrix model =
Mat4.makePerspective 50 (toFloat model.winSize.width / toFloat model.winSize.height) 0.01 1000
viewMatrix: Model -> Mat4
viewMatrix model =
Mat4.makeLookAt model.pos (Vec3.add model.pos model.lookDir) Vec3.j
getLookPos: Mouse.Position -> Mouse.Position -> ( Float , Float ) -> ( Vec3 , (Float , Float) )
getLookPos lmp mp ( lastPitch , lastYaw ) =
let sensitivity = 0.0039
rangeY = 89
ox = mp.x - lmp.x |> toFloat
oy = lmp.y - mp.y |> toFloat
yaw = ox * sensitivity + lastYaw |> radians
pitch = -oy * sensitivity + lastPitch |> radians
pitch_ = if pitch > rangeY then rangeY else if pitch < -rangeY then -rangeY else pitch
lookDir = vec3 (cos yaw * cos pitch_) (sin pitch_) (sin yaw * cos pitch_)
in (Vec3.normalize lookDir , ( pitch_ , yaw ) )
colorToVec3: Color -> Vec3
colorToVec3 color =
let to01 x = toFloat x / 255
c = Color.toRgb color
in vec3 (to01 c.red) (to01 c.green) (to01 c.blue)
movePos: (Bool, Bool, Bool, Bool) -> Vec3 -> Vec3 -> Float -> Vec3
movePos ( left , down , up , right ) lookDir pos speed =
let lookDir_ = Vec3.setY 0 lookDir
forward = if up then 1 else if down then -1 else 0
strafe = if right then 1 else if left then -1 else 0
cross = Vec3.cross lookDir_ Vec3.j
dir = Vec3.add (Vec3.scale strafe cross) (Vec3.scale forward lookDir_)
dir_ = if Vec3.length dir <= 0 then dir else Vec3.normalize dir
in Vec3.add pos <| Vec3.scale speed dir_
getKeys: Bool -> KeyCode -> Keys -> Keys
getKeys isOn code keys =
case code of
-- ◀ ▼ ▲ ▶
37 -> { keys | left = isOn }
40 -> { keys | down = isOn }
38 -> { keys | up = isOn }
39 -> { keys | right = isOn }
-- WASD
87 -> { keys | w = isOn }
65 -> { keys | a = isOn }
83 -> { keys | s = isOn }
68 -> { keys | d = isOn }
-- YHUJNM
89 -> { keys | y = isOn }
72 -> { keys | h = isOn }
85 -> { keys | u = isOn }
74 -> { keys | j = isOn }
78 -> { keys | n = isOn }
77 -> { keys | m = isOn }
_ -> keys
-------------
-- Geometry
-------------
wall = Mat4.makeTranslate3 0 0 3
floor = Mat4.makeTranslate3 0 -1 0
|> Mat4.rotate (pi / -2) ( vec3 1 0 0)
|> Mat4.rotate pi ( vec3 0 0 1)
|> Mat4.scale3 15 15 0
tetraB = Mat4.makeTranslate3 -5 1.5 5
|> Mat4.scale3 2 2 2
tetra = Mat4.makeTranslate3 5 0 5
-- right/left front/back top/bottom
cube: Mesh ObjVert
cube =
let rtf = vec3 1 1 1
ltf = vec3 -1 1 1
ltb = vec3 -1 1 -1
rtb = vec3 1 1 -1
rbb = vec3 1 -1 -1
rbf = vec3 1 -1 1
lbf = vec3 -1 -1 1
lbb = vec3 -1 -1 -1
front = Vec3.k
back = Vec3.scale -1 front
top = Vec3.j
bottom = Vec3.scale -1 top
right = Vec3.i
left = Vec3.scale -1 right
in
[ face right rtf rbf rbb rtb
, face left ltf lbf lbb ltb
, face front rtf rbf lbf ltf
, face back rtb rbb lbb ltb
, face top rtf ltf ltb rtb
, face bottom rbf lbf lbb rbb
] |> List.concat
|> WebGL.triangles
face: Vec3 -> Vec3 -> Vec3 -> Vec3 -> Vec3 -> List (ObjVert , ObjVert , ObjVert)
face norm a b c d =
let v pos = OBJ.Types.Vertex pos norm
in [ ( v a , v b , v c ) , ( v c , v d , v a ) ]
tetraBasic: Mesh UTVertex
tetraBasic =
let peak = UTVertex (vec3 0 1 0) (vec2 1 1)
bottomLeft = UTVertex (vec3 -1 -1 -1) (vec2 0 0)
bottomRight = UTVertex (vec3 -1 -1 1) (vec2 1 0)
topLeft = UTVertex (vec3 1 -1 1) (vec2 0 0)
topRight = UTVertex (vec3 1 -1 -1) (vec2 0 1)
in [ ( peak , bottomLeft , bottomRight )
, ( peak , bottomLeft , topRight )
, ( peak , bottomRight , topLeft )
, ( peak , topRight , topLeft )
, ( bottomLeft , bottomRight , topRight)
, ( bottomRight, topLeft , topRight ) ]
|> WebGL.triangles
tetraF: Mesh UTVertex
tetraF =
let f0a = UTVertex (vec3 -1 -1 1) (vec2 0 0.5)
f0b = UTVertex (vec3 1 -1 1) (vec2 0.5 0.5)
f0c = UTVertex (vec3 0 1 0) (vec2 0.25 1)
f1a = UTVertex (vec3 -1 -1 -1) (vec2 0.5 0.5)
f1b = UTVertex (vec3 -1 -1 1) (vec2 1 0.5)
f1c = UTVertex (vec3 0 1 0) (vec2 0.75 1)
f2a = UTVertex (vec3 1 -1 -1) (vec2 0 0)
f2b = UTVertex (vec3 -1 -1 -1) (vec2 0.5 0)
f2c = UTVertex (vec3 0 1 0) (vec2 0.25 0.5)
f3a = UTVertex (vec3 1 -1 1) (vec2 0.5 0)
f3b = UTVertex (vec3 1 -1 -1) (vec2 1 0)
f3c = UTVertex (vec3 0 1 0) (vec2 0.75 0.5)
in [ ( f0a , f0b , f0c ) , ( f1a , f1b , f1c ) , ( f2a , f2b , f2c ) , ( f3a , f3b , f3c ) ]
|> WebGL.triangles
texturedPlane: Mesh UTVertex
texturedPlane =
let topLeft = UTVertex (vec3 -1 1 1) (vec2 0 1)
topRight = UTVertex (vec3 1 1 1) (vec2 1 1)
bottomLeft = UTVertex (vec3 -1 -1 1) (vec2 0 0)
bottomRight = UTVertex (vec3 1 -1 1) (vec2 1 0)
in [ ( topLeft, topRight, bottomLeft ) , ( bottomLeft, topRight, bottomRight ) ]
|> WebGL.triangles

130
Shaders.elm Normal file
View File

@ -0,0 +1,130 @@
module Shaders exposing (..)
import Math.Matrix4 as Mat4 exposing (Mat4)
import Math.Vector2 as Vec2 exposing (vec2, Vec2)
import Math.Vector3 as Vec3 exposing (vec3, Vec3)
import WebGL exposing (Mesh, Shader)
import WebGL.Texture as Texture exposing (..)
import OBJ
import OBJ.Types
type alias UTVertex = { position : Vec3 , coord : Vec2 }
type alias ColorVertex = { position : Vec3 , normal : Vec3 , color : Vec3 }
type alias UnlitColor = { projection : Mat4 , view : Mat4 , model : Mat4 , color : Vec3 }
type alias UnlitTextured = { projection : Mat4 , view : Mat4 , model : Mat4 , texture : Texture }
type alias DiffuseColor =
{ projection : Mat4
, view : Mat4
, model : Mat4
, color : Vec3
, ambient : Vec3
, diffuse : Vec3
, specular : Vec3
, shininess : Float }
diffuseVS: Shader { position : Vec3 , normal : Vec3 } DiffuseColor { vlightWeight : Vec3 }
diffuseVS =
[glsl|
attribute vec3 position;
attribute vec3 normal;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform vec3 ambient;
uniform vec3 diffuse;
uniform vec3 specular;
uniform float shininess;
varying vec3 vlightWeight;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
vec3 lightDir = normalize(vec3(0.0, -0.0, -1.0));
vec4 norm = model * vec4(normal, 0.0);
vec3 n = norm.xyz;
float dir = max(dot(n, lightDir), 0.0);
float v = 0.5;
vlightWeight = diffuse * dir + vec3(v, v, v);
}
|]
diffuseFS: Shader {} DiffuseColor { vlightWeight : Vec3 }
diffuseFS =
[glsl|
precision mediump float;
uniform vec3 color;
varying vec3 vlightWeight;
void main()
{
gl_FragColor = vec4(color * vlightWeight, 1.0);
}
|]
unlitColorVS: Shader OBJ.Types.Vertex UnlitColor { vcolor : Vec3 }
unlitColorVS =
[glsl|
attribute vec3 position;
attribute vec3 normal;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform vec3 color;
varying vec3 vcolor;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
vcolor = color;
}
|]
unlitColorFS: Shader {} UnlitColor { vcolor : Vec3 }
unlitColorFS =
[glsl|
precision mediump float;
varying vec3 vcolor;
void main()
{
gl_FragColor = vec4(vcolor, 1.0);
}
|]
unlitTexturedVS: Shader { position : Vec3 , coord : Vec2 } UnlitTextured { vcoord : Vec2 }
unlitTexturedVS =
[glsl|
attribute vec3 position;
attribute vec2 coord;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
varying vec2 vcoord;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
vcoord = coord;
}
|]
unlitTexturedFS: Shader {} UnlitTextured { vcoord : Vec2 }
unlitTexturedFS =
[glsl|
precision mediump float;
uniform sampler2D texture;
varying vec2 vcoord;
void main()
{
gl_FragColor = texture2D(texture, vcoord);
}
|]

22
elm-package.json Normal file
View File

@ -0,0 +1,22 @@
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"Zinggi/elm-obj-loader": "1.0.3 <= v < 2.0.0",
"elm-community/linear-algebra": "3.1.2 <= v < 4.0.0",
"elm-community/webgl": "2.0.5 <= v < 3.0.0",
"elm-lang/animation-frame": "1.0.1 <= v < 2.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/keyboard": "1.0.1 <= v < 2.0.0",
"elm-lang/mouse": "1.0.1 <= v < 2.0.0",
"elm-lang/window": "1.0.1 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@ -0,0 +1,6 @@
elm-stuff/
index.html
src/Main.elm
documentation.json
# this is only for debugging
src/Native/

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Florian Zinggeler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,34 @@
# elm-obj-loader
This is a small library for importing [wavefront .obj][objSpecs] files into your WebGL application.
# Demo/Examples
* [Model viewer][modelViewer]
* [Suzanne from Blender][suzanne]
[Src](/examples)
# Limitations
Only a small subset of the .obj file specification is supported.
The easiest is to export/convert your model from Blender or another 3D content creation tool as an .obj file.
The default export options from blender work fine. Make sure you keep 'Write Normals' selected.
Completely supported face types are those with vertex position, normals and optionally vertex texture coordinates. (`v`, `vn`, `vt`)
Smooth groups are ignored (`s n`).
Your model needs vertex normals (`vn`).
Only tris and quads are supported. (Tris might load slightly faster)
No free-form curves/surfaces.
No material library (mtllib) support.
No negative indexing.
[suzanne]: https://zinggi.github.io/randomDemos/webgl/objLoader_simple.html
[modelViewer]: https://zinggi.github.io/randomDemos/webgl/objLoader_modelViewer.html
[objSpecs]: http://www.martinreddy.net/gfx/3d/OBJ.spec

View File

@ -0,0 +1,22 @@
{
"version": "1.0.3",
"summary": "Load wavefront .obj files in your WebGL scene.",
"repository": "https://github.com/Zinggi/elm-obj-loader.git",
"license": "MIT",
"source-directories": [
"src"
],
"exposed-modules": [
"OBJ",
"OBJ.Types"
],
"dependencies": {
"Bogdanp/elm-combine": "3.1.1 <= v < 4.0.0",
"Skinney/elm-array-exploration": "2.0.1 <= v < 3.0.0",
"elm-community/linear-algebra": "3.1.0 <= v < 4.0.0",
"elm-community/webgl": "2.0.0 <= v < 3.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-lang/http": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@ -0,0 +1,3 @@
elm-stuff/
index.html
testObjs/

View File

@ -0,0 +1,299 @@
module ElmLogo exposing (..)
import AnimationFrame
import Dict exposing (Dict)
import Html exposing (Html, div, text)
import Html.Attributes as Attr
import Html.Events exposing (on, onInput, onCheck)
import Json.Decode as JD
import Math.Matrix4 as M4 exposing (Mat4)
import Math.Vector3 as V3 exposing (Vec3, vec3)
import Task
import WebGL as GL
import WebGL.Texture
import WebGL.Settings exposing (cullFace, front)
import WebGL.Settings.DepthTest as DepthTest
import Mouse
import Window
--
import Shaders
import OBJ
import OBJ.Types exposing (ObjFile, Mesh(..))
main : Program Never Model Msg
main =
Html.program
{ init = ( initModel, initCmd )
, view = view
, subscriptions = subscriptions
, update = update
}
-- MODEL
type alias Model =
{ time : Float
, mesh : Result String ObjFile
, currentModel : String
, zoom : Float
, diffText : Result String GL.Texture
, normText : Result String GL.Texture
, isDown : Bool
, lastMousePos : Mouse.Position
, mouseDelta : MouseDelta
, windowSize : Window.Size
, withTangent : Bool
}
type alias MouseDelta =
{ x : Float, y : Float }
initModel : Model
initModel =
{ mesh = Err "loading ..."
, currentModel = "meshes/elmLogo.obj"
, time = 0
, zoom = 5
, diffText = Err "Loading texture..."
, normText = Err "Loading texture..."
, isDown = False
, lastMousePos = Mouse.Position 0 0
, mouseDelta = MouseDelta 0 (pi / 2)
, windowSize = Window.Size 800 600
, withTangent = True
}
models : List String
models =
[ "meshes/elmLogo.obj"
, "meshes/suzanneNoUV.obj"
]
initCmd : Cmd Msg
initCmd =
Cmd.batch
[ loadModel True "meshes/elmLogo.obj"
, loadTexture "textures/elmLogoDiffuse.png" DiffTextureLoaded
, loadTexture "textures/elmLogoNorm.png" NormTextureLoaded
, Task.perform ResizeWindow Window.size
]
loadModel : Bool -> String -> Cmd Msg
loadModel withTangents url =
OBJ.loadObjFileWith { withTangents = withTangents } url (LoadObj url)
-- UPDATE
type Msg
= Tick Float
| LoadObj String (Result String (Dict String (Dict String Mesh)))
| Zoom Float
| MouseMove Mouse.Position
| MouseDown Mouse.Position
| MouseUp
| DiffTextureLoaded (Result String GL.Texture)
| NormTextureLoaded (Result String GL.Texture)
| ResizeWindow Window.Size
| SelectMesh String
| SetUseTangent Bool
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick dt ->
( { model | time = model.time + dt / 1000 }, Cmd.none )
Zoom dy ->
( { model | zoom = max 0.01 (model.zoom + dy / 100) }, Cmd.none )
SelectMesh url ->
( model, loadModel model.withTangent url )
SetUseTangent t ->
( { model | withTangent = t }, loadModel t model.currentModel )
LoadObj url mesh ->
( { model | mesh = mesh, currentModel = url }, Cmd.none )
DiffTextureLoaded t ->
( { model | diffText = t }, Cmd.none )
NormTextureLoaded t ->
( { model | normText = t }, Cmd.none )
MouseDown p ->
( { model | isDown = True, lastMousePos = p }, Cmd.none )
MouseUp ->
( { model | isDown = False }, Cmd.none )
MouseMove p ->
( { model | mouseDelta = getDelta p model.lastMousePos model.mouseDelta, lastMousePos = p }, Cmd.none )
ResizeWindow w ->
( { model | windowSize = w }, Cmd.none )
-- VIEW / RENDER
renderModel : Model -> GL.Texture -> GL.Texture -> Mesh -> GL.Entity
renderModel model textureDiff textureNorm mesh =
let
( camera, view, viewProjection, cameraPos ) =
getCamera model
modelM =
M4.makeTranslate (vec3 -1 0 0)
lightPos =
vec3 (0.5 * cos (2 * model.time)) (1 + 0.5 * sin (2 * model.time)) 0.5
uniforms =
{ camera = camera
, mvMat = M4.mul view modelM
, modelViewProjectionMatrix = M4.mul viewProjection modelM
, modelMatrix = modelM
, viewPosition = cameraPos
, textureDiff = textureDiff
, textureNorm = textureNorm
, lightPosition = lightPos
}
in
case mesh of
WithoutTexture { vertices, indices } ->
renderCullFace Shaders.simpleVert Shaders.simpleFrag (GL.indexedTriangles vertices indices) uniforms
WithTexture { vertices, indices } ->
renderCullFace Shaders.noNormalVert Shaders.noNormalFrag (GL.indexedTriangles vertices indices) uniforms
WithTextureAndTangent { vertices, indices } ->
renderCullFace Shaders.normalVert Shaders.normalFrag (GL.indexedTriangles vertices indices) uniforms
getCamera : Model -> ( Mat4, Mat4, Mat4, Vec3 )
getCamera { mouseDelta, zoom, windowSize } =
let
( mx, my ) =
( mouseDelta.x, mouseDelta.y )
aspect =
toFloat windowSize.width / toFloat windowSize.height
proj =
M4.makePerspective 45 aspect 0.01 10000
position =
vec3 (zoom * sin -mx * sin my) (-zoom * cos my + 1) (zoom * cos -mx * sin my)
view =
M4.makeLookAt (position) (vec3 0 1 0) (vec3 0 1 0)
in
( proj, view, M4.mul proj view, position )
view : Model -> Html.Html Msg
view model =
div []
[ selectModel model
, case ( model.mesh, model.diffText, model.normText ) of
( Ok m, Ok td, Ok tn ) ->
GL.toHtmlWith [ GL.antialias, GL.depth 1 ]
[ onZoom
, Attr.width (model.windowSize.width)
, Attr.height (model.windowSize.height)
, Attr.style [ ( "position", "absolute" ) ]
]
(Dict.values m
|> List.concatMap Dict.values
|> List.map (renderModel model td tn)
)
err ->
Html.div [] [ Html.text (toString err) ]
]
selectModel : Model -> Html Msg
selectModel model =
div [ Attr.style [ ( "position", "absolute" ), ( "z-index", "2" ), ( "backgroundColor", "white" ) ] ]
([ Html.select [ onInput SelectMesh, Attr.value model.currentModel ]
(List.map (\t -> Html.option [ Attr.value t ] [ text t ]) models)
]
++ if String.startsWith "meshes/elmLogo" model.currentModel then
[ text "\twith normal map: "
, Html.input [ Attr.type_ "checkbox", onCheck SetUseTangent, Attr.checked model.withTangent ] []
]
else
[]
)
-- SUBS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
((if model.isDown then
[ Mouse.moves MouseMove ]
else
[]
)
++ [ AnimationFrame.diffs Tick
, Mouse.downs MouseDown
, Mouse.ups (\_ -> MouseUp)
, Window.resizes ResizeWindow
]
)
-- HELPERS
onZoom : Html.Attribute Msg
onZoom =
on "wheel" (JD.map Zoom (JD.field "deltaY" JD.float))
getDelta : Mouse.Position -> Mouse.Position -> MouseDelta -> MouseDelta
getDelta curr lastP delta =
MouseDelta (toFloat (curr.x - lastP.x) / 100 + delta.x) (clamp 0.01 pi (toFloat (curr.y - lastP.y) / 100 + delta.y))
loadTexture : String -> (Result String GL.Texture -> msg) -> Cmd msg
loadTexture url msg =
WebGL.Texture.load url
|> Task.attempt
(\r ->
case r of
Ok t ->
msg (Ok t)
Err e ->
msg (Err ("Failed to load texture: " ++ toString e))
)
renderCullFace : GL.Shader a u v -> GL.Shader {} u v -> GL.Mesh a -> u -> GL.Entity
renderCullFace =
GL.entityWith [ DepthTest.default, cullFace front ]

View File

@ -0,0 +1,9 @@
# Running
Due to [a bug in elm reactor](https://github.com/elm-lang/elm-reactor/issues/217), the obj file will be served as an html page instead of `plain/text`. This screws up the parsing.
Using another file server works, e.g.
using [elm-live](https://github.com/tomekwi/elm-live) or `python -m http.server`
# Acknowledgements
Brick textures were taken from: http://www.textures.com/

View File

@ -0,0 +1,301 @@
module Shaders exposing (..)
import WebGL
{-| This shader uses Spherical Environment Mapping (SEM).
Here are some relevant links:
- [very cool demo](https://www.clicktorelease.com/code/spherical-normal-mapping/#)
- <https://www.clicktorelease.com/blog/creating-spherical-environment-mapping-shader>
- <http://www.ozone3d.net/tutorials/glsl_texturing_p04.php>
-}
reflectionVert =
[glsl|
attribute vec3 position;
attribute vec3 normal;
uniform mat4 mvMat;
uniform mat4 camera;
varying vec3 vNormal;
void main()
{
vec4 vertex4 = mvMat * vec4(position, 1.0);
vNormal = vec3(mvMat * vec4(normal, 0.0));
vec3 nm_z = normalize(vec3(vertex4));
vec3 nm_x = cross(nm_z, vec3(0.0, 1.0, 0.0));
vec3 nm_y = cross(nm_x, nm_z);
vNormal = vec3(dot(vNormal, nm_x), dot(vNormal, nm_y), dot(vNormal, nm_z));
gl_Position = camera * vertex4;
}
|]
reflectionFrag =
[glsl|
precision mediump float;
uniform sampler2D texture;
varying vec3 vNormal;
void main()
{
vec2 texCoord = vec2(0.5 * vNormal.x + 0.5, - 0.5 * vNormal.y - 0.5);
vec4 fragColor = texture2D(texture, texCoord);
fragColor.a = 1.0;
gl_FragColor = fragColor;
}
|]
{-| normal mapping according to:
<http://www.gamasutra.com/blogs/RobertBasler/20131122/205462/Three_Normal_Mapping_Techniques_Explained_For_the_Mathematically_Uninclined.php?print=1>
-}
normalVert =
[glsl|
attribute vec3 position;
attribute vec3 normal;
attribute vec2 texCoord;
attribute vec4 tangent;
varying vec2 vTexCoord;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
uniform mat4 modelViewProjectionMatrix;
uniform mat4 modelMatrix;
uniform vec3 lightPosition;
uniform vec3 viewPosition;
mat3 transpose(mat3 m) {
return mat3(m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]);
}
void main()
{
vec4 pos = vec4(position, 1.0 );
vec3 posWorld = (modelMatrix * pos).xyz;
// Tangent, Bitangent, Normal space matrix TBN
// this isn't entirely correct, it should use the normal matrix
// In this special case it works out well,
// since my model matrix does not contain any rotation or translation.
// http://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
vec3 n = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
vec3 t = normalize((modelMatrix * vec4(tangent.xyz, 0.0)).xyz);
vec3 b = normalize((modelMatrix * vec4((cross(normal, tangent.xyz) * tangent.w), 0.0)).xyz);
mat3 tbn = transpose(mat3(t, b, n));
vLightDirection = tbn*(lightPosition - posWorld);
vViewDirection = tbn*(viewPosition - posWorld);
vTexCoord = texCoord;
gl_Position = modelViewProjectionMatrix * pos;
}
|]
normalFrag =
[glsl|
precision mediump float;
uniform sampler2D textureDiff;
uniform sampler2D textureNorm;
varying vec2 vTexCoord;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
void main() {
vec3 lightDir = normalize(vLightDirection);
// Local normal, in tangent space
vec3 pixelNormal = normalize(texture2D(textureNorm, vTexCoord).rgb*2.0 - 1.0);
float lambert = max(dot(pixelNormal, lightDir), 0.0);
// diffuse + lambert
vec3 lightIntensities = vec3(1.5, 1.0, 1.0);
vec3 diffuseColor = texture2D(textureDiff, vTexCoord).rgb;
vec3 diffuse = lambert * diffuseColor * lightIntensities;
// ambient
vec3 ambient = 0.3 * diffuseColor;
// specular
float shininess = 32.0;
vec3 viewDir = normalize(vViewDirection);
vec3 reflectDir = reflect(-lightDir, pixelNormal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(pixelNormal, halfwayDir), 0.0), shininess);
vec3 specular = vec3(0.2) * spec * lightIntensities;
// attenuation
float lightAttenuation = 0.3;
float attenuation = 1.0 / (1.0 + lightAttenuation * pow(length(vLightDirection), 2.0));
vec3 final_color = ambient + (diffuse + specular) * attenuation;
gl_FragColor = vec4(final_color, 1.0);
}
|]
{-| same as the normal mapping shader, but without deforming normals.
-}
noNormalVert =
[glsl|
attribute vec3 position;
attribute vec3 normal;
attribute vec2 texCoord;
varying vec2 vTexCoord;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
varying vec3 vNormal;
uniform mat4 modelViewProjectionMatrix;
uniform mat4 modelMatrix;
uniform vec3 lightPosition;
uniform vec3 viewPosition;
void main()
{
vec4 pos = vec4(position, 1.0 );
vec3 posWorld = (modelMatrix * pos).xyz;
vLightDirection = lightPosition - posWorld;
vViewDirection = viewPosition - posWorld;
vTexCoord = texCoord;
// this is incorrect, it should use the normal matrix
vNormal = mat3(modelMatrix) * normal;
gl_Position = modelViewProjectionMatrix * pos;
}
|]
noNormalFrag =
[glsl|
precision mediump float;
uniform sampler2D textureDiff;
varying vec2 vTexCoord;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
varying vec3 vNormal;
void main()
{
vec3 lightDir = normalize(vLightDirection);
// lambert
vec3 pixelNormal = normalize(vNormal);
float lambert = max(dot(pixelNormal, lightDir), 0.0);
// diffuse + lambert
vec3 lightIntensities = vec3(1.5, 1.0, 1.0);
vec3 diffuseColor = texture2D(textureDiff, vTexCoord).rgb;
vec3 diffuse = lambert * diffuseColor * lightIntensities;
// ambient
vec3 ambient = 0.3 * diffuseColor;
// specular
float shininess = 32.0;
vec3 viewDir = normalize(vViewDirection);
vec3 reflectDir = reflect(-lightDir, pixelNormal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(pixelNormal, halfwayDir), 0.0), shininess);
vec3 specular = vec3(0.2) * spec * lightIntensities;
// attenuation
float lightAttenuation = 0.3;
float attenuation = 1.0 / (1.0 + lightAttenuation * pow(length(vLightDirection), 2.0));
vec3 final_color = ambient + (diffuse + specular) * attenuation;
gl_FragColor = vec4(final_color, 1.0);
}
|]
{-| same as above, but without any textures.
-}
simpleVert =
[glsl|
attribute vec3 position;
attribute vec3 normal;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
varying vec3 vNormal;
uniform mat4 modelViewProjectionMatrix;
uniform mat4 modelMatrix;
uniform vec3 lightPosition;
uniform vec3 viewPosition;
void main()
{
vec4 pos = vec4(position, 1.0 );
vec3 posWorld = (modelMatrix * pos).xyz;
vLightDirection = lightPosition - posWorld;
vViewDirection = viewPosition - posWorld;
// this is incorrect, it should use the normal matrix, like this:
// vNormal = mat3(normalMatrix) * normal;
// it works in this case, since the modelMatrix is the identity matrix
vNormal = normal;
gl_Position = modelViewProjectionMatrix * pos;
}
|]
simpleFrag =
[glsl|
precision mediump float;
varying vec3 vLightDirection;
varying vec3 vViewDirection;
varying vec3 vNormal;
void main()
{
vec3 lightDir = normalize(vLightDirection);
// lambert
vec3 pixelNormal = normalize(vNormal);
float lambert = max(dot(pixelNormal, lightDir), 0.0);
// diffuse + lambert
vec3 lightIntensities = vec3(1.5, 1.0, 1.0);
vec3 diffuseColor = vec3(0.3, 0.2, 0.95);
vec3 diffuse = lambert * diffuseColor * lightIntensities;
// ambient
vec3 ambient = 0.2 * diffuseColor;
// specular
float shininess = 32.0;
vec3 viewDir = normalize(vViewDirection);
vec3 reflectDir = reflect(-lightDir, pixelNormal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(pixelNormal, halfwayDir), 0.0), shininess);
vec3 specular = vec3(0.2) * spec * lightIntensities;
// attenuation
float lightAttenuation = 0.3;
float attenuation = 1.0 / (1.0 + lightAttenuation * pow(length(vLightDirection), 2.0));
vec3 final_color = ambient + (diffuse + specular) * attenuation;
gl_FragColor = vec4(final_color, 1.0);
}
|]

View File

@ -0,0 +1,148 @@
module Suzanne exposing (..)
import AnimationFrame
import Html
import Html.Attributes as Attr
import Html.Events exposing (on)
import Json.Decode as JD
import Math.Matrix4 as M4 exposing (Mat4)
import Math.Vector3 as V3 exposing (Vec3, vec3)
import Task
import WebGL as GL
import WebGL.Texture
import WebGL.Settings exposing (cullFace, front)
import WebGL.Settings.DepthTest as DepthTest
--
import OBJ
import OBJ.Types exposing (MeshWith, VertexWithTexture)
import Shaders exposing (reflectionVert, reflectionFrag)
main : Program Never Model Msg
main =
Html.program
{ init = ( initModel, initCmd )
, view = view
, subscriptions = (\model -> AnimationFrame.diffs Tick)
, update = update
}
-- MODEL
type alias Model =
{ time : Float
, mesh : Result String (MeshWith VertexWithTexture)
, zoom : Float
, reflectionTexture : Result String GL.Texture
}
initModel : Model
initModel =
{ mesh = Err "loading ...", time = 0, zoom = 10, reflectionTexture = Err "Loading texture..." }
initCmd : Cmd Msg
initCmd =
Cmd.batch
[ OBJ.loadMesh "meshes/suzanne.obj" LoadObj
, loadTexture "textures/chavant.jpg" TextureLoaded
]
-- UPDATE
type Msg
= Tick Float
| LoadObj (Result String (MeshWith VertexWithTexture))
| Zoom Float
| TextureLoaded (Result String GL.Texture)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick dt ->
( { model | time = model.time + dt / 1000 }, Cmd.none )
Zoom dy ->
( { model | zoom = model.zoom + dy / 100 }, Cmd.none )
LoadObj mesh ->
( { model | mesh = mesh }, Cmd.none )
TextureLoaded t ->
( { model | reflectionTexture = t }, Cmd.none )
-- VIEW / RENDER
renderModel : Model -> GL.Texture -> MeshWith VertexWithTexture -> GL.Entity
renderModel { zoom, time } texture { vertices, indices } =
let
( camera, view ) =
getCamera zoom time
model =
M4.makeRotate time (vec3 0 1 0)
modelView =
M4.mul view model
in
GL.entityWith [ DepthTest.default, cullFace front ]
reflectionVert
reflectionFrag
(GL.indexedTriangles vertices indices)
{ camera = camera, mvMat = modelView, texture = texture }
getCamera : Float -> Float -> ( Mat4, Mat4 )
getCamera zoom t =
( (M4.makePerspective 45 1 0.01 10000)
, (M4.makeLookAt (vec3 (zoom) (zoom / 2) (zoom)) (vec3 0 0 0) (vec3 0 1 0))
)
view : Model -> Html.Html Msg
view model =
case ( model.mesh, model.reflectionTexture ) of
( Ok m, Ok t ) ->
GL.toHtmlWith [ GL.antialias, GL.depth 1 ]
[ onZoom, Attr.width 400, Attr.height 400 ]
[ renderModel model t m ]
( a, b ) ->
Html.div [] [ Html.text (toString a ++ "\n\n\n" ++ toString b) ]
-- HELPERS
loadTexture : String -> (Result String GL.Texture -> msg) -> Cmd msg
loadTexture url msg =
WebGL.Texture.load url
|> Task.attempt
(\r ->
case r of
Ok t ->
msg (Ok t)
Err e ->
msg (Err ("Failed to load texture: " ++ toString e))
)
onZoom : Html.Attribute Msg
onZoom =
on "wheel" (JD.map Zoom (JD.field "deltaY" JD.float))

View File

@ -0,0 +1,24 @@
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/Zinggi/elm-obj-loader.git",
"license": "BSD3",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"Bogdanp/elm-combine": "3.1.1 <= v < 4.0.0",
"Skinney/elm-array-exploration": "2.0.1 <= v < 3.0.0",
"elm-community/linear-algebra": "1.0.0 <= v < 2.0.0",
"elm-community/webgl": "2.0.0 <= v < 3.0.0",
"elm-lang/animation-frame": "1.0.1 <= v < 2.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/http": "1.0.0 <= v < 2.0.0",
"elm-lang/mouse": "1.0.1 <= v < 2.0.0",
"elm-lang/window": "1.0.1 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

View File

@ -0,0 +1,169 @@
module OBJ exposing (..)
{-|
# .obj file loader
The returned models can be rendered using `indexedTriangles` from `WebGL`.
WebGL.indexedTriangles mesh.vertices mesh.indices
## From URL
All these methods take an URL as the first parameter.
### Single model
Use the methods from here if you know whats in your file
and if they only contain a single object with a single material.
These are just provided for convenience.
@docs loadMeshWithoutTexture, loadMesh, loadMeshWithTangent
### General
Use these methods if you don't know what kind of files you'll get or
if your files contain multiple groups or materials.
@docs loadObjFile, loadObjFileWith, Settings, defaultSettings
## From String
@docs parseObjStringWith
-}
import Dict exposing (Dict)
import Http
import OBJ.Assembler exposing (compile)
import OBJ.Parser exposing (parse)
import OBJ.Types exposing (Mesh, ObjFile)
import Task
--
import OBJ.Assembler exposing (compile)
import OBJ.Parser exposing (parse)
import OBJ.Types exposing (..)
{-| Load a model that doesn't have texture coordinates.
-}
loadMeshWithoutTexture : String -> (Result String (MeshWith Vertex) -> msg) -> Cmd msg
loadMeshWithoutTexture url msg =
loadObjFile url
(\res ->
case res of
Ok f ->
case (Dict.values f |> List.map Dict.values) of
[ [ WithoutTexture m ] ] ->
msg (Ok m)
_ ->
msg (Err "file loaded correctly, but there were more than one models.")
Err e ->
msg (Err e)
)
{-| Load a model with texture coordinates.
-}
loadMesh : String -> (Result String (MeshWith VertexWithTexture) -> msg) -> Cmd msg
loadMesh url msg =
loadObjFile url
(\res ->
case res of
Ok f ->
case (Dict.values f |> List.map Dict.values) of
[ [ WithTexture m ] ] ->
msg (Ok m)
_ ->
msg (Err "file loaded correctly, but there were more than one models.")
Err e ->
msg (Err e)
)
{-| Load a model with texture coordinate and calculate vertex tangents.
This is needed if you want to do tangent space normal mapping.
-}
loadMeshWithTangent : String -> (Result String (MeshWith VertexWithTextureAndTangent) -> msg) -> Cmd msg
loadMeshWithTangent url msg =
loadObjFileWith { withTangents = True }
url
(\res ->
case res of
Ok f ->
case (Dict.values f |> List.map Dict.values) of
[ [ WithTextureAndTangent m ] ] ->
msg (Ok m)
_ ->
msg (Err "file loaded correctly, but there were more than one models.")
Err e ->
msg (Err e)
)
{-| Load a .obj file from an URL
loadObjFile url ObjFileLoaded
-}
loadObjFile : String -> (Result String ObjFile -> msg) -> Cmd msg
loadObjFile =
loadObjFileWith defaultSettings
{-| withTangents : If true, vertex tangents will be calculated for meshes with texture coordinates.
This is needed if you want to do tangent space normal mapping.
-}
type alias Settings =
{ withTangents : Bool }
{-| -}
defaultSettings : Settings
defaultSettings =
{ withTangents = False }
{-| -}
loadObjFileWith : Settings -> String -> (Result String ObjFile -> msg) -> Cmd msg
loadObjFileWith settings url msg =
Http.toTask (Http.getString url)
|> Task.andThen
(\s ->
parseObjStringWith settings s |> Task.succeed
)
|> Task.onError (\e -> Task.succeed (Err ("failed to load:\n" ++ toString e)))
|> Task.attempt
(\r ->
case r of
Ok (Ok m) ->
msg (Ok m)
Ok (Err e) ->
msg (Err e)
Err e ->
msg (Err e)
)
{-| Same as `loadObjFile`, but works on a string.
-}
parseObjStringWith : Settings -> String -> Result String ObjFile
parseObjStringWith config input =
parse input
|> Result.map (compile config)

View File

@ -0,0 +1,463 @@
module OBJ.Assembler exposing (..)
import Array.Hamt as Array exposing (Array)
import Dict exposing (Dict)
import Math.Vector2 as V2 exposing (Vec2)
import Math.Vector3 as V3 exposing (Vec3, vec3)
import Math.Vector4 as V4 exposing (Vec4, vec4)
import OBJ.Types exposing (..)
import OBJ.InternalTypes exposing (..)
compile config lines =
compileHelper (emptyCompileState config) lines
|> addCurrentMesh
|> addCurrentGroup
|> .groups
emptyCompileState config =
{ -- this is a Dict (GroupName/String) Group
-- it's the final output of this algorithm
-- Group = Dict (MtlName/String) Mesh
groups = Dict.empty
, currentGroupName = "__default__"
, currentMaterialName = "__default__"
, currentMesh = Nothing
, currentGroup = Dict.empty
, vs = Array.empty
, vts = Array.empty
, vns = Array.empty
, currentIndex = 0
, knownVertexTextures = Dict.empty
, knownVertexTexturesTangents = Dict.empty
, knownVertex = Dict.empty
, config = config
}
compileHelper state lines =
case lines of
[] ->
state
l :: ls ->
compileHelper (insertLine l state) ls
{-|
this 'inserts' a line into the state.
This means it manipulates the current state to reflect state changing commands
and buils meshes on the fly.
-}
insertLine line state =
case line of
Object s ->
-- even though the specs doesn't give it any meaningful meaning,
-- I treat is exactely like a group statement.
-- This is because blender uses o instead of g per default.
addCurrentGroup state
|> (\st -> { st | currentGroupName = s })
MtlLib s ->
-- MtlLib statements are ignored,
-- as I don't plan to support loading .mtl files
state
Group s ->
addCurrentGroup state
|> (\st -> { st | currentGroupName = s })
Smooth s ->
-- smooth groups are ignored.
-- I tried to calculate these, but failed,
-- since doing them correctly is more tricky than you might think:
-- http://www.bytehazard.com/articles/vertnorm.html
-- { state | activeSmoothGroup = s }
state
UseMtl s ->
addCurrentMesh state
|> (\st -> { st | currentMaterialName = s })
V v ->
{ state | vs = Array.push v state.vs }
Vt v ->
{ state | vts = Array.push v state.vts }
Vn v ->
{ state | vns = Array.push v state.vns }
F f ->
triangulateFace f
|> List.foldr addFace state
triangulateFace f =
case f of
FVertexTextureNormal a ->
triangulate a |> List.map FTVertexTextureNormal
FVertexNormal a ->
triangulate a |> List.map FTVertexNormal
addCurrentMesh state =
-- this adds the current mesh, to the current group.
-- We also normalize all values here that need normalizing
case state.currentMesh of
Just m ->
{ state
| currentGroup = Dict.insert state.currentMaterialName (finalizeMesh m) state.currentGroup
, currentMesh = Nothing
, knownVertexTextures = Dict.empty
, knownVertexTexturesTangents = Dict.empty
, knownVertex = Dict.empty
}
_ ->
state
finalizeMesh mesh =
case mesh of
WithTextureT m ->
WithTexture m
WithoutTextureT m ->
WithoutTexture m
WithTextureAndTangentT m ->
WithTextureAndTangent
{ m
| vertices =
Array.foldr
(\({ position, texCoord, normal, sdir, tdir } as v) acc ->
let
-- handedness:
-- https://web.archive.org/web/20160409104130/http://www.terathon.com/code/tangent.html
w =
if V3.dot (V3.cross normal sdir) tdir < 0 then
-1
else
1
( x, y, z ) =
-- I have not seen this anywhere, but I added it because I sometimes got (0,0,0)
if V3.lengthSquared sdir /= 0 then
V3.toTuple <| V3.normalize (V3.sub sdir (V3.scale (V3.dot normal sdir) normal))
else
V3.toTuple <| V3.cross (V3.normalize (V3.sub tdir (V3.scale (V3.dot normal tdir) normal))) normal
in
{ position = position
, texCoord = texCoord
, normal = normal
, tangent = vec4 x y z w
}
:: acc
)
[]
m.vertices
}
addCurrentGroup state =
if Dict.isEmpty state.currentGroup then
state
else
{ state
| groups = Dict.insert state.currentGroupName state.currentGroup state.groups
, currentGroup = Dict.empty
}
addFace f state =
-- this function adds a single face to the currentMesh
-- for this it needs a dictionary containing the already known vertices,
-- indexed using the v/vn etc.
case state.currentMesh of
Nothing ->
-- we dont have a mesh yet, create one based on the type of the face
addFaceToMesh f (createMesh state.config.withTangents f) { state | currentIndex = 0 }
Just m ->
addFaceToMesh f m state
addFaceToMesh f mesh ({ vs, vts, vns, currentIndex } as state) =
-- add a face to the mesh
case ( f, mesh ) of
( FTVertexTextureNormal ( v1, v2, v3 ), WithTextureT m ) ->
let
( newState, newVs, newIs ) =
applyForFace getOrInsertVTN ( v1, v2, v3 ) state
newMesh =
WithTextureT { m | indices = newIs :: m.indices, vertices = m.vertices ++ newVs }
in
{ newState | currentMesh = Just newMesh }
( FTVertexTextureNormal ( v1, v2, v3 ), WithTextureAndTangentT m ) ->
let
tangents =
getFaceTangent ( v1, v2, v3 ) state
( newState, newVs, newIs ) =
applyForFaceA (getOrInsertVTNT tangents) ( v1, v2, v3 ) state
newMesh =
WithTextureAndTangentT { m | indices = newIs :: m.indices, vertices = Array.append m.vertices newVs }
in
{ newState | currentMesh = Just newMesh }
( FTVertexNormal ( v1, v2, v3 ), WithoutTextureT m ) ->
let
( newState, newVs, newIs ) =
applyForFace getOrInsertVN ( v1, v2, v3 ) state
newMesh =
WithoutTextureT { m | indices = newIs :: m.indices, vertices = m.vertices ++ newVs }
in
{ newState | currentMesh = Just newMesh }
_ ->
-- TODO: lift this error into a Result type
Debug.crash "mixed face types in the model!"
applyForFace f ( i1, i2, i3 ) s_0 =
let
( s_1, vs_1, i_1 ) =
f i1 s_0
( s_2, vs_2, i_2 ) =
f i2 s_1
( s_3, vs_3, i_3 ) =
f i3 s_2
in
( s_3, vs_1 ++ vs_2 ++ vs_3, ( i_3, i_2, i_1 ) )
applyForFaceA f ( i1, i2, i3 ) s_0 =
let
( s_1, vs_1, i_1 ) =
f i1 s_0
( s_2, vs_2, i_2 ) =
f i2 s_1
( s_3, vs_3, i_3 ) =
f i3 s_2
in
( s_3, Array.append (Array.append vs_1 vs_2) vs_3, ( i_3, i_2, i_1 ) )
getFaceTangent (( ( pi1, ti1, ni1 ), ( pi2, ti2, ni2 ), ( pi3, ti3, ni3 ) ) as index) { vs, vts, vns } =
-- This is from here:
-- https://web.archive.org/web/20160409104130/http://www.terathon.com/code/tangent.html
-- But since the reference doesn't mention what to do in case the denominator is 0,
-- This is probably not correct.
case ( get3 ( pi1, pi2, pi3 ) vs vs vs, get3 ( ti1, ti2, ti3 ) vts vts vts ) of
( Just ( v1, v2, v3 ), Just ( w1, w2, w3 ) ) ->
let
( ( v1x, v1y, v1z ), ( v2x, v2y, v2z ), ( v3x, v3y, v3z ) ) =
t3map V3.toTuple ( v1, v2, v3 )
( ( w1x, w1y ), ( w2x, w2y ), ( w3x, w3y ) ) =
t3map V2.toTuple ( w1, w2, w3 )
( ( x1, x2 ), ( y1, y2 ), ( z1, z2 ) ) =
( ( v2x - v1x, v3x - v1x )
, ( v2y - v1y, v3y - v1y )
, ( v2z - v1z, v3z - v1z )
)
( ( s1, s2 ), ( t1, t2 ) ) =
( ( w2x - w1x, w3x - w1x ), ( w2y - w1y, w3y - w1y ) )
denom =
s1 * t2 - s2 * t1
r =
if abs denom <= 0.000001 then
0.1
else
1 / denom
sdir =
vec3 ((t2 * x1 - t1 * x2) * r) ((t2 * y1 - t1 * y2) * r) ((t2 * z1 - t1 * z2) * r)
tdir =
vec3 ((s1 * x2 - s2 * x1) * r) ((s1 * y2 - s2 * y1) * r) ((s1 * z2 - s2 * z1) * r)
in
( sdir, tdir )
_ ->
-- TODO: lift this error into a Result type
( vec3 1 1 1, vec3 1 1 1 )
|> log ("index " ++ toString index ++ " out of bounds!\nThis should never happen with a well formed file")
getOrInsertVTN index ({ vs, vts, vns, knownVertexTextures, currentIndex } as state) =
case Dict.get index knownVertexTextures of
Just i ->
( state, [], i )
Nothing ->
case get3 index vs vts vns of
Just ( p, t, n ) ->
( { state
| knownVertexTextures = Dict.insert index currentIndex knownVertexTextures
, currentIndex = currentIndex + 1
}
, [ VertexWithTexture p t n ]
, currentIndex
)
Nothing ->
-- TODO: lift this error into a Result type
( state, [], -42 )
|> log ("index " ++ toString index ++ " out of bounds!\nThis should never happen with a well formed file")
getOrInsertVTNT ( s_dir, t_dir ) index ({ vs, vts, vns, knownVertexTexturesTangents, currentIndex } as state) =
case Dict.get index knownVertexTexturesTangents of
Just i ->
case state.currentMesh of
Just (WithTextureAndTangentT m) ->
( { state
| currentMesh =
Just
(WithTextureAndTangentT
{ m
| vertices =
updateArray i
(\({ sdir, tdir } as v) ->
{ v | sdir = V3.add sdir s_dir, tdir = V3.add tdir t_dir }
)
m.vertices
}
)
}
, Array.empty
, i
)
_ ->
-- should never happen
( state, Array.empty, i )
Nothing ->
case get3 index vs vts vns of
Just ( p, t, n ) ->
( { state
| knownVertexTexturesTangents = Dict.insert index currentIndex knownVertexTexturesTangents
, currentIndex = currentIndex + 1
}
, Array.fromList [ VertexWithTextureAndTangentT p t n s_dir t_dir ]
, currentIndex
)
Nothing ->
-- TODO: lift this error into a Result type
( state, Array.empty, -42 )
|> log ("index " ++ toString index ++ " out of bounds!\nThis should never happen with a well formed file")
getOrInsertVN index ({ vs, vns, knownVertex, currentIndex } as state) =
case Dict.get index knownVertex of
Just i ->
( state, [], i )
Nothing ->
case get2 index vs vns of
Just ( p, n ) ->
( { state
| knownVertex = Dict.insert index currentIndex knownVertex
, currentIndex = currentIndex + 1
}
, [ Vertex p n ]
, currentIndex
)
Nothing ->
-- TODO: lift this error into a Result type
( state, [], -42 )
|> log ("index " ++ toString index ++ " out of bounds!\nThis should never happen with a well formed file")
fst2 ( a, b, c ) =
( a, b )
arrayUpdate i f a =
case Array.get i a of
Just e ->
Array.set i (f e) a
_ ->
a
triangulate threeOrFour =
case threeOrFour of
Three t ->
[ t ]
Four ( a, b, c, d ) ->
[ ( a, b, c ), ( d, a, c ) ]
createMesh withTangents f =
let
emptyMesh =
{ vertices = [], indices = [] }
in
case f of
FTVertexTextureNormal _ ->
if withTangents then
WithTextureAndTangentT { emptyMesh | vertices = Array.empty }
else
WithTextureT emptyMesh
FTVertexNormal _ ->
WithoutTextureT emptyMesh
--
-- Some helpers:
--
t3map f ( a, b, c ) =
( f a, f b, f c )
updateArray i f a =
case Array.get i a of
Just v ->
Array.set i (f v) a
Nothing ->
a
get3 ( a, b, c ) a1 a2 a3 =
case ( Array.get (a - 1) a1, Array.get (b - 1) a2, Array.get (c - 1) a3 ) of
( Just a_, Just b_, Just c_ ) ->
Just ( a_, b_, c_ )
_ ->
Nothing
get2 ( a, b ) a1 a2 =
case ( Array.get (a - 1) a1, Array.get (b - 1) a2 ) of
( Just a_, Just b_ ) ->
Just ( a_, b_ )
_ ->
Nothing

View File

@ -0,0 +1,102 @@
module OBJ.InternalTypes exposing (..)
import Math.Vector3 exposing (Vec3)
import Math.Vector2 exposing (Vec2)
import Array.Hamt as Array exposing (Array)
import OBJ.Types exposing (..)
--
-- {- DEBUG -}
--
-- import Native.Time
--
--
-- debugNow : () -> Float
-- debugNow =
-- Native.Time.now
--
--
-- time : String -> (() -> a) -> a
-- time name f =
-- let
-- a =
-- debugNow ()
-- in
-- f ()
-- |> (\r -> log (name ++ ": " ++ toString (debugNow () - a) ++ " ms") r)
log s a =
let
_ =
Debug.log s ()
in
a
type MeshT
= WithoutTextureT (MeshWith Vertex)
| WithTextureT (MeshWith VertexWithTexture)
| WithTextureAndTangentT (MeshWithT VertexWithTextureAndTangentT)
type alias MeshWithT a =
{ vertices : Array a
, indices : List Int3
}
type alias VertexWithTextureAndTangentT =
{ position : Vec3, texCoord : Vec2, normal : Vec3, sdir : Vec3, tdir : Vec3 }
type
Line
-- v 1 3 4
= V Vec3
-- vt 2 4
| Vt Vec2
-- vn 3 3 1
| Vn Vec3
-- f 1 2 4
-- f 1/3 2/3 1/7
-- f 1/2/3 7/4/2 8/12/90
-- f 4//8 4//1 6//2
| F Face
-- steteful stuff
| Object String
| Group String
| Smooth String
| MtlLib String
| UseMtl String
type Face
= FVertexTextureNormal (ThreeOrFour Int3)
| FVertexNormal (ThreeOrFour Int2)
type FaceTriangle
= FTVertexTextureNormal ( Int3, Int3, Int3 )
| FTVertexNormal ( Int2, Int2, Int2 )
type ThreeOrFour a
= Three ( a, a, a )
| Four ( a, a, a, a )
type Group
= GV { faces : List Int3 }
| GVT { faces : List ( Int2, Int2, Int2 ) }
| GVTN { faces : List ( Int3, Int3, Int3 ) }
| GVN { faces : List ( Int2, Int2, Int2 ) }
type alias Int2 =
( Int, Int )
type alias Int3 =
( Int, Int, Int )

View File

@ -0,0 +1,258 @@
module OBJ.Parser exposing (..)
import Math.Vector3 as V3 exposing (Vec3, vec3)
import Math.Vector2 as V2 exposing (Vec2, vec2)
import Combine exposing (..)
import Json.Decode as JD
import Combine.Num exposing (..)
import Combine.Char exposing (..)
import OBJ.InternalTypes exposing (..)
import Regex exposing (find, HowMany(..))
-- TODO: figure out how nice error messages work
--
-- The obj specs:
-- http://www.martinreddy.net/gfx/3d/OBJ.spec
parse : String -> Result String (List Line)
parse input =
String.split "\n" input
|> List.foldr parseLineAcc (Ok [])
parseLineAcc : String -> Result String (List Line) -> Result String (List Line)
parseLineAcc line acc =
case acc of
Ok lines ->
if canSkip line then
Ok lines
else
parseLine line
|> Result.andThen
(\l ->
Ok (l :: lines)
)
Err e ->
Err e
canSkip line =
Regex.contains (Regex.regex "^((\\s*)|(\\s*#.*\\r?))$") line
parseLine l =
case Combine.parse line l of
Ok ( _, stream, result ) ->
Ok result
Err ( _, stream, errors ) ->
Err (formatError errors stream)
file : Parser s (List Line)
file =
(many ignoredLines)
*> sepBy (many1 ignoredLines)
line
<* (many ignoredLines)
<* end
ignoredLines : Parser s ()
ignoredLines =
(skip eol) <|> (skip comment)
objectName : Parser s String
objectName =
regex "o[ \t]+" *> regex ".+"
mtllib : Parser s String
mtllib =
regex "mtllib[ \t]+" *> regex ".+"
group : Parser s String
group =
(regex "g[ \t]+" *> regex ".+")
<|> (char 'g' *> succeed "")
smooth : Parser s String
smooth =
regex "s[ \t]+" *> regex ".+"
usemtl : Parser s String
usemtl =
regex "usemtl[ \t]+" *> regex ".+"
line : Parser s Line
line =
choice
[ V <$> vertex
, Vt <$> vertexTexture
, Vn <$> vertexNormal
, F <$> face
, Object <$> objectName
, Group <$> group
, Smooth <$> smooth
, UseMtl <$> usemtl
, MtlLib <$> mtllib
]
<* regex "[ \t]*"
face : Parser s Face
face =
regex "f[ \t]+"
*> choice
[ fVertexTextureNormal
, fVertexNormal
, fVertex
, fVertexTexture
]
fVertex : Parser s a
fVertex =
threeOrFourValues int
*> fail "Models with no precalculated vertex normals are not supported!"
fVertexTexture : Parser s a
fVertexTexture =
threeOrFourValues int_int
*> fail "Models with no precalculated vertex normals are not supported!"
fVertexTextureNormal : Parser s Face
fVertexTextureNormal =
FVertexTextureNormal <$> threeOrFourValues int_int_int
fVertexNormal : Parser s Face
fVertexNormal =
FVertexNormal <$> threeOrFourValues int__int
threeValues : (a -> a -> a -> b) -> Parser s a -> Parser s b
threeValues tagger vtype =
tagger <$> (vtype) <*> (spaces *> vtype) <*> (spaces *> vtype)
fourValues : (a -> a -> a -> a -> b) -> Parser s a -> Parser s b
fourValues tagger vtype =
tagger <$> (vtype) <*> (spaces *> vtype) <*> (spaces *> vtype) <*> (spaces *> vtype)
threeOrFourValues : Parser s a -> Parser s (ThreeOrFour a)
threeOrFourValues elements =
(Four <$> (fourValues (,,,) elements))
<|> (Three <$> (threeValues (,,) elements))
int_int : Parser s ( Int, Int )
int_int =
(,) <$> int <*> (string "/" *> int)
int_int_int : Parser s ( Int, Int, Int )
int_int_int =
(,,) <$> int <*> (string "/" *> int) <*> (string "/" *> int)
int__int : Parser s ( Int, Int )
int__int =
(,) <$> int <*> (string "//" *> int)
vertexNormal : Parser s Vec3
vertexNormal =
regex "vn[ \t]+" *> (V3.normalize <$> vector3)
vertexTexture : Parser s Vec2
vertexTexture =
regex "vt[ \t]+" *> ((ignoreZ <$> vector3) <|> vector2)
vertex : Parser s Vec3
vertex =
regex "v[ \t]+" *> vector3
comment : Parser s String
comment =
regex "#" *> regex ".*"
vector3 : Parser s Vec3
vector3 =
threeValues vec3 betterFloat
spaces : Parser s String
spaces =
regex "[ \t]+"
vector2 : Parser s Vec2
vector2 =
vec2 <$> betterFloat <*> (spaces *> betterFloat)
betterFloat : Parser s Float
betterFloat =
(\s -> Result.withDefault 0 (JD.decodeString JD.float s)) <$> regex "[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?"
formatError : List String -> InputStream -> String
formatError ms stream =
let
location =
currentLocation stream
separator =
"| "
expectationSeparator =
"\n * "
lineNumberOffset =
floor (logBase 10 (toFloat location.line)) + 1
separatorOffset =
String.length separator
padding =
location.column + separatorOffset + 2
in
"Parse error around line:\n\n"
++ toString location.line
++ separator
++ location.source
++ "\n"
++ String.padLeft padding ' ' "^"
++ "\nI expected one of the following:\n"
++ expectationSeparator
++ String.join expectationSeparator ms
toInt : String -> Int
toInt s =
String.toInt s |> Result.withDefault 0
ignoreZ : Vec3 -> Vec2
ignoreZ v =
let
( x, y, _ ) =
V3.toTuple v
in
vec2 x y

View File

@ -0,0 +1,74 @@
module OBJ.Types exposing (..)
{-|
These are the types used by the obj loader.
@docs ObjFile, Mesh, MeshWith
--
@docs Vertex, VertexWithTexture, VertexWithTextureAndTangent
-}
import Dict exposing (Dict)
import Math.Vector2 exposing (Vec2)
import Math.Vector3 exposing (Vec3)
import Math.Vector4 exposing (Vec4)
{-| A .obj file is optionally divided into different groups/objects.
Each group/object is optionally made up of different meshes, each with it's own material.
So the keys of this dictionary are:
Dict GroupNameOrObjectName (Dict MaterialName Mesh)
If no name is specified in the input file, "__default__" will be used instead.
-}
type alias ObjFile =
Dict String (Dict String Mesh)
{-|
A `Mesh` loaded by the obj loader is a record with a list of vertices and a list of indices.
Depending on the mesh type and the loading options you get a different kind of mesh.
They differ on what information a vertex contains.
These meshes are meant to be used with `WebGL.indexedTriangles mesh.vertices mesh.indices`.
-}
type Mesh
= WithoutTexture (MeshWith Vertex)
| WithTexture (MeshWith VertexWithTexture)
| WithTextureAndTangent (MeshWith VertexWithTextureAndTangent)
{-|
-}
type alias MeshWith a =
{ vertices : List a
, indices : List ( Int, Int, Int )
}
{-| -}
type alias Vertex =
{ position : Vec3, normal : Vec3 }
{-| -}
type alias VertexWithTexture =
{ position : Vec3, texCoord : Vec2, normal : Vec3 }
{-|
The `tangent` is a vector pointing tangential to the object surface, in the direction of the `u` texture coordinate.
This is needed for doing tangent space normal mapping.
The 4th component is either 1 or -1 and has to be used to get the bitangent in the glsl shader,
e.g: `vec3 bitangent = cross(normal, tangent.xyz) * tangent.w`
more info here:
https://web.archive.org/web/20160409104130/http://www.terathon.com/code/tangent.html
-}
type alias VertexWithTextureAndTangent =
{ position : Vec3, texCoord : Vec2, normal : Vec3, tangent : Vec4 }