Skip to content

Commit

Permalink
Merge pull request #97 from JuliaIO/ff/GeometryBasics_refactor
Browse files Browse the repository at this point in the history
Update MeshIO for GeometryBasics refactor
  • Loading branch information
ffreyer authored Nov 19, 2024
2 parents 9f452ff + 8666d91 commit fd17a83
Show file tree
Hide file tree
Showing 14 changed files with 4,093 additions and 216 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
name: CI
on:
- push
- pull_request

push:
branches:
- master
tags: "*"
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
Expand All @@ -15,7 +18,6 @@ jobs:
fail-fast: false
matrix:
version:
- '1.3'
- '1.6'
- '1'
os:
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "MeshIO"
uuid = "7269a6da-0436-5bbc-96c2-40638cbb6118"
author = "Simon Danisch"
version = "0.4.13"
version = "0.5.0"

[deps]
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
Expand All @@ -12,7 +12,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
[compat]
ColorTypes = "0.8, 0.9, 0.10, 0.11, 0.12"
FileIO = "1.2.4"
GeometryBasics = "0.4.1"
GeometryBasics = "0.5"
julia = "1.3"

[extras]
Expand Down
58 changes: 22 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![codecov.io](http://codecov.io/github/JuliaIO/MeshIO.jl/coverage.svg?branch=master)](http://codecov.io/github/JuliaIO/MeshIO.jl?branch=master)
[![Coverage Status](https://coveralls.io/repos/JuliaIO/MeshIO.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/JuliaIO/MeshIO.jl?branch=master)

This package supports loading 3D model file formats: `obj`, `stl`, `ply`, `off` and `2DM`.
This package supports loading 3D model file formats: `obj`, `stl`, `ply`, `off`, `msh` and `2DM`.
More 3D model formats will be supported in the future.

## Installation
Expand All @@ -22,49 +22,35 @@ This means loading a mesh is as simple as this:
using FileIO
mesh = load("path/to/mesh.obj")
```
The result will usually be a [GeometryBasics](https://github.com/JuliaGeometry/GeometryBasics.jl) `Mesh`.
The exception are `obj` files with non-vertex data such as material data or "group" tags, which return a `MetaMesh`.

Displaying a mesh can be achieved with [Makie](https://github.com/JuliaPlots/Makie.jl).

Functions for mesh manipulation can be found in [JuliaGeometry](https://github.com/JuliaGeometry)

## Additional Information

MeshIO now has the HomogenousMesh type. Name is still not settled, but it's supposed to be a dense mesh with all attributes either having the length of one (constant over the whole mesh) or the same length (per vertex).
This meshtype holds a large variability for all the different attribute mixtures that I've encountered while trying to visualize things over at GLVisualize. This is the best type I've found so far to encode this large variability, without an explosion of functions.

The focus is on conversion between different mesh types and creation of different mesh types.
This has led to some odd seeming design choices.
First, you can get an attribute via `decompose(::Type{AttributeType}, ::Mesh)`.
This will try to get this attribute, and if it has the wrong type try to convert it, or if it is not available try to create it.
So `decompose(Point3{Float32}, mesh)` on a mesh with vertices of type `Point3{Float64}` will return a vector of type `Point3{Float32}`.
Similarly, if you call `decompose(Normal{3, Float32}, mesh)` but the mesh doesn't have normals, it will call the function `normals(mesh.vertices, mesh.faces, Normal{3, Float32}`, which will create the normals for the mesh.
As most attributes are independent, this enables us to easily create all kinds of conversions.
Also, I can define `decompose` for arbitrary geometric types.
`decompose{T}(Point3{T}, r::Rectangle)` can actually return the needed vertices for a rectangle.
This together with `convert` enables us to create mesh primitives like this:
```Julia
MeshType(Cube(...))
MeshType(Sphere(...))
MeshType(Volume, 0.4f0) #0.4f0 => isovalue
```
### Usage

Similarly, I can pass a meshtype to an IO function, which then parses only the attributes that I really need.
So passing `Mesh{Point3{Float32}, Face3{UInt32}}` to the obj importer will skip normals, uv coordinates etc, and automatically converts the given attributes to the right number type.
The GeometryBasics `Mesh` supports vertex attributes with different lengths which get addressed by different faces (as of 0.5).
As such MeshIO makes no effort to convert vertex attributes to a common length, indexed by one set of faces.
If you need a single set of faces, e.g. for rendering, you can use `new_mesh = GeometryBasics.expand_faceviews(mesh)` to generate a fitting mesh.

To put this one level further, the `Face` type has the index offset relative to Julia's indexing as a parameter (e.g. `Face3{T, 0}` is 1 indexed). Also, you can index into an array with this face type, and it will convert the indexes correctly while accessing the array. So something like this always works, independent of the underlying index offset:
```Julia
v1, v2, v3 = vertices[face]
The GeometryBasics `Mesh` allows for different element types for coordinates, normals, faces, etc.
These can set when loading a mesh using keyword arguments:
```julia
load(filename; pointtype = Point3f, uvtype = Vec2f, facetype = GLTriangleFace, normaltype = Vec3f)
```
Also, the importer is sensitive to this, so if you always want to work with 0-indexed faces (like it makes sense for opengl based visualizations), you can parse the mesh already as an 0-indexed mesh, by just defining the mesh format to use `Face3{T, -1}`. (only the OBJ importer yet)
Note that not every file format supports normals and uvs (texture coordinates) and thus some loaders don't accept `uvtype` and/or `normaltype`.

Small example to demonstrate the advantage for IO:
```Julia
#Export takes any mesh
function write{M <: Mesh}(msh::M, fn::File{:ply_binary})
# even if the native mesh format doesn't have an array of dense points or faces, the correct ones will
# now be created, or converted:
vts = decompose(Point3{Float32}, msh) # I know ply_binary needs Point3{Float32}
fcs = decompose(Face3{Int32, -1}, msh) # And 0 indexed Int32 faces.
#write code...
end
```
The facetypes from GeometryBasics support 0 and 1-based indexing using `OffsetInteger`s.
For example `GLTriangleFace` is an alias for `NgonFace{3, OffsetInteger{-1, UInt32}}`, i.e. a face containing 3 indices offset from 1-based indexing by `-1`.
The raw data in a `GLTriangleFace` is 0-based so that it can be uploaded directly in a Graphics API.
In Julia code it gets converted back to a 1-based Int, so that it can be used as is.

### Extending MeshIO

To implement a new file format you need to add the appropriate `load()` and `save()` methods.
You also need to register the file format with [FileIO](https://juliaio.github.io/FileIO.jl/stable/registering/)
For saving it may be useful to know that you can convert vertex data to specific types using the [decompose interface](https://juliageometry.github.io/GeometryBasics.jl/stable/decomposition/).
2 changes: 0 additions & 2 deletions src/MeshIO.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ using FileIO: FileIO, @format_str, Stream, File, stream, skipmagic

import Base.show

include("util.jl")

include("io/off.jl")
include("io/ply.jl")
include("io/stl.jl")
Expand Down
12 changes: 5 additions & 7 deletions src/io/gts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ function parseGtsLine( s::AbstractString, C, T=eltype(C) )
end
end

function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh )
function load( st::Stream{format"GTS"}; facetype=GLTriangleFace, pointtype=Point)
io = stream(st)
head = readline( io )
FT = facetype(MeshType)
VT = vertextype(MeshType)

nVertices, nEdges, nFacets = parseGtsLine( head, Tuple{Int,Int,Int} )
iV = iE = iF = 1
vertices = Vector{VT}(undef, nVertices)
vertices = Vector{pointtype}(undef, nVertices)
edges = Vector{Vector{Int}}(undef, nEdges)
facets = Vector{Vector{Int}}(undef, nFacets)
for full_line::String in eachline(io)
Expand All @@ -30,7 +28,7 @@ function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh )

if !startswith(line, "#") && !isempty(line) && !all(iscntrl, line) #ignore comments
if iV <= nVertices
vertices[iV] = parseGtsLine( line, VT )
vertices[iV] = parseGtsLine( line, pointtype )
iV += 1
elseif iV > nVertices && iE <= nEdges
edges[iE] = parseGtsLine( line, Array{Int} )
Expand All @@ -41,8 +39,8 @@ function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh )
end # if
end # if
end # for
faces = [ FT( union( edges[facets[i][1]], edges[facets[i][2]], edges[facets[i][3]] ) ) for i in 1:length(facets) ] # orientation not guaranteed
return MeshType( vertices, faces )
faces = [ facetype( union( edges[facets[i][1]], edges[facets[i][2]], edges[facets[i][3]] ) ) for i in 1:length(facets) ] # orientation not guaranteed
return Mesh( vertices, faces )
end

function save( st::Stream{format"GTS"}, mesh::AbstractMesh )
Expand Down
12 changes: 6 additions & 6 deletions src/io/ifs.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function load(fs::Stream{format"IFS"}, MeshType = GLNormalMesh)
function load(fs::Stream{format"IFS"}; facetype=GLTriangleFace, pointtype=Point3f)
io = stream(fs)
function str()
n = read(io, UInt32)
Expand All @@ -11,15 +11,15 @@ function load(fs::Stream{format"IFS"}, MeshType = GLNormalMesh)
end
nverts = read(io, UInt32)
verts_float = read(io, Float32, nverts * 3)
verts = reinterpret(Point3f0, verts_float)
verts = reinterpret(pointtype, verts_float)
tris = str()
if tris != "TRIANGLES\0"
error("$(filename(fs)) does not seem to be of format IFS")
end
nfaces = read(io, UInt32)
faces_int = read(io, UInt32, nfaces * 3)
faces = reinterpret(GLTriangle, faces_int)
MeshType(vertices = verts, faces = faces)
faces = reinterpret(facetype, faces_int)
return GeometryBasics.Mesh(verts, faces)
end

function save(fs::Stream{format"IFS"}, msh::AbstractMesh; meshname = "mesh")
Expand All @@ -29,8 +29,8 @@ function save(fs::Stream{format"IFS"}, msh::AbstractMesh; meshname = "mesh")
write(io, UInt32(length(s0)))
write(io, s0)
end
vts = decompose(Point3f0, msh)
fcs = decompose(GLTriangle, msh)
vts = decompose(Point3f, msh)
fcs = decompose(GLTriangleFace, msh)

# write the header
write0str("IFS")
Expand Down
Loading

2 comments on commit fd17a83

@ffreyer
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/119762

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.0 -m "<description of version>" fd17a8305ad48b8de23fa7f45ca8a95519282eb4
git push origin v0.5.0

Please sign in to comment.