diff --git a/deps/build.jl b/deps/build.jl index 6e6e909..c4be771 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -2,7 +2,7 @@ using Base.Filesystem using BinDeps: unpack_cmd, download_cmd -const meshcat_sha = "efc157436ca4abf5706cf248c51d2e86cae1a0f0" +const meshcat_sha = "3122cecd5da022ad96bb0c8dc0f811a1bc492350" const meshcat_url = "https://github.com/rdeits/meshcat/archive/$meshcat_sha.zip" const assets_dir = normpath(joinpath(@__DIR__, "..", "assets")) diff --git a/src/MeshCat.jl b/src/MeshCat.jl index 4231a3e..977ffce 100644 --- a/src/MeshCat.jl +++ b/src/MeshCat.jl @@ -48,6 +48,7 @@ export Object, Triad, Mesh, MeshFileGeometry, + MeshFileObject, Points, Line, LineLoop, @@ -66,8 +67,12 @@ export Animation, export ArrowVisualizer +abstract type AbstractObject end +abstract type AbstractMaterial end + include("trees.jl") using .SceneTrees +include("mesh_files.jl") include("geometry.jl") include("objects.jl") include("animations.jl") diff --git a/src/geometry.jl b/src/geometry.jl index 7d8a3db..2954991 100644 --- a/src/geometry.jl +++ b/src/geometry.jl @@ -40,27 +40,6 @@ center(geometry::HyperSphere) = origin(geometry) center(geometry::Cylinder) = (origin(geometry) + geometry.extremity) / 2 center(geometry::Cone) = (origin(geometry) + geometry.apex) / 2 -""" -An MeshFileGeometry represents a mesh which is stored as the raw contents -of a file, rather than as a collection of points and vertices. This is useful for -transparently passing mesh files which we can't load in Julia directly to meshcat. -""" -struct MeshFileGeometry{S <: Union{String, Vector{UInt8}}} - contents::S - format::String -end - -function MeshFileGeometry(filename) - ext = lowercase(splitext(filename)[2]) - if ext ∈ (".obj", ".dae") - MeshFileGeometry(open(f -> read(f, String), filename), ext[2:end]) - elseif ext == ".stl" - MeshFileGeometry(open(read, filename), ext[2:end]) - else - throw(ArgumentError("Unsupported extension: $ext. Only .obj, .dae, and .stl meshes can be used to construct MeshFileGeometry")) - end -end - """ $(SIGNATURES) diff --git a/src/lowering.jl b/src/lowering.jl index 4655492..0c3afaf 100644 --- a/src/lowering.jl +++ b/src/lowering.jl @@ -185,11 +185,26 @@ end function lower(geom::MeshFileGeometry) Dict{String, Any}( "uuid" => string(uuid1()), - "type" => "_meshfile", + "type" => "_meshfile_geometry", "format" => geom.format, "data" => pack_mesh_file_data(geom.contents)) end +function lower(obj::MeshFileObject) + Dict{String, Any}( + "metadata" => Dict{String, Any}("version" => 4.5, "type" => "Object"), + "geometries" => [], + "materials" => [], + "object" => Dict{String, Any}( + "uuid" => string(uuid1()), + "type" => "_meshfile_object", + "format" => obj.format, + "data" => pack_mesh_file_data(obj.contents), + "mtl_library" => obj.mtl_library, + "resources" => obj.resources)) +end + + # TODO: Unify these two methods once https://github.com/rdeits/meshcat/issues/50 is resolved pack_mesh_file_data(s::AbstractString) = s pack_mesh_file_data(s::AbstractVector{UInt8}) = PackedVector(s) @@ -228,7 +243,7 @@ end function lower(img::PngImage) Dict{String, Any}( "uuid" => string(uuid1()), - "url" => string("data:image/png;base64,", base64encode(img.data)) + "url" => data_uri(img.data) ) end diff --git a/src/mesh_files.jl b/src/mesh_files.jl new file mode 100644 index 0000000..bd798ba --- /dev/null +++ b/src/mesh_files.jl @@ -0,0 +1,114 @@ + +""" +An MeshFileGeometry represents a mesh which is stored as the raw contents +of a file, rather than as a collection of points and vertices. This is useful for +transparently passing mesh files which we can't load in Julia directly to meshcat. + +Supported formats: + * .stl (ASCII and binary) + * .obj + * .dae (Collada) + +For .obj and .dae files, only a single mesh geometry will be loaded, and any +material or texture properties will be ignored. To load an entire collection of +objects (complete with materials and textures) from an .obj or .dae file, see +MeshFileObject instead. +""" +struct MeshFileGeometry{S <: Union{String, Vector{UInt8}}} + contents::S + format::String +end + +function MeshFileGeometry(filename) + ext = lowercase(splitext(filename)[2]) + if ext ∈ (".obj", ".dae") + MeshFileGeometry(open(f -> read(f, String), filename), ext[2:end]) + elseif ext == ".stl" + MeshFileGeometry(open(read, filename), ext[2:end]) + else + throw(ArgumentError("Unsupported extension: $ext. Only .obj, .dae, and .stl meshes can be used to construct MeshFileGeometry")) + end +end + + +""" +A MeshFileObject is similar to a MeshFileGeometry, but rather than representing +a single geometry, it supports loading an entire object (with geometries, +materials, and textures), or even a collection of objects (for supported file +types). + +Supported formats: + * .obj + * .dae (Collada) + +Since mesh files may include references to other files (such as texture images), +the MeshFileObject also includes a `resources` dictionary mapping relative paths +(as specified in the mesh file) to the contents of the referenced files. Those contents are specified using data URIs, e.g.: + + data:image/png;base64, + +If you construct a MeshFileObject from a `.dae` or `.obj` file, the resources +dictionary will automatically be populated. +""" +struct MeshFileObject <: AbstractObject + contents::String + format::String + mtl_library::Union{String, Nothing} + resources::Dict{String, String} +end + +function MeshFileObject(filename) + ext = lowercase(splitext(filename)[2]) + if ext ∉ (".obj", ".dae") + throw(ArgumentError("Unsupported extension: $ext. Only .obj and .dae meshes can be used to construct MeshFileObject")) + end + contents = open(f -> read(f, String), filename) + format = ext[2:end] + if ext == ".obj" + mtl_library = load_mtl_library(contents, dirname(filename)) + resources = load_mtl_textures(mtl_library, dirname(filename)) + else + mtl_library = nothing + resources = load_dae_textures(contents, dirname(filename)) + end + MeshFileObject(contents, format, mtl_library, resources) +end + +function load_mtl_library(obj_contents, directory=".") + libraries = String[] + for line in eachline(IOBuffer(obj_contents)) + m = match(r"^mtllib (.*)$", line) + if m !== nothing + push!(libraries, open(f -> read(f, String), joinpath(directory, m.captures[1]))) + end + end + join(libraries, '\n') +end + +data_uri(contents::Vector{UInt8}) = string("data:image/png;base64,", base64encode(contents)) + +function load_mtl_textures(mtl_contents, directory=".") + textures = Dict{String, String}() + for line in eachline(IOBuffer(mtl_contents)) + m = match(r"^map_[^ ]* (.*)$", line) + if m !== nothing + name = m.captures[1] + contents = open(read, joinpath(directory, name)) + textures[name] = data_uri(contents) + end + end + textures +end + +function load_dae_textures(dae_contents, directory=".") + # TODO: this is probably not a very robust parsing strategy, + # but I don't have enough examples to work from to decide on a + # better one, so I'm keeping it simple. + r = r"\]*\>\s*([^\<]*)\<\/init_from\>"m + textures = Dict{String, String}() + for match in eachmatch(r, dae_contents) + name = match.captures[1] + textures[name] = data_uri(open(read, joinpath(directory, name))) + end + textures +end diff --git a/src/objects.jl b/src/objects.jl index dc16429..5871043 100644 --- a/src/objects.jl +++ b/src/objects.jl @@ -1,6 +1,4 @@ const GeometryLike = Union{AbstractGeometry, AbstractMesh, MeshFileGeometry} -abstract type AbstractObject end -abstract type AbstractMaterial end struct Object{G <: GeometryLike, M <: AbstractMaterial} <: AbstractObject geometry::G @@ -76,3 +74,4 @@ LineBasicMaterial(;kw...) = GenericMaterial(_type="LineBasicMaterial"; kw...) size::Float32 = 0.002 vertexColors::Int = 2 end + diff --git a/test/data/meshes/cube.dae b/test/data/meshes/cube.dae new file mode 100644 index 0000000..1c12574 --- /dev/null +++ b/test/data/meshes/cube.dae @@ -0,0 +1,148 @@ + + + + + VCGLab + VCGLib | MeshLab + + Y_UP + Tue May 28 03:20:42 2019 + Tue May 28 03:20:42 2019 + + + + cube.png + + + + + + + + + + + + + texture0 + A8R8G8B8 + + + + + texture0-surface + LINEAR + LINEAR + + + + + + 0 0 0 1 + + + 0 0 0 1 + + + + + + 0 0 0 1 + + + 0.3 + + + 0 0 0 1 + + + 0.5 + + + 0 0 0 1 + + + 0 + + + 0 + + + + + + + + + + + 0 0 0 1 0 0 1 1 0 0 1 0 0 0 1 1 0 1 1 1 1 0 1 1 + + + + + + + + + + 0 0 -1 0 0 -1 0 0 1 0 0 1 0 -1 0 0 -1 0 1 0 0 1 0 0 0 1 0 0 1 0 -1 0 0 -1 0 0 + + + + + + + + + + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + + + + + + + + + + 0.3 0.375 0.3 0.375 0.3 0.375 0.3 0.375 0.3 0.375 0.3 0.375 0.3 0.875 0.3 0.875 0.3 0.875 0.3 0.875 0.3 0.875 0.3 0.875 0.3 0.625 0.3 0.625 0.3 0.625 0.3 0.625 0.3 0.625 0.3 0.625 0.6 0.625 0.6 0.625 0.6 0.625 0.6 0.625 0.6 0.625 0.6 0.625 0.3 0.125 0.3 0.125 0.3 0.125 0.3 0.125 0.3 0.125 0.3 0.125 0.2 0.625 0.2 0.625 0.2 0.625 0.2 0.625 0.2 0.625 0.2 0.625 + + + + + + + + + + + + + + + +

