Skip to content

Commit

Permalink
Merge pull request #18 from manyfold3d/compaction
Browse files Browse the repository at this point in the history
Re-order vertices during decimation
  • Loading branch information
Floppy authored Dec 18, 2024
2 parents 0312315 + 68343b5 commit aaddee8
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 72 deletions.
4 changes: 2 additions & 2 deletions examples/decimate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
target = geometry.faces.count

renderer.window.run do
# Decimate by 0.1%
# Decimate by 0.5% per frame
target = (target * 0.995).floor
exit if target <= 500
exit if target < 50
new_geometry, vertex_splits = decimator.decimate(target, vertex_splits: true)
puts "f: #{new_geometry.faces.count}, v: #{new_geometry.vertices.count}"
vertex_splits.each do |v|
Expand Down
4 changes: 2 additions & 2 deletions lib/mittsu/mesh_analysis/modifiers/decimator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ def decimate(target_face_count, vertex_splits: false)
edge_collapses = edge_collapse_costs.sort_by { |x| x[:cost] }
splits = []
loop do
break if @geometry.faces.count <= target_face_count
break if @geometry.faces.count <= target_face_count || edge_collapses.empty?
edge = edge_collapses.shift
splits.unshift @geometry.collapse(edge[:edge_index])
end
# Return vertex splits if requested
if vertex_splits
[@geometry, splits]
[@geometry, splits.compact]
else
@geometry
end
Expand Down
49 changes: 26 additions & 23 deletions lib/mittsu/mesh_analysis/winged_edge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,28 @@ def other_vertex(index)
(@start == index) ? @finish : @start
end

def reattach_vertex!(from:, to:)
def reattach_vertex(from:, to:)
out = clone
if @start == from
@start = to
out.start = to
elsif @finish == from
@finish = to
out.finish = to
end
out
end

def reattach_edge!(from:, to:)
if @start_left == from
@start_left = to
elsif @finish_left == from
@finish_left = to
elsif @start_right == from
@start_right = to
elsif @finish_right == from
@finish_right = to
def reattach_edge(from:, to:)
out = clone
if out.start_left == from
out.start_left = to
elsif out.finish_left == from
out.finish_left = to
elsif out.start_right == from
out.start_right = to
elsif out.finish_right == from
out.finish_right = to
end
out
end

def coincident_at(edge)
Expand Down Expand Up @@ -98,26 +102,25 @@ def normalize
# Stitches another edge into this one
# The edges must share a face and a vertex
# The edge passed as an argument will be invalid
# Returns nil if not stiched, or the face index that might need
# an edge reference update if it was
def stitch!(edge)
# Returns the new edge, or nil if stitch failed
def stitch(edge)
# Make sure the edges share a vertex and face
face = shared_face(edge)
return nil unless face && edge.coincident_at(edge)
# Flip incoming edge if it's not pointing the same way
edge = edge.flip unless same_direction?(edge)
# Stitch left side of other edge if our left face is the shared one, or vice versa
stitched_edge = clone
if face == @left
@start_left = edge.start_left
@finish_left = edge.finish_left
@left = edge.left
@left
stitched_edge.start_left = edge.start_left
stitched_edge.finish_left = edge.finish_left
stitched_edge.left = edge.left
else
@start_right = edge.start_right
@finish_right = edge.finish_right
@right = edge.right
@right
stitched_edge.start_right = edge.start_right
stitched_edge.finish_right = edge.finish_right
stitched_edge.right = edge.right
end
stitched_edge
end
end
end
89 changes: 51 additions & 38 deletions lib/mittsu/mesh_analysis/winged_edge_geometry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,65 +120,67 @@ def split(vertex:, left:, right:, displacement:, flatten: true)
# and merging everything onto the start vertex instead.
# If the result would be degenerate in some way, the mesh is unchanged
def collapse(index, flatten: true)
# find the edge
# Find edge, reorder vertices and reload
e0 = edge(index)
return if e0.nil?

# Create vertex split record
split = VertexSplit.new(vertex: e0.start)

# Calculate displacement vector and move old vertex
split.displacement = Mittsu::Vector3.new
split.displacement.sub_vectors(@vertices[e0.finish], @vertices[e0.start])
split.displacement.divide_scalar(2)
@vertices[e0.start].add(split.displacement)

# Collapse left face
start_left = @edges[e0.start_left]
finish_left = @edges[e0.finish_left]
if start_left && finish_left
split.left = start_left.other_vertex(e0.start)
start_left.stitch!(finish_left)
@edges[start_left.index] = start_left
end

# Collapse right face
start_right = @edges[e0.start_right]
finish_right = @edges[e0.finish_right]
if start_right && finish_right
split.right = finish_right.other_vertex(e0.start)
finish_right.stitch!(start_right)
@edges[finish_right.index] = finish_right
if e0
move_vertex_to_end(e0.finish)
e0 = edge(index)
else
# Invalid edge index
return nil
end

