Code covered by the BSD License  

Highlights from
Fast Loop mesh subdivision

  • ... LoopSubdivisionLimited - FUNCTION Perform one round of Loop mesh subdivision, with a resolution limit
  • View all files
image thumbnail
from Fast Loop mesh subdivision by Dylan Muir
Subdivide a surface mesh, using Loop subdivision. Boundary- and shape-maintaining.

...
function [mfRefinedMesh, mnTriangulation] = ...
   LoopSubdivisionLimited( mfMeshPoints, mnTriangulation, fMinResolution, ...
                           vbBoundaryEdges)

% LoopSubdivisionLimited - FUNCTION Perform one round of Loop mesh subdivision, with a resolution limit
%
% Usage: [mfRefinedMesh, mnTriangulation] = ...
%           LoopSubdivisionLimited( mfMeshPoints, mnTriangulation, fMinResolution, ...
%                                   <vbBoundaryEdges>)
%
% This function sub-divides surface meshes, using the Loop subdivision algorithm
% [1].  This algorithm is based on B-spline curve continuity, leading to good
% shape-maintaining smoothing of a surface.  The algorithm attempts to leave the
% boundary of the surface essentially undistorted.
%
% 'mfMeshPoints' is an Nx3 matrix, each row of which ['x' 'y' 'z'] defines a
% point in three-dimensional space.  'mnTriangulation' is a Mx3 matrix, each row
% of which  ['m' 'n' 'p'] defines a triangle existing on the surface, where 'm',
% 'n' and 'p' are indices into 'mfMeshPoints'.
%
% 'fMinResolution' defines the desired minimum length of an edge in the
% final subdivision.  Edges shorter than 'fMinResolution' will not be
% divided further.
%
% The optional argument 'vbBoundaryEdges' identifies which edges should be
% treated as boundary edges (and so should their locations should be attempted
% to be maintained by the algorithm).  This argument will be calculated by the
% algortihm, if it is not supplied.
%
% 'mfRefinedMesh' will be a Px3 matrix, each row of which specifies a vertex in
% the subdivided mesh.  'mnTringulation' will be a Rx3 matrix, each row of which
% specifies a surface triangle in the subdivided mesh.
%
% Algorithm from [1].
%
% ROOM FOR IMPROVEMENT
% * If you work out how to maintain the vertex and edge adjacency matrices
% through a full subdivision run, then great!  That would speed up subsequent
% runs a great deal, since a lot of the time is spent computing the edge
% adjacency matrix...
%
%
% References
% [1] Loop, C 1987. "Smooth subdivision surfaces based on triangles."  M.S.
%        Mathematics thesis, University of Utah.
%        http://research.microsoft.com/en-us/um/people/cloop/thesis.pdf

% Author: Dylan Muir <dylan@ini.phys.ethz.ch>
% Created: 30th November, 2009

% -- Check arguments

if (nargin < 2)
   disp('*** LoopSubdivisionLimited: Incorrect usage');
   help LoopSubdivisionLimited;
   return;
end

% - Ensure triangulation is all integers
mnTriangulation = round(mnTriangulation);

if ((min(mnTriangulation(:)) < 1) || (max(mnTriangulation(:)) > size(mfMeshPoints, 1)))
   disp('*** LoopSubdivisionLimited: Entries in ''mnTriangulation'' must all be indices for ''mfMeshPoints''.');
   return;
end


% -- Compute edge and adjacency matrices

% - Throw away points not used in the triangulation
vnUsedPoints = unique(mnTriangulation);
mfMeshPoints = mfMeshPoints(vnUsedPoints, :);

% - Renumber points in the triangulation
for (nNewPointNum = 1:numel(vnUsedPoints))
   mnTriangulation(mnTriangulation == vnUsedPoints(nNewPointNum)) = nNewPointNum;
end

% - Record sizes of mesh
nNumMeshPoints = size(mfMeshPoints, 1);
nNumTriangles = size(mnTriangulation, 1);


