module obj /********************************************************************** * * .obj loader * * Copyright (c) 2021 Dario Deledda. All rights reserved. * Use of this source code is governed by an MIT license * that can be found in the LICENSE file. * * TODO: **********************************************************************/ import gg.m4 import strconv enum F_state { start first ints decimals exp_start exp_sign exp_int } // read a int from a string fn get_int(s string, start_index int) (int, int) { mut i := start_index mut res := 0 mut sgn := 1 mut state := F_state.start for true { if i >= s.len { break } c := s[i] if state == .start { match c { `+` { i++ state = .ints continue } `-` { sgn = -1 i++ state = .ints continue } `0`...`9` { state = .ints } ` `, `\t` { i++ continue } else { // no number found break } } } if state == .ints { match c { `0`...`9` { // println("$res => ${(int(c) - 48)}") res = res * 10 + (int(c) - 48) i++ continue } else { break } } } } // println("---") return res * sgn, i } // reas a float number from a string fn get_float(s string, start_index int) (f64, int) { mut i1 := start_index //+ 1 for i1 < s.len && s[i1] in [` `, `\t`] { i1++ } mut i := i1 for i < s.len { if s[i] in [` `, `\t`] { break } i++ } // println(" get_float: ($start_index,$i) [${s[start_index..i]}]") // f_res := strconv.atof_quick(s[start_index..i]) f_res := strconv.atof_quick(s[i1..i]) return f_res, i } // read 3 f32 in sequence from a string fn parse_3f(row string, start_index int) m4.Vec4 { // println(row) mut i := start_index //+ 1 mut f1 := f64(0) mut f2 := f64(0) f0, mut p := get_float(row, i) // print("Here f0: $f0 $p ") f1, p = get_float(row, p + 1) // print("Here f1: $f1 $p ") f2, p = get_float(row, p + 1) // print("Here f2: $f2 $p ") return m4.Vec4{ e: [f32(f0), f32(f1), f32(f2), 1]! } } // reas a sequence of f32 from a string fn (mut m ObjPart) parse_floats(row string, start_index int) m4.Vec4 { mut i := start_index //+ 1 mut res_f := f64(0) mut res := m4.Vec4{ e: [f32(0), 0, 0, 1]! } mut c := 0 for true { res_f, i = get_float(row, i) unsafe { res.e[c] = f32(res_f) } c++ i++ if i >= row.len { break } } return res } // read and manage all the faes from an .obj file data fn (mut p Part) parse_faces(row string, start_index int, obj ObjPart) { mut i := start_index + 1 mut res := [][3]int{} mut v := 0 mut t := 0 mut n := 0 // println("row: ${row[i..]}") for true { t = 0 n = 0 if i >= row.len { break } mut c := row[i] if (c > `9` || c < `0`) && c != `-` { i++ continue } v, i = get_int(row, i) if i < row.len && row[i] == `/` { if row[i + 1] != `/` { t, i = get_int(row, i + 1) if i < row.len && row[i] == `/` { n, i = get_int(row, i + 1) } } else { i++ n, i = get_int(row, i + 1) } } // manage negative indexes // NOTE: not well suporeted now if v < 0 { // println("${obj.v.len} ${obj.v.len-c}") v = obj.v.len - v + 1 // exit(0) } if n < 0 { n = obj.vn.len - n + 1 } if t < 0 { t = obj.vt.len - t + 1 } res << [v - 1, n - 1, t - 1]! } // println("ok res: ${res}") // println(p.faces.len) p.faces << res } // parse the obj file, if single_material is true it use only one default material pub fn (mut obj_part ObjPart) parse_obj_buffer(rows []string, single_material bool) { mut mat_count := 0 mut row_count := 0 default_part := Part{ name: 'default part' } obj_part.part << default_part // println("OBJ file has ${rows.len} rows") for c, row in rows { // println("$c $row") mut i := 0 row_count++ for true { if i >= row.len { break } match row[i] { `s` { break } `m` { if row[i..i + 6] == 'mtllib' { obj_part.material_file = row[i + 7..].trim_space() obj_part.load_materials() } break } `o`, `g` { mut part := Part{} part.name = row[i + 1..].trim_space() obj_part.part << part mat_count = 0 break } `u` { if single_material == false && row[i..i + 6] == 'usemtl' { material := row[i + 7..].trim_space() // println("material: $material") // manage multiple materials in an part if obj_part.part[obj_part.part.len - 1].material.len > 0 { mat_count++ mut part := Part{} if mat_count > 1 { li := obj_part.part[obj_part.part.len - 1].name.last_index('_m') or { obj_part.part[obj_part.part.len - 1].name.len - 1 } part.name = obj_part.part[obj_part.part.len - 1].name[..li] + '_m${mat_count:02}' } else { part.name = obj_part.part[obj_part.part.len - 1].name + '_m01' } obj_part.part << part } obj_part.part[obj_part.part.len - 1].material = material } break } `v` { i++ match row[i] { // normals `n` { obj_part.vn << parse_3f(row, i + 2) // println("Vertex line: $c") break } // parameters uvw `p` { obj_part.vp << parse_3f(row, i + 2) // println("Vertex line: ${obj_part.vp.len}") break } // texture uvw `t` { obj_part.vt << obj_part.parse_floats(row, i + 2) // println("Vertex line: $c") break } else { obj_part.v << parse_3f(row, i + 1) // println("$row => ${obj_part.v[obj_part.v.len-1]}") break } } } `f` { // println("$c $row") obj_part.part[obj_part.part.len - 1].parse_faces(row, i, obj_part) // println(obj_part.part[obj_part.part.len - 1].faces.len) // println("Faces line: $c") break } // end of the line, comments `\n`, `#` { break } else {} } i++ } // if c == 2 { break } if c % 100000 == 0 && c > 0 { println('${c} rows parsed') } } println('${row_count} .obj Rows parsed') // remove default part if empty if obj_part.part.len > 1 && obj_part.part[0].faces.len == 0 { obj_part.part = obj_part.part[1..] } } // load the materials if found the .mtl file fn (mut obj_part ObjPart) load_materials() { rows := read_lines_from_file(obj_part.material_file) println('Material file [${obj_part.material_file}] ${rows.len} Rows.') for row in rows { // println("$row") mut i := 0 for true { if i >= row.len { break } match row[i] { `n` { if row[i..i + 6] == 'newmtl' { name := row[i + 6..].trim_space() mut mat := Material{ name: name } obj_part.mat << mat break } } `K` { if row[i + 1] !in [`a`, `d`, `e`, `s`] { break } k_name := row[i..i + 2] i += 3 value := parse_3f(row, i) obj_part.mat[obj_part.mat.len - 1].ks[k_name] = value break } `N` { n_name := row[i..i + 2] i += 3 value, _ := get_float(row, i) obj_part.mat[obj_part.mat.len - 1].ns[n_name] = f32(value) break } `m` { if row[i..i + 4] == 'map_' { name := row[i..i + 6] if (i + 7) < row.len { file_name := row[i + 7..].trim_space() obj_part.mat[obj_part.mat.len - 1].maps[name] = file_name } break } } // transparency `d` { if row[i + 1] == ` ` { value, _ := get_float(row, i + 2) obj_part.mat[obj_part.mat.len - 1].ns['Tr'] = f32(value) } } `T` { if row[i + 1] == `r` { value, _ := get_float(row, i + 3) obj_part.mat[obj_part.mat.len - 1].ns['Tr'] = f32(1.0 - value) } } // end of the line, comments `\n`, `#` { break } ` `, `\t` { i++ continue } else { break } } i++ } } // create map material name => material index for i, m in obj_part.mat { if m.name !in obj_part.mat_map { obj_part.mat_map[m.name] = i } } println('Material Loading Done!') } //============================================================================== // Sokol data //============================================================================== // vertex data struct pub struct Vertex_pnct { pub mut: x f32 // position y f32 z f32 nx f32 // normal ny f32 nz f32 // color u32 = 0xFFFFFFFF // color u f32 // uv v f32 // u u16 // for compatibility with D3D11 // v u16 // for compatibility with D3D11 } // struct used to pass the data to the sokol calls pub struct Skl_buffer { pub mut: vbuf []Vertex_pnct ibuf []u32 n_vertex u32 } // transforms data from .obj format to buffer ready to be used in the render pub fn (mut obj_part ObjPart) get_buffer(in_part_list []int) Skl_buffer { // in_part := 0 mut v_count_index := 0 mut out_buf := Skl_buffer{} mut cache := map[string]int{} mut cache_hit := 0 // has_normals := obj_part.vn.len > 0 // has_uvs := obj_part.vt.len > 0 for in_part in in_part_list { part := obj_part.part[in_part] for fc, face in part.faces { // println("$fc $face") // default 3 faces mut v_seq := [0, 1, 2] if face.len == 4 { v_seq = [0, 1, 2, 0, 2, 3] } // if big faces => use the fan of triangles as solution // Note: this trick doesn't work with concave faces if face.len > 4 { v_seq = [] mut i := 1 for i < (face.len - 1) { v_seq << 0 v_seq << i v_seq << (i + 1) i++ } // println("BIG FACES! ${fc} ${face.len} v_seq:${v_seq.len}") } // no vertex index, generate normals if face[0][1] == -1 && face.len >= 3 { mut v_count := 0 v0 := face[v_count + 0][0] v1 := face[v_count + 1][0] v2 := face[v_count + 2][0] vec0 := obj_part.v[v2] - obj_part.v[v1] vec1 := obj_part.v[v0] - obj_part.v[v1] tmp_normal := vec0 % vec1 for v_count < face.len { obj_part.vn << tmp_normal obj_part.part[in_part].faces[fc][v_count][1] = obj_part.vn.len - 1 v_count++ } } for vertex_index in v_seq { // position if vertex_index >= face.len { continue } v_index := face[vertex_index][0] // vertex index n_index := face[vertex_index][1] // normal index t_index := face[vertex_index][2] // uv texture index key := '${v_index}_${n_index}_${t_index}' if key !in cache { cache[key] = v_count_index mut pnct := Vertex_pnct{ x: obj_part.v[v_index].e[0] y: obj_part.v[v_index].e[1] z: obj_part.v[v_index].e[2] } // normal if n_index >= 0 { pnct.nx = obj_part.vn[n_index].e[0] pnct.ny = obj_part.vn[n_index].e[1] pnct.nz = obj_part.vn[n_index].e[2] } // texture uv if t_index >= 0 { pnct.u = obj_part.vt[t_index].e[0] pnct.v = obj_part.vt[t_index].e[1] } out_buf.vbuf << pnct out_buf.ibuf << u32(v_count_index) v_count_index++ } else { // println("Cache used! $key") out_buf.ibuf << u32(cache[key]) cache_hit++ } } } } /* println("------------") for c1, x1 in out_buf.vbuf[..10] { println("$c1 $x1") } println(out_buf.ibuf[..10]) */ // println("vbuf size: ${out_buf.vbuf.len} ibuf size: ${out_buf.ibuf.len} Cache hit: $cache_hit") out_buf.n_vertex = u32(out_buf.ibuf.len) return out_buf } //============================================================================== // Utility //============================================================================== // print on the console the summary of the .obj model loaded pub fn (obj_part ObjPart) summary() { println('---- Stats ----') println('vertices: ${obj_part.v.len}') println('normals : ${obj_part.vn.len}') println('uv : ${obj_part.vt.len}') println('parts : ${obj_part.part.len}') // Parts println('---- Parts ----') for c, x in obj_part.part { println('${c:3} [${x.name:-16}] mat:[${x.material:-10}] ${x.faces.len:7} faces') } // Materials println('---- Materials ----') println('Material dict: ${obj_part.mat_map.keys()}') for c, mat in obj_part.mat { println('${c:3} [${mat.name:-16}]') for k, v in mat.ks { print('${k} = ${v}') } for k, v in mat.ns { println('${k} = ${v}') } for k, v in mat.maps { println('${k} = ${v}') } } } // debug test function, do not remove. pub fn tst() { /* //fname := "capsule.obj" //fname := "Forklift.obj" fname := "cube.obj" //fname := "Orange Robot 3D ObjPart.obj" mut obj := ObjPart{} buf := os.read_lines(fname) or { panic(err.msg) } obj.parse_obj_buffer(buf) obj.summary() */ /* a :="f 7048 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7080 7003" mut f1 := 0 mut f2 := 0 f0,mut p := get_int(a,1) f1, p = get_int(a,p) f2, p = get_int(a,p) println("res: ${f0} ${f1} ${f2}") */ /* a :="v -0 0.107769 -0.755914" println("${parse_3f(a,1)}") */ /* ort := m4.ortho(0,300,0,200,0,0) println(ort) a := m4.vec3(0,0,0) println("a: $a") res := m4.mul_vec(ort, a) println("res:\n${res}") */ s := 'K 1 1 1' r := strconv.atof_quick(s[1..s.len - 1]) println(r) }