# Remove one vertex, and three edges
@vertices[e0.finish] = Mittsu::Vector3.new(0, 0, 0) # This can become nil later when we compact and reindex things
@edges[e0.finish_left] = nil
@edges[e0.start_right] = nil
@edges[e0.index] = nil
# Create vertex split record
split = VertexSplit.new(
vertex: e0.start,
left: edge(e0.start_left)&.other_vertex(e0.start),
right: edge(e0.start_right)&.other_vertex(e0.start),
displacement: Mittsu::Vector3.new.sub_vectors(@vertices[e0.finish], @vertices[e0.start]).divide_scalar(2)
)

# Create changeset to store edges that will be changed
changeset = {
e0.index => nil,
e0.finish_left => nil,
e0.finish_right => nil
}

# Collapse faces
stitched = collapse_face(e0, :left)
@edges[stitched.index] = stitched if stitched
stitched = collapse_face(e0, :right)
@edges[stitched.index] = stitched if stitched

# Reattach edges to remove old indexes
# This could be much more efficient by walking round the wings
@edges.each do |e|
next if e.nil?
e.reattach_edge!(from: finish_left.index, to: start_left.index) if finish_left && start_left
e.reattach_edge!(from: start_right.index, to: finish_right.index) if finish_right && start_right
e.reattach_vertex!(from: e0.finish, to: e0.start) if e0
next if e.nil? || e.index == e0.index
@edges[e.index] =
e.reattach_edge(from: e0.finish_left, to: e0.start_left)
.reattach_edge(from: e0.finish_right, to: e0.start_right)
.reattach_vertex(from: e0.finish, to: e0.start)
end

# Apply edge changes
changeset.each_pair { |i, e| @edges[i] = e }
# Move vertex
@vertices[e0.start] = Mittsu::Vector3.new.add_vectors(@vertices[e0.start], split.displacement)
# Remove old vertex
@vertices[e0.finish] = Mittsu::Vector3.new(0, 0, 0) # This can become nil later when we compact and reindex things
# Prepare for rendering
flatten! if flatten
# Return split parameters required to invert operation
split
end

def move_vertex_to_end(index)
return if index >= @vertices.count
return unless @vertices[index]
# Add move vertex to end of array
@vertices.push @vertices.slice!(index)
new_index = @vertices.count - 1
# Update all vertex references
@edges.each do |edge|
next if edge.nil?
if edge.start == index
edge.start = new_index
elsif edge.start > index
Expand All @@ -194,6 +196,17 @@ def move_vertex_to_end(index)

private

def collapse_face(e0, face)
if face == :left
start = @edges[e0.start_left]
finish = @edges[e0.finish_left]
else
start = @edges[e0.start_right]
finish = @edges[e0.finish_right]
end
start.stitch(finish) if start && finish
end

def find_edge_indexes(from:, to:)
@edges.select { |e| !e.nil? && (e.start == from && e.finish == to) }.map(&:index)
end
Expand Down
7 changes: 6 additions & 1 deletion spec/lib/mittsu/mesh_analysis/winged_edge_geometry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@
expect { geometry.collapse(100) }.to change { geometry.faces.count }.by(-2)
end

it "removes one vertex when collapsing a single edge" do
pending "awaiting implementation of vertex compaction"
expect { geometry.collapse(100) }.to change { geometry.vertices.count }.by(-1)
end

context "when inspecting vertex split data" do
let(:split_data) { geometry.collapse(100) }

Expand All @@ -150,7 +155,7 @@
end

it "returns right vertex index" do
expect(split_data.right).to eq 77
expect(split_data.right).to eq 481
end

it "returns displacement vector" do
Expand Down
18 changes: 12 additions & 6 deletions spec/lib/mittsu/mesh_analysis/winged_edge_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,22 @@
expect(edge.other_vertex(2)).to eq 1
end

it "can reattach start to a different vertex" do
expect { edge.reattach_vertex!(from: 1, to: 3) }.to change(edge, :start).from(1).to(3).and change(edge, :finish).by(0) # rubocop:todo RSpec/ChangeByZero
it "can reattach start to a different vertex" do # rubocop:disable RSpec/MultipleExpectations
new_edge = edge.reattach_vertex(from: 1, to: 3)
expect(new_edge.start).to eq 3
expect(new_edge.finish).to eq 2
end

it "can reattach end to a different vertex" do
expect { edge.reattach_vertex!(from: 2, to: 3) }.to change(edge, :finish).from(2).to(3).and change(edge, :start).by(0) # rubocop:todo RSpec/ChangeByZero
it "can reattach end to a different vertex" do # rubocop:disable RSpec/MultipleExpectations
new_edge = edge.reattach_vertex(from: 2, to: 3)
expect(new_edge.start).to eq 1
expect(new_edge.finish).to eq 3
end

it "does not change unattached vertices" do
expect { edge.reattach_vertex!(from: 5, to: 3) }.to change(edge, :start).by(0).and change(edge, :finish).by(0) # rubocop:todo RSpec/ChangeByZero
it "does not change unattached vertices" do # rubocop:disable RSpec/MultipleExpectations
new_edge = edge.reattach_vertex(from: 5, to: 3)
expect(new_edge.start).to eq 1
expect(new_edge.finish).to eq 2
end

context "when testing for duplication with #colinear?" do
Expand Down

0 comments on commit aaddee8

Please sign in to comment.