% - Collect all edges
mnAllEdges = vertcat([mnTriangulation(:, 1:2)   (1:nNumTriangles)'], ...
                     [mnTriangulation(:, 2:3)   (1:nNumTriangles)'], ...
                     [mnTriangulation(:, [3 1]) (1:nNumTriangles)']);

mnAllEdges(:, 1:2) = sort(mnAllEdges(:, 1:2), 2);
                  
% - Collect the edges list
[mnEdges, nul, vnUniqueEdgesIndices] = unique(mnAllEdges(:, 1:2), 'rows'); %#ok<ASGLU>

% - Record number of edges
nNumEdges = size(mnEdges, 1);

% - Compute the vertex adjacency matrix
fprintf(1, '\r--- LoopSubdivisionLimited: Computing vertex adjacency...                     ');

% - Create adjacency matrix as a sparse logical matrix
mbVertexAdjacency = sparse([mnEdges(:, 1); mnEdges(:, 2)], ...
   [mnEdges(:, 2); mnEdges(:, 1)], true, ...
   nNumMeshPoints, nNumMeshPoints);


% -- Record edge lengths
vfEdgeDists = mfMeshPoints(mnEdges(:, 1), :) - mfMeshPoints(mnEdges(:, 2), :);
vfEdgeDists = vfEdgeDists.^2;
vfEdgeDists = sqrt(sum(vfEdgeDists, 2));

% - Only divide edges that are longer than the minimum resolution
vbDividedEdge = vfEdgeDists > fMinResolution;

% - Should we divide any edges?
if (~any(vbDividedEdge))
   % - No, so just return the existing mesh
   mfRefinedMesh = mfMeshPoints;
   fprintf(1, '\r--- LoopSubdivisionLimited: Done.                                       ');
   return;
end

% - Compute the edge/vertex adjacency matrix for divided edges
fprintf(1, '\r--- LoopSubdivisionLimited: Computing edge adjacency...                     ');

% - Create adjacency matrix as a sparse logical matrix
mbEdgeTriAdjacency = sparse(vnUniqueEdgesIndices, mnAllEdges(:, 3), true, nNumEdges, nNumTriangles);


% - Assign boundary edges, if not supplied as an argument
if (~exist('vbBoundaryEdges', 'var') || isempty(vbBoundaryEdges))
   vbPermittedBoundaryEdges = true(nNumEdges, 1);
else
   vbPermittedBoundaryEdges = vbBoundaryEdges;
end

% - Pre-allocate adjacency lists
vnDividedEdges = find(vbDividedEdge);
nNumDividedEdges = numel(vnDividedEdges);
cellAdjacentVerticesList = cell(nNumDividedEdges, 1);
cellAdjacentTrianglesList = cell(nNumDividedEdges, 1);

% - Calculate boundary vertices
vbBoundaryEdgesCalc = (sum(mbEdgeTriAdjacency, 2) == 1);

parfor (nDivEdgeIndex = 1:nNumDividedEdges)
   nEdgeIndex = vnDividedEdges(nDivEdgeIndex);
   
   % - Find the triangles associated with this edge
   vbAdjacentTris = mbEdgeTriAdjacency(nEdgeIndex, :); %#ok<PFBNS>

   % - Check for a boundary edge
   if (vbBoundaryEdgesCalc(nEdgeIndex) && (vbPermittedBoundaryEdges(nEdgeIndex))) %#ok<PFBNS>
      % - Adjacency is only with those points comprising this edge
      vnAdjacentVertices = mnEdges(nEdgeIndex, :); %#ok<PFBNS>

   else
      % - Find all the points comprising those triangles
      vnAdjacentVertices = mnTriangulation(vbAdjacentTris, :); %#ok<PFBNS>
      vnAdjacentVertices = vnAdjacentVertices(:);
   end

   % - Record adjacency
   cellAdjacentVerticesList{nDivEdgeIndex} = ...
      [repmat(nEdgeIndex, numel(vnAdjacentVertices), 1) vnAdjacentVertices(:)];

   cellAdjacentTrianglesList{nDivEdgeIndex} = ...
      [repmat(nEdgeIndex, nnz(vbAdjacentTris), 1) find(vbAdjacentTris)'];
end

% - Complete the edge/triangle adjacency matrix for divided triangles
vbDividedTris = any(mbEdgeTriAdjacency(vbDividedEdge, :), 1)';
vnDividedTris = find(vbDividedTris);
nNumDividedTris = numel(vnDividedTris);

cellDividedTrisAdjacency = cell(nNumDividedTris, 1);

% - Find adjacency for divided triangles
parfor (nDivTriIndex = 1:nNumDividedTris) %#ok<FORPF>
   nTriIndex = vnDividedTris(nDivTriIndex);
   
   vbAllEdgeIndices = mnAllEdges(:, 3) == nTriIndex; %#ok<PFBNS>
   vnAdjacentEdges = vnUniqueEdgesIndices(vbAllEdgeIndices); %#ok<PFBNS>
   
   cellDividedTrisAdjacency{nDivTriIndex} = [vnAdjacentEdges repmat(nTriIndex, numel(vnAdjacentEdges), 1)];
end


% - Construct adjacency matrices
%   Note: Must use logical(sparse(...)), since repeated indices are not
%   supported for logical arrays
mnAdjacentVertices = vertcat(cellAdjacentVerticesList{:});
mbEdgeVertexAdjacency = logical(sparse(mnAdjacentVertices(:, 1), mnAdjacentVertices(:, 2), 1, nNumEdges, nNumMeshPoints));

mnAdjacentTris = vertcat(cellAdjacentTrianglesList{:}, cellDividedTrisAdjacency{:});
mbEdgeTriAdjacency = logical(sparse(mnAdjacentTris(:, 1), mnAdjacentTris(:, 2), 1, nNumEdges, nNumTriangles));

% - Assign boundary edges
if (~exist('vbBoundaryEdges', 'var') || isempty(vbBoundaryEdges))
   vbBoundaryEdges = vbBoundaryEdgesCalc & vbPermittedBoundaryEdges;
end

% - Compute boundary vertices
vnBoundaryVertices = mnEdges(vbBoundaryEdges, :);
vnBoundaryVertices = vnBoundaryVertices(:);
vbBoundaryVertex = logical(sparse(vnBoundaryVertices, 1, 1, nNumMeshPoints, 1));


% -- Perform a round of Loop subdivision over edges

fprintf(1, '\r--- LoopSubdivisionLimited: Performing edge subdivision...                     ');

% - Pre-allocate new vertex matrix and new edges matrix
%   Each edge splits into two, and each triangle adds three new edges

mfParVertices = nan(nNumDividedEdges, 3);

parfor (nDivEdgeIndex = 1:nNumDividedEdges)
   nEdgeIndex = vnDividedEdges(nDivEdgeIndex);
   
   % - Is this a boundary edge?
   if (vbBoundaryEdges(nEdgeIndex)) %#ok<PFBNS>
      % - Get the adjacent points for this edge
      mfAdjacentPoints = mfMeshPoints(mbEdgeVertexAdjacency(nEdgeIndex, :), :); %#ok<PFBNS,PFBNS>

      % - The new vertex is simply the mean of the two adjacent vertices
      mfParVertices(nDivEdgeIndex, :) = mean(mfAdjacentPoints, 1);
      
   else
      % - The new vertex depends on all points adjacent to this edge
      vnAdjacentPoints = find(mbEdgeVertexAdjacency(nEdgeIndex, :));
      mfAdjacentPoints = mfMeshPoints(vnAdjacentPoints, :); 

      % - Find which points are actually connected to this edge
      vnEdgePoints = mnEdges(nEdgeIndex, :); %#ok<PFBNS>
      
      vfWeights = 1/8 * ones(numel(vnAdjacentPoints), 1);
      vfWeights(ismember(vnAdjacentPoints, vnEdgePoints)) = 3/8;
      
      % - The new vertex is the weighted sum of the adjacent vertices
      mfParVertices(nDivEdgeIndex, :) = sum(mfAdjacentPoints .* (nonzeros(vfWeights) * [1 1 1]), 1);
   end
end

% - Record numbering for the new vertices
vnNewVertexNumbers = nan(nNumEdges, 1);
vnNewVertexNumbers(vnDividedEdges) = 1:nNumDividedEdges;
mfNewVertices = mfParVertices;

% mfNewVertices = nan(nNumEdges, 3);
% mfNewVertices(vnDividedEdges, :) = mfParVertices;



% -- Compute the new triangles caused by splitting the existing edges

fprintf(1, '\r--- LoopSubdivisionLimited: Calculating new triangles...                     ');

% - Pre-allocate new triangles matrix
cellNewTriangles = cell(nNumDividedTris, 1);

parfor (nDivTriIndex = 1:nNumDividedTris)
   nTriIndex = vnDividedTris(nDivTriIndex);
   
   % - Find the edge indices and edge vertices for this triangle
   vbEdgeIndices = mbEdgeTriAdjacency(:, nTriIndex); %#ok<PFBNS>
   vnEdgeIndices = find(vbEdgeIndices);
   mnEdgeVertices = mnEdges(vbEdgeIndices, :); %#ok<PFBNS>
   vnTriVertices = mnTriangulation(nTriIndex, :); %#ok<PFBNS>

   % - Find divided edges
   vbWhichEdgesDiv = vbDividedEdge(vnEdgeIndices); %#ok<PFBNS>
   nNumDividedEdges = nnz(vbWhichEdgesDiv);

   % - Pre-allocate shared vertex array
   vbSharedVertex = false(nNumMeshPoints, 1);
   
   % - The number of new triangles to generate
   switch (nNumDividedEdges)
      case 1
         % - Build new triangles
         %   Two new triangles are composed of two existing vertices and the
         %   single added vertex
         mnNewTriangles = nan(2, 3);
         
         % - Find the ID of the inserted vertex
         % - Find the new vertices created by the divided edges
         nNewVertexIndex = nNumMeshPoints + vnNewVertexNumbers(vnEdgeIndices(vbWhichEdgesDiv))'; %#ok<PFBNS>
         
         % - Find the ID of the vertex of the existing triangle shared by the
         % non-divided edge
         vbSharedVertex(:) = false;
         vbSharedVertex(mnEdgeVertices) = true;
         vbSharedVertex(mnEdgeVertices(vbWhichEdgesDiv, :)) = false;
         nSharedVertexIndex = find(vbSharedVertex);
         mbIsSharedVertex = mnEdgeVertices == nSharedVertexIndex;
         
         % - Make a vector of vertices, where the first element is the shared
         % (non-divided) vertex
         vnVertices = [nSharedVertexIndex unique(mnEdgeVertices(~mbIsSharedVertex))'];

         % - Make two new triangles, consisting of the shared vertex, the new
         % vertex and one of the other two vertices
         mnNewTriangles(1, :) = [nSharedVertexIndex vnVertices(2) nNewVertexIndex];
         mnNewTriangles(2, :) = [nSharedVertexIndex vnVertices(3) nNewVertexIndex];
         
      case 2
         % - Build new triangles
         %   Three new triangles are required
         mnNewTriangles = nan(3, 3);
         
         % - Find the ID of the vertex shared by the two divided edges
         vbSharedVertex(:) = false;
         vbSharedVertex(mnEdgeVertices) = true;
         vbSharedVertex(mnEdgeVertices(~vbWhichEdgesDiv, :)) = false;
         nSharedVertexIndex = find(vbSharedVertex);
         mbIsSharedVertex = mnEdgeVertices == nSharedVertexIndex;

         % - Make a vector of vertices, where the first element is the shared
         % vertex
         vnVertices = [nSharedVertexIndex unique(mnEdgeVertices(~mbIsSharedVertex))'];
         
         % - Find the new vertices created by the divided edges
         vnNewVertexIndices = nNumMeshPoints + vnNewVertexNumbers(vnEdgeIndices(vbWhichEdgesDiv))';

         % - One new triangle is composed of the two new vertices and the shared
         % vertex
         mnNewTriangles(1, :) = [nSharedVertexIndex vnNewVertexIndices];
         
         % - Work out which way to split the other slab into two triangles
         mfNewVertexPoints = mfNewVertices(vnNewVertexNumbers(vnEdgeIndices(vbWhichEdgesDiv)), :); %#ok<PFBNS>
         mfOtherVertexPoints = mfMeshPoints(vnVertices(2:3), :); %#ok<PFBNS>
         vfDists = sum((mfNewVertexPoints([1 2], :) - mfOtherVertexPoints([2 1], :)).^2, 2);
         
         if (vfDists(1) <= vfDists(2))
            % - Split between new vertex 1 and old vertex 3
            mnNewTriangles(2, :) = [vnNewVertexIndices vnVertices(3)];
            mnNewTriangles(3, :) = [vnNewVertexIndices(1) vnVertices(2:3)];
         else
            % - Split between new vertex 2 and old vertex 2
            mnNewTriangles(2, :) = [vnNewVertexIndices vnVertices(2)];
            mnNewTriangles(3, :) = [vnNewVertexIndices(2) vnVertices(2:3)];
         end

      case 3
         % - Build new triangles
         %   Three new triangles are composed of one existing vertex and two added
         %   vertices.  The fourth new triangle is composed of three added vertices
         mnNewTriangles = nan(4, 3);

         % - Add the three corner triangles
         for (nTriVertexIndex = 1:3)
            [vnEdges, nul] = find(mnEdgeVertices == vnTriVertices(nTriVertexIndex)); %#ok,NASGU>

            mnNewTriangles(nTriVertexIndex, :) = ...
               [vnTriVertices(nTriVertexIndex) nNumMeshPoints + vnNewVertexNumbers(vnEdgeIndices(vnEdges))'];
         end

         % - Add the central triangle
         mnNewTriangles(4, :) = nNumMeshPoints + vnNewVertexNumbers(vnEdgeIndices(:));
   end
   
   % - Record the triangles added by this division
   cellNewTriangles{nDivTriIndex} = mnNewTriangles;
end

% - Collect all new triangles
mnNewTriangles = vertcat(cellNewTriangles{:});

% - Determine which existing triangles to discard and which should remain
mnKeepTriangles = mnTriangulation(~vbDividedTris, :);


% -- Perform a round of Loop updates over old vertices

fprintf(1, '\r--- LoopSubdivisionLimited: Updating existing vertex locations...                     ');

% - Pre-allocate updated vertices matrix
mfUpdatedVertices = nan(nNumMeshPoints, 3);

parfor (nVertexIndex = 1:nNumMeshPoints)
   % - Find pre-existing adjacent vertices
   mfAdjacentPoints = mfMeshPoints(mbVertexAdjacency(nVertexIndex, :), :); %#ok<PFBNS>
   nNumAdjacentPoints = size(mfAdjacentPoints, 1);
   
   % - Check for a boundary vertex
   if (vbBoundaryVertex(nVertexIndex))
      % - Find the edges along the boundary from this vertex
      vbThisBoundary = any((mnEdges == nVertexIndex), 2) & vbBoundaryEdges;
      
      % - Find the adjacent vertices along the boundary
      vnAdjacentVertices = mnEdges(vbThisBoundary, :);
      vbAdjacentVertices = logical(sparse(vnAdjacentVertices, 1, 1, nNumMeshPoints, 1));
      mfAdjacentVertices = mfMeshPoints(vbAdjacentVertices, :);
      
      % - The updated vertex location is the weighted average of the adjacent
      % boundary vertices
      vfWeights = 1/8 * vbAdjacentVertices;
      vfWeights(nVertexIndex) = 3/4;
      vfWeights = nonzeros(vfWeights);
      vfWeights = vfWeights ./ sum(vfWeights);
      mfUpdatedVertices(nVertexIndex, :) = sum(repmat(vfWeights, 1, 3) .* mfAdjacentVertices, 1);
      
   else
      % - This is an internal vertex

      % - Beta = 3/(8*n) for n > 3; Beta = 3/16 for n == 3
      if (nNumAdjacentPoints > 3)
         fBeta = 3 / (8 * nNumAdjacentPoints);
      else
         fBeta = 3 / 16;
      end

      % - Perform weighted average of adjacent vertices
      mfUpdatedVertices(nVertexIndex, :) = sum(vertcat(  mfMeshPoints(nVertexIndex, :) * (1-nNumAdjacentPoints*fBeta), ...
                                                         mfAdjacentPoints * fBeta), 1);
   end
end


% -- Update vertex and triangulation matrices

% - Assign new vertices and triangles
mfRefinedMesh = vertcat(mfUpdatedVertices, mfNewVertices);
mnTriangulation = vertcat(mnKeepTriangles, mnNewTriangles);

% -- Done!
fprintf(1, '\r--- LoopSubdivisionLimited: Done.                                         \n');

% --- END of LoopSubdivisionLimited.m ---

Contact us