2 2 0 0 1 1 0 1 0 0 0 2 0 0 1 3 3 3 1 4 2 2 1 5 4 4 2 6 5 5 2 7 6 6 2 8 6 6 3 9 7 7 3 10 4 4 3 11 0 0 4 12 1 1 4 13 5 5 4 14 5 5 5 15 4 4 5 16 0 0 5 17 1 1 6 18 2 2 6 19 6 6 6 20 6 6 7 21 5 5 7 22 1 1 7 23 2 2 8 24 3 3 8 25 7 7 8 26 7 7 9 27 6 6 9 28 2 2 9 29 3 3 10 30 0 0 10 31 4 4 10 32 4 4 11 33 7 7 11 34 3 3 11 35

+
+
+
+
+ + + + + + + + + + + + + + + + + + +
diff --git a/test/data/meshes/cube.mtl b/test/data/meshes/cube.mtl new file mode 100644 index 0000000..9b6ebf9 --- /dev/null +++ b/test/data/meshes/cube.mtl @@ -0,0 +1,8 @@ +newmtl material_0 +Ka 1.0 1.0 1.0 +Kd 1.000000 1.000000 1.000000 +Ks 1.000000 1.000000 1.000000 +Tr 0.000000 +illum 2 +Ns 0.000000 +map_Kd cube.png diff --git a/test/data/meshes/cube.obj b/test/data/meshes/cube.obj new file mode 100644 index 0000000..edbc07e --- /dev/null +++ b/test/data/meshes/cube.obj @@ -0,0 +1,38 @@ +mtllib ./cube.mtl +usemtl material_0 + +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 1.0 1.0 0.0 +v 0.0 1.0 0.0 +v 0.0 0.0 1.0 +v 1.0 0.0 1.0 +v 1.0 1.0 1.0 +v 0.0 1.0 1.0 + +vt 0.3 0.875 +vt 0.2 0.625 +vt 0.3 0.625 +vt 0.6 0.625 +vt 0.3 0.375 +vt 0.3 0.125 + +vn 0 0 -1 +vn 0 0 1 +vn 0 -1 0 +vn 1 0 0 +vn 0 1 0 +vn -1 0 0 + +f 3/5/1 2/5/1 1/5/1 +f 1/5/1 4/5/1 3/5/1 +f 5/1/2 6/1/2 7/1/2 +f 7/1/2 8/1/2 5/1/2 +f 1/3/3 2/3/3 6/3/3 +f 6/3/3 5/3/3 1/3/3 +f 2/4/4 3/4/4 7/4/4 +f 7/4/4 6/4/4 2/4/4 +f 3/6/5 4/6/5 8/6/5 +f 8/6/5 7/6/5 3/6/5 +f 4/2/6 1/2/6 5/2/6 +f 5/2/6 8/2/6 4/2/6 \ No newline at end of file diff --git a/test/data/meshes/cube.png b/test/data/meshes/cube.png new file mode 100644 index 0000000..16a5448 Binary files /dev/null and b/test/data/meshes/cube.png differ diff --git a/test/data/meshes/cube.stl b/test/data/meshes/cube.stl new file mode 100644 index 0000000..8c89c08 Binary files /dev/null and b/test/data/meshes/cube.stl differ diff --git a/test/visualizer.jl b/test/visualizer.jl index 61109b4..c7ea397 100644 --- a/test/visualizer.jl +++ b/test/visualizer.jl @@ -109,27 +109,45 @@ end settransform!(v[:cat_color], Translation(0, -2.0, 0) ∘ LinearMap(RotZ(π)) ∘ LinearMap(RotX(π/2))) end - @testset "mesh files" begin - settransform!(v[:mesh_files], Translation(0, -3.0, 0)) + @testset "mesh file geometries" begin + settransform!(v[:mesh_file_geometries], Translation(0, -3.0, 0)) base_path = joinpath(@__DIR__, "data", "meshes", "mesh_0_convex_piece_0") @testset "obj" begin path = base_path * ".obj" - setobject!(v[:mesh_files, :obj], MeshFileGeometry(path)) + setobject!(v[:mesh_file_geometries, :obj], MeshFileGeometry(path)) end @testset "dae" begin path = base_path * ".dae" - setobject!(v[:mesh_files, :dae], MeshFileGeometry(path)) - settransform!(v[:mesh_files, :dae], Translation(0, -0.5, 0)) + setobject!(v[:mesh_file_geometries, :dae], MeshFileGeometry(path)) + settransform!(v[:mesh_file_geometries, :dae], Translation(0, -0.5, 0)) end @testset "stl_ascii" begin path = base_path * ".ascii.stl" - setobject!(v[:mesh_files, :stl_ascii], MeshFileGeometry(path)) - settransform!(v[:mesh_files, :stl_ascii], Translation(0, -1.0, 0)) + setobject!(v[:mesh_file_geometries, :stl_ascii], MeshFileGeometry(path)) + settransform!(v[:mesh_file_geometries, :stl_ascii], Translation(0, -1.0, 0)) end @testset "stl_binary" begin path = base_path * ".binary.stl" - setobject!(v[:mesh_files, :stl_binary], MeshFileGeometry(path)) - settransform!(v[:mesh_files, :stl_binary], Translation(0, -1.5, 0)) + setobject!(v[:mesh_file_geometries, :stl_binary], MeshFileGeometry(path)) + settransform!(v[:mesh_file_geometries, :stl_binary], Translation(0, -1.5, 0)) + end + end + + @testset "mesh file objects" begin + let v = v[:mesh_file_objects] + settransform!(v, Translation(0, 3.0, -1.5)) + + base_path = joinpath(@__DIR__, "data", "meshes") + @testset "obj" begin + path = joinpath(base_path, "cube.obj") + setobject!(v[:obj], MeshFileObject(path)) + end + + @testset "dae" begin + path = joinpath(base_path, "cube.dae") + setobject!(v[:dae], MeshFileObject(path)) + settransform!(v[:dae], Translation(0, -1.5, 0)) + end end end