From 3b274110c8b696de7b65f76faf550af2d0b76dac Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 14 Jul 2024 13:37:00 +0200 Subject: [PATCH 1/7] track materials --- src/io/obj.jl | 94 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index 5f48873..4edc4e0 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -7,10 +7,25 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, pointtype=Point3f, normaltype=Vec3f, uvtype=Any) + # function parse_bool(x, default) + # if lowercase(x) == "off" || x == "0" + # return false + # elseif lowercase(x) == "on" || x == "1" + # return true + # else + # error("Failed to parse $x as Bool.") + # end + # end + points, v_normals, uv, faces = pointtype[], normaltype[], uvtype[], facetype[] - f_uv_n_faces = (faces, facetype[], facetype[]) - last_command = "" - attrib_type = nothing + f_uv_n_faces = (faces, facetype[], facetype[], facetype[]) + + # TODO: Allow GeometryBasics to keep track of this in Mesh? + material_ids = Int[] + materials = Dict{String, Int}() + current_material = 0 + material_counter = 0 + for full_line in eachline(stream(io)) # read a line, remove newline and leading/trailing whitespaces line = strip(chomp(full_line)) @@ -22,8 +37,10 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, if "v" == command # mesh always has vertices push!(points, pointtype(parse.(eltype(pointtype), lines))) + elseif "vn" == command push!(v_normals, normaltype(parse.(eltype(normaltype), lines))) + elseif "vt" == command if length(lines) == 2 if uvtype == Any @@ -40,7 +57,10 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, else error("Unknown UVW coordinate: $lines") end + elseif "f" == command # mesh always has faces + # add material + if any(x-> occursin("//", x), lines) fs = process_face_normal(lines) elseif any(x-> occursin("/", x), lines) @@ -52,11 +72,70 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, for i = 1:length(first(fs)) append!(f_uv_n_faces[i], triangulated_faces(facetype, getindex.(fs, i))) end + + # elseif "s" == command # Blender sets this just before faces + # shading = parse_bool(lines[1]) + + # elseif "o" == command # Blender sets this before vertices + # object_name = join(lines, ' ') + + # elseif "g" == command + # group_name = join(lines, ' ') + + # elseif "mtllib" == command + # filename = join(lines, ' ') + + elseif "usemtl" == command # Blender sets this just before faces + name = join(lines, ' ') + last_material = current_material + current_material = get!(materials, name) do + material_counter += 1 + end + if current_material != last_material + push!(material_ids, current_material) + last_material == 0 && continue # first material + + # find material face buffer and push all the material faces + target_N = length(faces) + face = facetype(last_material) + for i in 2:4 + if length(f_uv_n_faces[i]) < target_N + sizehint!(f_uv_n_faces[i], target_N) + while length(f_uv_n_faces[i]) < target_N + push!(f_uv_n_faces[i], face) + end + break + end + end + end else #TODO end end end + + # drop material ids if no materials were specified + if material_counter == 1 + for i in 4:-1:1 + if !isempty(f_uv_n_faces[i]) + empty!(f_uv_n_faces[i]) + break + end + end + empty!(material_ids) + else + face = facetype(current_material) + target_N = length(faces) + for i in 2:4 + if length(f_uv_n_faces[i]) < target_N + sizehint!(f_uv_n_faces[i], target_N) + while length(f_uv_n_faces[i]) < target_N + push!(f_uv_n_faces[i], face) + end + break + end + end + end point_attributes = Dict{Symbol, Any}() non_empty_faces = filtertuple(!isempty, f_uv_n_faces) @@ -78,6 +157,10 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, end if !isempty(v_normals) point_attributes[:normals] = v_normals[attrib_maps[counter]] + counter += 1 + end + if !isempty(material_ids) + point_attributes[:material] = material_ids[attrib_maps[counter]] end else # we have vertex indexing - no need to remap @@ -88,7 +171,10 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, if !isempty(uv) point_attributes[:uv] = uv end - + if !isempty(material_ids) + point_attributes[:material] = material_ids + end + end return Mesh(meta(points; point_attributes...), faces) From 5cbd96559df3953d4bd32359a20e0ae66698aac0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 14 Jul 2024 13:37:28 +0200 Subject: [PATCH 2/7] add experimental mtl loading + mesh splitting util --- src/io/obj.jl | 284 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 1 deletion(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index 4edc4e0..a1bf1ab 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -72,7 +72,7 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, for i = 1:length(first(fs)) append!(f_uv_n_faces[i], triangulated_faces(facetype, getindex.(fs, i))) end - + # elseif "s" == command # Blender sets this just before faces # shading = parse_bool(lines[1]) @@ -219,3 +219,285 @@ function save(f::Stream{format"OBJ"}, mesh::AbstractMesh) println(io, "f ", join(convert.(Int, f), " ")) end end + + +# Experimental stuff for loading .mtl files and working with multiple materials + +""" + MehsIO.split_mesh(mesh) + +Experimental function for splitting a mesh based material indices. +Also remaps vertices to avoid passing all vertices with a submesh. +""" +function split_mesh(mesh) + ps = coordinates(mesh) + ns = normals(mesh) + uvs = texturecoordinates(mesh) + ids = mesh.material + fs = faces(mesh) + + meshes = Dict{Int, Any}() + target_ids = unique(ids) + IndexType = eltype(eltype(fs)) + + for target_id in target_ids + _fs = eltype(fs)[] + indexmap = Dict{UInt32, UInt32}() + counter = MeshIO._typemin(IndexType) + + for f in fs + if any(ids[f] .== target_id) + f = map(f) do _i + i = GeometryBasics.value(_i) + if haskey(indexmap, i) + return indexmap[i] + else + indexmap[i] = counter + counter += 1 + return counter-1 + end + end + push!(_fs, f) + end + end + + indices = Vector{UInt32}(undef, counter-1) + for (old, new) in indexmap + indices[new] = old + end + + meshes[target_id] = GeometryBasics.Mesh( + meta(ps[indices], normals = ns[indices], uv = uvs[indices]), _fs + ) + end + + return meshes +end + +""" + load_materials(obj_filename) + +Experimental functionality for loading am mtl file attached to an obj file. Also +recovers loads the object_group_id -> (object, group) name mapping from the obj +file. +""" +function load_materials(filename::String) + endswith(filename, ".obj") || error("File should be a .obj file!") + + data = Dict{String, Any}() + mat2id = Dict{String, Int}() + current_material = 0 + material_counter = 0 + + path = joinpath(splitpath(filename)[1:end-1]) + file = open(filename, "r") + + for full_line in eachline(file) + # read a line, remove newline and leading/trailing whitespaces + line = strip(chomp(full_line)) + !isascii(line) && error("non valid ascii in obj") + + if !startswith(line, "#") && !isempty(line) && !all(iscntrl, line) #ignore comments + lines = split(line) + command = popfirst!(lines) #first is the command, rest the data + + if "usemtl" == command + name = join(lines, ' ') + current_material = get!(mat2id, name) do + material_counter += 1 + end + + elseif "mtllib" == command + filename = join(lines, ' ') + materials = _load_mtl(joinpath(path, filename)) + for (k, v) in materials + data[k] = v + end + else + # Skipped + end + end + end + + close(file) + + data["id to material"] = Dict([v => k for (k, v) in mat2id]) + + return data +end + +function _load_mtl(filename::String) + endswith(filename, ".mtl") || error("File should be a .mtl file!") + materials = Dict{String, Dict{String, Any}}() + material = Dict{String, Any}() + + name_lookup = Dict( + "Ka" => "ambient", "Kd" => "diffuse", "Ks" => "specular", + "Ns" => "shininess", "d" => "alpha", "Tr" => "transmission", # 1 - alpha + "Ni" => "refractive index", "illum" => "illumination model", + # PBR + "Pr" => "roughness", "Pm" => "metallic", "Ps" => "sheen", + "Pc" => "clearcoat thickness", "Pcr" => "clearcoat roughness", + "Ke" => "emissive", "aniso" => "anisotropy", + "anisor" => "anisotropy rotation", + # texture maps + "map_Ka" => "ambient map", "map_Kd" => "diffuse map", + "map_Ks" => "specular map", "map_Ns" => "shininess map", + "map_d" => "alpha map", "map_Tr" => "transmission map", + "map_bump" => "bump map", "bump" => "bump map", + "disp" => "displacement map", "decal" => "decal map", + "refl" => "reflection map", "norm" => "normal map", + "map_Pr" => "roughness map", "map_Pm" => "metallic map", + "map_Ps" => "sheen map", "map_Ke" => "emissive map", + "map_RMA" => "roughness metalness occlusion map", + "map_ORM" => "occlusion roughness metalness map" + ) + + path = joinpath(splitpath(filename)[1:end-1]) + file = open(filename, "r") + + try + for full_line in eachline(file) + # read a line, remove newline and leading/trailing whitespaces + line = strip(chomp(full_line)) + !isascii(line) && error("non valid ascii in obj") + + if !startswith(line, "#") && !isempty(line) && !all(iscntrl, line) #ignore comments + lines = split(line) + command = popfirst!(lines) #first is the command, rest the data + + if command == "newmtl" + name = join(lines, ' ') + materials[name] = material = Dict{String, Any}() + + elseif command == "Ka" || command == "Kd" || command == "Ks" + material[name_lookup[command]] = Vec3f(parse.(Float32, lines)...) + + elseif command == "Ns" || command == "Ni" || command == "Pr" || + command == "Pm" || command == "Ps" || command == "Pc" || + command == "Pcr" || command == "Ke" || command == "aniso" || + command == "anisor" + + material[name_lookup[command]] = parse.(Float32, lines[1]) + + elseif command == "d" + haskey(material, "alpha") && error("Material alpha doubly defined.") + material[name_lookup[command]] = parse.(Float32, lines[1]) + + elseif command == "Tr" + haskey(material, "alpha") && error("Material alpha doubly defined.") + material[name_lookup["d"]] = 1f0 - parse.(Float32, lines[1]) + + # elseif Tf # transmission filter + + elseif command == "illum" + # See https://en.wikipedia.org/wiki/Wavefront_.obj_file#Basic_materials + material[name_lookup[command]] = parse.(Int, lines[1]) + + elseif startswith(command, "map") || command == "bump" || command == "norm" || + command == "refl" || command == "disp" || command == "decal" + + # TODO: treat all the texture options + material[get(name_lookup, command, command)] = parse_texture_info(path, lines) + + else + material[command] = lines + end + end + end + + finally + close(file) + end + + return materials +end + +function parse_texture_info(parent_path::String, lines::Vector{SubString{String}}) + idx = 1 + output = Dict{String, Any}() + name_lookup = Dict( + "o" => "origin offset", "s" => "scale", "t" => "turbulence", + "blendu" => "blend horizontal", "blendv" => "blend vertical", + "boost" => "mipmap sharpness", "bm" => "bump multiplier" + ) + + function parse_bool(x, default) + if lowercase(x) == "off" || x == "0" + return false + elseif lowercase(x) == "on" || x == "1" + return true + else + error("Failed to parse $x as Bool.") + end + end + + while idx < length(lines) + 1 + if startswith(lines[idx], '-') + command = lines[idx][2:end] + + if command == "blendu" || command == "blendv" + name = name_lookup[command] + output[name] = parse_bool(lines[idx+1], true) + idx += 2 + + elseif command == "boost" || command == "bm" + output[name_lookup[command]] = parse(Float32, lines[idx+1]) + idx += 2 + + elseif command == "mm" + output["brightness"] = parse(Float32, lines[idx+1]) + output["contrast"] = parse(Float32, lines[idx+2]) + idx += 3 + + elseif command == "o" || command == "s" || command == "t" + default = command == "s" ? 1f0 : 0f0 + x = parse(Float32, lines[idx+1]) + y = length(lines) >= idx+2 ? tryparse(Float32, lines[idx+2]) : nothing + z = length(lines) >= idx+3 ? tryparse(Float32, lines[idx+3]) : nothing + output[name_lookup[command]] = Vec3f( + x, something(y, default), something(z, default) + ) + idx += 2 + (y !== nothing) + (z !== nothing) + + elseif command == "texres" # is this only one value? + output["resolution"] = parse(Float32, lines[idx+1]) + idx += 2 + + elseif command == "clamp" + output["clamp"] = parse_bool(lines[idx+1]) + idx += 2 + + elseif command == "imfchan" + output["channel"] = lines[idx+1] + idx += 2 + + elseif command == "type" + output[command] = lines[idx+1] + idx += 2 + + # TODO: PBR tags + + else + @warn "Failed to parse -$command" + idx += 1 + end + else + filepath = joinpath(parent_path, lines[idx]) + i = idx+1 + while i <= length(lines) && !startswith(lines[i], '-') + filepath = filepath * ' ' * lines[i] + i += 1 + end + @info filepath + if isfile(filepath) + output["filename"] = filepath + idx = i + else + idx += 1 + end + end + end + + return output +end \ No newline at end of file From 2df6da85766298f12e3ee97866f39691b4a97288 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 14 Jul 2024 14:12:16 +0200 Subject: [PATCH 3/7] fix test failures --- src/io/obj.jl | 53 ++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index a1bf1ab..cb5b8c7 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -18,7 +18,9 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, # end points, v_normals, uv, faces = pointtype[], normaltype[], uvtype[], facetype[] - f_uv_n_faces = (faces, facetype[], facetype[], facetype[]) + material_faces = facetype[] + f_uv_n_faces = (faces, facetype[], facetype[], material_faces) + # TODO: Allow GeometryBasics to keep track of this in Mesh? material_ids = Int[] @@ -98,14 +100,9 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, # find material face buffer and push all the material faces target_N = length(faces) face = facetype(last_material) - for i in 2:4 - if length(f_uv_n_faces[i]) < target_N - sizehint!(f_uv_n_faces[i], target_N) - while length(f_uv_n_faces[i]) < target_N - push!(f_uv_n_faces[i], face) - end - break - end + sizehint!(material_faces, target_N) + while length(material_faces) < target_N + push!(material_faces, face) end end else @@ -115,25 +112,14 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, end # drop material ids if no materials were specified - if material_counter == 1 - for i in 4:-1:1 - if !isempty(f_uv_n_faces[i]) - empty!(f_uv_n_faces[i]) - break - end - end + if material_counter == 0 empty!(material_ids) else face = facetype(current_material) target_N = length(faces) - for i in 2:4 - if length(f_uv_n_faces[i]) < target_N - sizehint!(f_uv_n_faces[i], target_N) - while length(f_uv_n_faces[i]) < target_N - push!(f_uv_n_faces[i], face) - end - break - end + sizehint!(material_faces, target_N) + while length(material_faces) < target_N + push!(material_faces, face) end end @@ -151,13 +137,24 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, # Update order of vertex attributes points = points[attrib_maps[1]] counter = 2 + # With materials we can have merged position-uv-normal faces + # but still end up in this branch because of the material index, so + # we need to check if the uv/normals faces are set before remapping if !isempty(uv) - point_attributes[:uv] = uv[attrib_maps[counter]] - counter += 1 + if !isempty(f_uv_n_faces[counter]) + point_attributes[:uv] = uv[attrib_maps[counter]] + counter += 1 + else + point_attributes[:uv] = uv[attrib_maps[counter-1]] + end end if !isempty(v_normals) - point_attributes[:normals] = v_normals[attrib_maps[counter]] - counter += 1 + if !isempty(f_uv_n_faces[counter]) + point_attributes[:normals] = v_normals[attrib_maps[counter]] + counter += 1 + else + point_attributes[:normals] = v_normals[attrib_maps[counter-1]] + end end if !isempty(material_ids) point_attributes[:material] = material_ids[attrib_maps[counter]] From fd36a88a7ef18c7ab242a4b0f08d601546282533 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 14 Jul 2024 16:49:09 +0200 Subject: [PATCH 4/7] normalize path syntax and allow broken paths --- src/io/obj.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index cb5b8c7..16c2884 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -486,8 +486,9 @@ function parse_texture_info(parent_path::String, lines::Vector{SubString{String} filepath = filepath * ' ' * lines[i] i += 1 end - @info filepath - if isfile(filepath) + filepath = replace(filepath, "\\\\" => "/") + filepath = replace(filepath, "\\" => "/") + if isfile(filepath) || endswith(lowercase(filepath), r"\.(png|jpg|jpeg|tiff|bmp)") output["filename"] = filepath idx = i else From 6ed5a79f7f9a81ff46883b21625821cb9da5017e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 15 Sep 2024 21:54:35 +0200 Subject: [PATCH 5/7] change origin offset -> offset --- src/io/obj.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index afebff3..7f7e925 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -353,11 +353,12 @@ function _load_mtl!(materials::Dict{String, Dict{String, Any}}, filename::String return materials end +# TODO: Consider generating a ShaderAbstractions Sampler? function parse_texture_info(parent_path::String, lines::Vector{SubString{String}}) idx = 1 output = Dict{String, Any}() name_lookup = Dict( - "o" => "origin offset", "s" => "scale", "t" => "turbulence", + "o" => "offset", "s" => "scale", "t" => "turbulence", "blendu" => "blend horizontal", "blendv" => "blend vertical", "boost" => "mipmap sharpness", "bm" => "bump multiplier" ) From 64d624d8611c878e49c7c3bc9a58be873116b9de Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Mon, 16 Sep 2024 05:19:12 +0200 Subject: [PATCH 6/7] remove redundant split_mesh function --- src/io/obj.jl | 55 +-------------------------------------------------- 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index 7f7e925..c9a4c0b 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -214,59 +214,6 @@ function save(f::Stream{format"OBJ"}, mesh::AbstractMesh) end -# Experimental stuff for loading .mtl files and working with multiple materials - -""" - MehsIO.split_mesh(mesh) - -Experimental function for splitting a mesh based material indices. -Also remaps vertices to avoid passing all vertices with a submesh. -""" -function split_mesh(mesh) - ps = coordinates(mesh) - ns = normals(mesh) - uvs = texturecoordinates(mesh) - ids = mesh.material - fs = faces(mesh) - - meshes = Dict{Int, Any}() - target_ids = unique(ids) - IndexType = eltype(eltype(fs)) - - for target_id in target_ids - _fs = eltype(fs)[] - indexmap = Dict{UInt32, UInt32}() - counter = MeshIO._typemin(IndexType) - - for f in fs - if any(ids[f] .== target_id) - f = map(f) do _i - i = GeometryBasics.value(_i) - if haskey(indexmap, i) - return indexmap[i] - else - indexmap[i] = counter - counter += 1 - return counter-1 - end - end - push!(_fs, f) - end - end - - indices = Vector{UInt32}(undef, counter-1) - for (old, new) in indexmap - indices[new] = old - end - - meshes[target_id] = GeometryBasics.Mesh( - meta(ps[indices], normals = ns[indices], uv = uvs[indices]), _fs - ) - end - - return meshes -end - function _load_mtl!(materials::Dict{String, Dict{String, Any}}, filename::String) endswith(filename, ".mtl") || error("Material Template Library $filename must be a .mtl file.") @@ -442,4 +389,4 @@ function parse_texture_info(parent_path::String, lines::Vector{SubString{String} end return output -end \ No newline at end of file +end From 9a834c4cfdc3ed81ebcbc7a7878ebf6d6e72e8d7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 3 Oct 2024 20:57:30 +0200 Subject: [PATCH 7/7] add transmission filter & allow simultaneous alpha + transmission --- src/io/obj.jl | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/io/obj.jl b/src/io/obj.jl index 7f7e925..fcf162f 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -150,7 +150,7 @@ function load(fn::File{format"OBJ"}; facetype=GLTriangleFace, try _load_mtl!(materials, joinpath(path, filename)) catch e - @error exception = e + @error "While parsing $(joinpath(path, filename)):" exception = e end end metadata[:materials] = materials @@ -280,6 +280,7 @@ function _load_mtl!(materials::Dict{String, Dict{String, Any}}, filename::String "Pc" => "clearcoat thickness", "Pcr" => "clearcoat roughness", "Ke" => "emissive", "aniso" => "anisotropy", "anisor" => "anisotropy rotation", + "Tf" => "transmission filter", # texture maps "map_Ka" => "ambient map", "map_Kd" => "diffuse map", "map_Ks" => "specular map", "map_Ns" => "shininess map", @@ -290,7 +291,7 @@ function _load_mtl!(materials::Dict{String, Dict{String, Any}}, filename::String "map_Pr" => "roughness map", "map_Pm" => "metallic map", "map_Ps" => "sheen map", "map_Ke" => "emissive map", "map_RMA" => "roughness metalness occlusion map", - "map_ORM" => "occlusion roughness metalness map" + "map_ORM" => "occlusion roughness metalness map", ) path = joinpath(splitpath(filename)[1:end-1]) @@ -318,17 +319,23 @@ function _load_mtl!(materials::Dict{String, Dict{String, Any}}, filename::String elseif command == "Ns" || command == "Ni" || command == "Pr" || command == "Pm" || command == "Ps" || command == "Pc" || command == "Pcr" || command == "Ke" || command == "aniso" || - command == "anisor" + command == "anisor" || command == "Tf" material[name_lookup[command]] = parse.(Float32, lines[1]) elseif command == "d" - haskey(material, "alpha") && error("Material alpha doubly defined.") - material[name_lookup[command]] = parse.(Float32, lines[1]) + alpha = parse.(Float32, lines[1]) + if haskey(material, "alpha") && !(material["alpha"] ≈ alpha) + @error("Material alpha doubly defined. Overwriting $(material["alpha"]) with $alpha.") + end + material[name_lookup[command]] = alpha elseif command == "Tr" - haskey(material, "alpha") && error("Material alpha doubly defined.") - material[name_lookup["d"]] = 1f0 - parse.(Float32, lines[1]) + alpha = 1f0 - parse.(Float32, lines[1]) + if haskey(material, "alpha") && !(material["alpha"] ≈ alpha) + @error("Material alpha doubly defined. Overwriting $(material["alpha"]) with $alpha") + end + material[name_lookup["d"]] = alpha # elseif Tf # transmission filter