How to broadcast a scalar multiplication through a Mesh?

I’m trying to port a JavaScript 3D slicer example to Julia. The example loads a 3D model of a bunny (from https://enkimute.github.io/ganja.js/examples/bunny.obj having 5002 triangle faces) and then calculates horizontal cross sections. The JavaScript version scales all the vertex coordinates in the mesh by a factor of 13 while parsing the individual text lines of the bunny object file. The Julia version can parse the whole bunny object file using Makie.FileIO’s load(). A scalar multiplication broadcast works at the level of triangle faces, but I don’t know how to save the multiplication results back into the structure.

julia> using Makie.FileIO

julia> faces = load("bunny.obj");

julia> nFace = length(faces)
5002

julia> typeof(faces)
GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}

julia> typeof(faces[1]) # 3 vertices of a triangle face
GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}

julia> typeof(faces[1][1]) # 3 coordinates of a vertex
Point{3, Float32}

julia> typeof(faces[1][1][1]) # a coordinate
Float32

julia> faces[1]
Triangle(Float32[-0.089491, 0.143926, 0.0124885], Float32[-0.0865619, 0.142492, 0.00843268], Float32[-0.0896984, 0.139713, 0.0137748])

julia> faces[1] .* 13
3-element Vector{Point{3, Float32}}:
 [-1.163383, 1.871038, 0.1623505]
 [-1.1253047, 1.852396, 0.10962484]
 [-1.1660792, 1.816269, 0.1790724]

julia> faces[1] = faces[1] .* 13
ERROR: CanonicalIndexError: setindex! not defined for GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}
Stacktrace:
 [1] error_if_canonical_setindex(#unused#::IndexCartesian, A::GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}, #unused#::Int64)
   @ Base .\abstractarray.jl:1354
 [2] setindex!(A::GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}, v::Vector{Point{3, Float32}}, I::Int64)
   @ Base .\abstractarray.jl:1343
 [3] top-level scope
   @ REPL[17]:1

julia>

I tried using StaticArrays and declaring faces::MArray = load(“bunny.obj”) but got the following error and hint, but I don’t know how to apply the hint to such a complex structure. Any suggestions?

ERROR: LoadError: The size of type `MArray` is not known.

If you were trying to construct (or `convert` to) a `StaticArray` you
may need to add the size explicitly as a type parameter so its size is
inferrable to the Julia compiler (or performance would be terrible). For
example, you might try

    m = zeros(3,3)
    SMatrix(m)            # this error
    SMatrix{3,3}(m)       # correct - size is inferrable
    SArray{Tuple{3,3}}(m) # correct, note Tuple{3,3}

Stacktrace:
 [1] error(s::String)
   @ Base .\error.jl:35
 [2] missing_size_error(#unused#::Type{MArray})
   @ StaticArraysCore C:\Users\gsgm2\.julia\packages\StaticArraysCore\U2Z1K\src\StaticArraysCore.jl:475
 [3] Size(#unused#::Type{MArray})
   @ StaticArraysCore C:\Users\gsgm2\.julia\packages\StaticArraysCore\U2Z1K\src\StaticArraysCore.jl:491
 [4] length(a::Type{MArray})
   @ StaticArrays C:\Users\gsgm2\.julia\packages\StaticArrays\jA1zK\src\abstractarray.jl:2
 [5] convert(#unused#::Type{MArray}, a::GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}})
   @ StaticArrays C:\Users\gsgm2\.julia\packages\StaticArrays\jA1zK\src\convert.jl:195
 [6] top-level scope
   @ C:\dev\olarth\PGA\ripga2d3d\slicing.jl:9
 [7] include(fname::String)
   @ Base.MainInclude .\client.jl:476
 [8] top-level scope
   @ REPL[1]:1
in expression starting at C:\dev\olarth\PGA\ripga2d3d\slicing.jl:9

Don’t use StaticArrays for something like this. StaticArrays are when the length is a small compile-time constant, and they will be terrible for a length-5002 array (especially whose length is known only at runtime, but even if this were a compile-time constant it would be too long for a StaticArray to be a good idea).

If you want to convert faces= load(“bunny.obj”) into an ordinary Array, you could do Array(faces), since [GeometryMesh is a type of AbstractVector. Or, in one step:

faces= Array(load(“bunny.obj”))

That’s because the Geometry.Mesh is a read-only structure (it implements getindex but not setindex!). That’s why you have to convert to a mutable type like Array to write into the faces array.

2 Likes

That removes some abstraction but apparently not enough to allow a scaled element to be saved.

julia> using Makie.FileIO, GeometryBasics

julia> faces = load("bunny.obj");

julia> length(faces)
5002

julia> Afaces = Array(faces);

julia> length(Afaces)
5002

julia> typeof(faces)
GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, SimpleFaceView{3, Float32, 3, OffsetInteger{-1, UInt32}, Point{3, Float32}, NgonFace{3, OffsetInteger{-1, UInt32}}}}

julia> typeof(Afaces)
Vector{Ngon{3, Float32, 3, Point{3, Float32}}} (alias for Array{GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}, 1})

julia> typeof(Afaces[1])
GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}

julia> Afaces[1]
Triangle(Float32[-0.089491, 0.143926, 0.0124885], Float32[-0.0865619, 0.142492, 0.00843268], Float32[-0.0896984, 0.139713, 0.0137748])

julia> Afaces[1] .* 13
3-element Vector{Point{3, Float32}}:
 [-1.163383, 1.871038, 0.1623505]
 [-1.1253047, 1.852396, 0.10962484]
 [-1.1660792, 1.816269, 0.1790724]

julia> Afaces[1] = Afaces[1] .* 13
ERROR: MethodError: Cannot `convert` an object of type Vector{Point{3, Float32}} to an object of type GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}
Closest candidates are:
  convert(::Type, ::GeoInterface.AbstractGeometryTrait, ::Any) at C:\Users\gsgm2\.julia\packages\GeoInterface\J298z\src\fallbacks.jl:112
  convert(::Type{T}, ::T) where T at Base.jl:61
  GeometryBasics.Ngon{Dim, T, N, Point}(::Any) where {Dim, T<:Real, N, Point<:AbstractPoint{Dim, T}} at C:\Users\gsgm2\.julia\packages\GeometryBasics\KE3OI\src\basic_types.jl:60
Stacktrace:
 [1] setindex!(A::Vector{GeometryBasics.Ngon{3, Float32, 3, Point{3, Float32}}}, x::Vector{Point{3, Float32}}, i1::Int64)
   @ Base .\array.jl:966
 [2] top-level scope
   @ REPL[155]:1

julia>

For a while I was hopeful about using GeometryBasics’ coordinates(), but that loses about half of the faces.

julia> Cfaces = coordinates(faces);

julia> length(Cfaces)
2503

julia> typeof(Cfaces)
Vector{Point{3, Float32}} (alias for Array{Point{3, Float32}, 1})

julia>

There might be some way for Array() or coordinates() or decompose() or convert() or collect() to remove enough abstraction while retaining all the triangle faces but I haven’t found it.

1 Like