Vuo  2.3.2
VuoMeshUtility.cc
Go to the documentation of this file.
1 
10 #include "VuoMeshUtility.h"
11 #include "module.h"
12 #include <vector>
13 
14 extern "C"
15 {
16 #ifdef VUO_COMPILER
18  "title" : "VuoMeshUtility",
19  "dependencies" : [
20  "VuoList_VuoPoint4d",
21  "VuoList_VuoInteger"
22  ]
23  });
24 #endif
25 }
26 
28 #define PI 3.14159265359f
29 
35 {
37  {
38  VUserLog("VuoMeshUtility_calculateNormals() requires element assembly method VuoMesh_IndividualTriangles.");
39  return;
40  }
41 
42  unsigned int vertexCount, elementCount, *elements;
43  float *vertices, *textureCoordinates, *colors;
44  VuoMesh_getCPUBuffers(mesh, &vertexCount, &vertices, nullptr, &textureCoordinates, &colors, &elementCount, &elements);
45  if (!vertexCount)
46  return;
47 
48  float *normals = (float *)malloc(sizeof(float) * 3 * vertexCount);
49  unsigned int* normalsAverage = (unsigned int*)malloc(sizeof(unsigned int) * vertexCount);
50 
51  // initialize normals to {0,0,0,0}
52  for(unsigned int i = 0; i < vertexCount; i++)
53  {
54  normals[i * 3 ] = 0;
55  normals[i * 3 + 1] = 0;
56  normals[i * 3 + 2] = 0;
57  normalsAverage[i] = 0;
58  }
59 
60  // sum up all normals
61  if (elementCount)
62  for (unsigned int i = 0; i < elementCount; i += 3)
63  {
64  unsigned int i0 = elements[i + 0];
65  unsigned int i1 = elements[i + 1];
66  unsigned int i2 = elements[i + 2];
67 
68  VuoPoint3d a = VuoPoint3d_makeFromArray(&vertices[i0 * 3]);
69  VuoPoint3d b = VuoPoint3d_makeFromArray(&vertices[i1 * 3]);
70  VuoPoint3d c = VuoPoint3d_makeFromArray(&vertices[i2 * 3]);
71 
72  VuoPoint3d cross = VuoPoint3d_normalize(VuoPoint3d_crossProduct(b - a, c - a));
73 
74  VuoPoint3d n = VuoPoint3d_makeFromArray(&normals[i0 * 3]) + cross;
75  VuoPoint3d_setArray(&normals[i0 * 3], n);
76 
77  n = VuoPoint3d_makeFromArray(&normals[i1 * 3]) + cross;
78  VuoPoint3d_setArray(&normals[i1 * 3], n);
79 
80  n = VuoPoint3d_makeFromArray(&normals[i2 * 3]) + cross;
81  VuoPoint3d_setArray(&normals[i2 * 3], n);
82 
83  normalsAverage[i0]++;
84  normalsAverage[i1]++;
85  normalsAverage[i2]++;
86  }
87  else
88  for (unsigned int i = 0; i < vertexCount; i += 3)
89  {
90  unsigned int i0 = i + 0;
91  unsigned int i1 = i + 1;
92  unsigned int i2 = i + 2;
93 
94  VuoPoint3d a = VuoPoint3d_makeFromArray(&vertices[i0 * 3]);
95  VuoPoint3d b = VuoPoint3d_makeFromArray(&vertices[i1 * 3]);
96  VuoPoint3d c = VuoPoint3d_makeFromArray(&vertices[i2 * 3]);
97 
98  VuoPoint3d cross = VuoPoint3d_normalize(VuoPoint3d_crossProduct(b - a, c - a));
99 
100  VuoPoint3d n = VuoPoint3d_makeFromArray(&normals[i0 * 3]) + cross;
101  VuoPoint3d_setArray(&normals[i0 * 3], n);
102 
103  n = VuoPoint3d_makeFromArray(&normals[i1 * 3]) + cross;
104  VuoPoint3d_setArray(&normals[i1 * 3], n);
105 
106  n = VuoPoint3d_makeFromArray(&normals[i2 * 3]) + cross;
107  VuoPoint3d_setArray(&normals[i2 * 3], n);
108 
109  normalsAverage[i0]++;
110  normalsAverage[i1]++;
111  normalsAverage[i2]++;
112  }
113 
114  // now go through and average
115  for (unsigned int i = 0; i < vertexCount; i++)
116  {
117  normals[i * 3 ] /= normalsAverage[i];
118  normals[i * 3 + 1] /= normalsAverage[i];
119  normals[i * 3 + 2] /= normalsAverage[i];
120  }
121 
122  free(normalsAverage);
123 
124  VuoMesh_setCPUBuffers(mesh, vertexCount, vertices, normals, textureCoordinates, colors, elementCount, elements);
125 }
126 
130 bool VuoMeshUtility_bounds(const VuoMesh mesh, VuoPoint3d *min, VuoPoint3d *max)
131 {
132  unsigned int vertexCount;
133  float *positions;
134  VuoMesh_getCPUBuffers(mesh, &vertexCount, &positions, nullptr, nullptr, nullptr, nullptr, nullptr);
135  if (!vertexCount)
136  return false;
137 
138  // Calculate the center of the mesh.
139  *min = *max = VuoPoint3d_makeFromArray(&positions[0]);
140 
141  for (unsigned int i = 1; i < vertexCount; i++)
142  {
143  *min = VuoPoint3d_min(*min, VuoPoint3d_makeFromArray(&positions[i * 3]));
144  *max = VuoPoint3d_max(*max, VuoPoint3d_makeFromArray(&positions[i * 3]));
145  }
146 
147  return true;
148 }
149 
155 {
158 
159  unsigned int vertexCount, elementCount, *elements;
160  float *vertices, *normals, *colors;
161  VuoMesh_getCPUBuffers(mesh, &vertexCount, &vertices, &normals, nullptr, &colors, &elementCount, &elements);
162  if (!vertexCount)
163  return;
164 
165  unsigned int dup_vertex_start = vertexCount;
166 
167  float *textureCoordinates = (float *)malloc(sizeof(float) * 2 * vertexCount);
168 
169  VuoPoint3d center;
170  {
171  VuoPoint3d min, max;
172  VuoMeshUtility_bounds(mesh, &min, &max);
173  center = (min + max) / 2.f;
174  }
175 
176  VuoPoint2d min, max;
177  for (unsigned int i = 0; i < vertexCount; i++)
178  {
179  VuoPoint3d v = VuoPoint3d_normalize(vertices[i] - center);
180 
181  VuoPoint2d tc = (VuoPoint2d){
182  1.f - (.5f + (atan2f(v.z, v.x) / (2.f * PI))),
183  1.f - (.5f - (asinf(v.y) / PI))};
184 
185  if(i >= dup_vertex_start)
186  tc.x -= 1.;
187 
188  if(i == 0)
189  {
190  min = tc;
191  max = tc;
192  }
193 
194  min.x = fmin(min.x, tc.x);
195  min.y = fmin(min.y, tc.y);
196 
197  max.x = fmax(max.x, tc.x);
198  max.y = fmax(max.y, tc.y);
199 
200  textureCoordinates[i * 2 ] = tc.x;
201  textureCoordinates[i * 2 + 1] = tc.y;
202  }
203 
204  VuoPoint2d scale = { 1 / (max.x - min.x), 1 / (max.y - min.y) };
205 
206  for (unsigned int i = 0; i < vertexCount; i++)
207  {
208  textureCoordinates[i * 2 ] -= min.x;
209  textureCoordinates[i * 2 + 1] -= min.y;
210  textureCoordinates[i * 2 ] *= scale.x;
211  textureCoordinates[i * 2 + 1] *= scale.y;
212  }
213 
214  VuoMesh_setCPUBuffers(mesh, vertexCount, vertices, normals, textureCoordinates, colors, elementCount, elements);
215 
216  // remove vertices *after* generating textures because
217  // textures uses the overflowing vertices to determine
218  // which coordinates need to be wrapped.
219  if (VuoMesh_getElementAssemblyMethod(mesh) == VuoMesh_IndividualTriangles && dup_vertex_start < vertexCount)
221 }
222 
226 enum Plane {
227  PlaneX,
228  PlaneY,
229  PlaneZ,
230  PlaneNegX,
231  PlaneNegY,
232  PlaneNegZ
233 };
234 
240 {
241  float x = fabs(normal.x),
242  y = fabs(normal.y),
243  z = fabs(normal.z);
244 
245  if( x > y && x > z ) {
246  return normal.x < 0 ? PlaneNegX : PlaneX;
247  } else if( y > x && y > z ) {
248  return normal.y < 0 ? PlaneNegY : PlaneY;
249  } else {
250  return normal.z < 0 ? PlaneNegZ : PlaneZ;
251  }
252 }
253 
257 static inline VuoPoint3d VuoMeshUtility_averageNormal(VuoPoint3d a, VuoPoint3d b, VuoPoint3d c)
258 {
259  return (VuoPoint3d){(a.x + b.x + c.x) / 3.f,
260  (a.y + b.y + c.y) / 3.f,
261  (a.z + b.z + c.z) / 3.f};
262 }
263 
268 {
269  VuoPoint3d min, max;
270  VuoMeshUtility_bounds(mesh, &min, &max);
271  VuoPoint3d range = max - min;
272 
273  unsigned int vertexCount, elementCount, *elements;
274  float *positions, *normals, *colors;
275  VuoMesh_getCPUBuffers(mesh, &vertexCount, &positions, &normals, nullptr, &colors, &elementCount, &elements);
276  if (!vertexCount || !normals)
277  return;
278 
279  float *textureCoordinates = (float *)malloc(sizeof(float) * 2 * vertexCount);
280 
281  for (unsigned int i = 0; i < vertexCount; i++)
282  {
283  VuoPoint3d p = VuoPoint3d_makeFromArray(&positions[i * 3]);
284  VuoPoint3d n = VuoPoint3d_makeFromArray(&normals[i * 3]);
285  VuoPoint3d uv = p - min;
286 
287  uv.x = uv.x / range.x;
288  uv.y = uv.y / range.y;
289  uv.z = uv.z / range.z;
290 
292  {
293  case PlaneX:
294  case PlaneNegX:
295  textureCoordinates[i * 2 ] = uv.z;
296  textureCoordinates[i * 2 + 1] = uv.y;
297  break;
298 
299  case PlaneY:
300  case PlaneNegY:
301  textureCoordinates[i * 2 ] = uv.x;
302  textureCoordinates[i * 2 + 1] = uv.z;
303  break;
304 
305  case PlaneZ:
306  case PlaneNegZ:
307  textureCoordinates[i * 2 ] = uv.x;
308  textureCoordinates[i * 2 + 1] = uv.y;
309  break;
310  }
311  }
312 
313  VuoMesh_setCPUBuffers(mesh, vertexCount, positions, normals, textureCoordinates, colors, elementCount, elements);
314 }
315 
320 {
321  VuoPoint3d min, max;
322  VuoMeshUtility_bounds(mesh, &min, &max);
323  VuoPoint3d range = max - min;
324 
325  unsigned int vertexCount, elementCount, *elements;
326  float *positions, *normals, *colors;
327  VuoMesh_getCPUBuffers(mesh, &vertexCount, &positions, &normals, nullptr, &colors, &elementCount, &elements);
328  if (!vertexCount || !normals || !elementCount)
329  return;
330 
331  float *textureCoordinates = (float *)malloc(sizeof(float) * 2 * vertexCount);
332 
333  for (unsigned int i = 0; i < elementCount; i+=3)
334  {
335  unsigned int a = elements[i+0],
336  b = elements[i+1],
337  c = elements[i+2];
338 
339  VuoPoint3d pa = VuoPoint3d_makeFromArray(&positions[a * 3]);
340  VuoPoint3d pb = VuoPoint3d_makeFromArray(&positions[b * 3]);
341  VuoPoint3d pc = VuoPoint3d_makeFromArray(&positions[c * 3]);
342 
343  VuoPoint3d uv_a = pa - min;
344  VuoPoint3d uv_b = pb - min;
345  VuoPoint3d uv_c = pc - min;
346 
347  uv_a.x = uv_a.x / range.x;
348  uv_b.x = uv_b.x / range.x;
349  uv_c.x = uv_c.x / range.x;
350  uv_a.y = uv_a.y / range.y;
351  uv_b.y = uv_b.y / range.y;
352  uv_c.y = uv_c.y / range.y;
353  uv_a.z = uv_a.z / range.z;
354  uv_b.z = uv_b.z / range.z;
355  uv_c.z = uv_c.z / range.z;
356 
357  switch (VuoMeshUtility_calculateBestPlane( VuoMeshUtility_averageNormal(normals[a], normals[b], normals[c])))
358  {
359  case PlaneX:
360  case PlaneNegX:
361  textureCoordinates[a * 2 ] = uv_a.z;
362  textureCoordinates[a * 2 + 1] = uv_a.y;
363  textureCoordinates[b * 2 ] = uv_b.z;
364  textureCoordinates[b * 2 + 1] = uv_b.y;
365  textureCoordinates[c * 2 ] = uv_c.z;
366  textureCoordinates[c * 2 + 1] = uv_c.y;
367  break;
368 
369  case PlaneY:
370  case PlaneNegY:
371  textureCoordinates[a * 2 ] = uv_a.x;
372  textureCoordinates[a * 2 + 1] = uv_a.z;
373  textureCoordinates[b * 2 ] = uv_b.x;
374  textureCoordinates[b * 2 + 1] = uv_b.z;
375  textureCoordinates[c * 2 ] = uv_c.x;
376  textureCoordinates[c * 2 + 1] = uv_c.z;
377  break;
378 
379  case PlaneZ:
380  case PlaneNegZ:
381  textureCoordinates[a * 2 ] = uv_a.x;
382  textureCoordinates[a * 2 + 1] = uv_a.y;
383  textureCoordinates[b * 2 ] = uv_b.x;
384  textureCoordinates[b * 2 + 1] = uv_b.y;
385  textureCoordinates[c * 2 ] = uv_c.x;
386  textureCoordinates[c * 2 + 1] = uv_c.y;
387  break;
388  }
389  }
390 
391  VuoMesh_setCPUBuffers(mesh, vertexCount, positions, normals, textureCoordinates, colors, elementCount, elements);
392 }
393 
398 {
399  unsigned int vertexCount, elementCount, *indices;
400  float *positions, *normals, *textures;
401  VuoMesh_getCPUBuffers(mesh, &vertexCount, &positions, &normals, &textures, nullptr, &elementCount, &indices);
402  if (!vertexCount || !elementCount)
403  return;
404 
405  unsigned int *seam_indices = (unsigned int *)malloc(sizeof(unsigned int) * elementCount);
406  memcpy(seam_indices, indices, sizeof(unsigned int) * elementCount);
407 
408  std::vector<float> seam_positions = std::vector<float>(positions != NULL ? vertexCount * 3 : 0);
409  if(positions) std::copy(positions, positions + vertexCount * 3, seam_positions.begin());
410  std::vector<float> seam_normals = std::vector<float>(normals != NULL ? vertexCount * 3 : 0);
411  if(normals) std::copy(normals, normals + vertexCount * 3, seam_normals.begin());
412  std::vector<float> seam_textures = std::vector<float>(textures != NULL ? vertexCount * 2 : 0);
413  if(textures) std::copy(textures, textures + vertexCount * 2, seam_textures.begin());
414 
415  const VuoPoint3d left = (VuoPoint3d){-1., 0., 0.};
416  const VuoPoint3d forward = (VuoPoint3d){ 0., 0., 1.};
417  const VuoPoint3d up = (VuoPoint3d){ 0., 1., 0.};
418  const VuoPoint3d down = (VuoPoint3d){ 0., -1., 0.};
419 
420  // get mesh bounds and center point
421  VuoPoint3d min, max;
422  VuoMeshUtility_bounds(mesh, &min, &max);
423  VuoPoint3d center = (max + min) / 2.f;
424 
425  // Insert vertical seam along western hemisphere
426  for (unsigned int i = 0; i < elementCount; i+=3)
427  {
428  if(indices[i] > vertexCount || indices[i+1] > vertexCount || indices[i+2] > vertexCount)
429  {
430  VUserLog("Triangle indices are referencing out of bounds vertices. (%u, %u, %u) Vertex Count: %u", indices[i], indices[i+1], indices[i+2], vertexCount);
431  continue;
432  }
433  // {0,1} {1,2}, {2,0}
434  for(int n = 0; n < 3; n++)
435  {
436  unsigned int x = i + n;
437  unsigned int y = i + (n == 2 ? 0 : n+1);
438 
439  // check if this triangle edge crosses the new seam
440  VuoPoint3d px = VuoPoint3d_makeFromArray(&positions[indices[x] * 3]);
441  VuoPoint3d py = VuoPoint3d_makeFromArray(&positions[indices[y] * 3]);
442  VuoPoint3d v0 = VuoPoint3d_normalize(px - center);
443  VuoPoint3d v1 = VuoPoint3d_normalize(py - center);
444 
445  if (VuoPoint3d_areEqual(v0, up) || VuoPoint3d_areEqual(v0, down)
446  || VuoPoint3d_areEqual(v1, up) || VuoPoint3d_areEqual(v1, down))
447  continue;
448 
449  // if v0 is in left back quadrant, and v1 is in the forward half, v0 needs to be duplicated. Ditto in opposite order
450  if (VuoPoint3d_dotProduct(v0, left) > 0
451  && VuoPoint3d_dotProduct(v0, forward) < 0
452  && VuoPoint3d_dotProduct(v1, forward) >= 0)
453  {
454  if (positions)
455  {
456  seam_positions.push_back(positions[indices[x] * 3 ]);
457  seam_positions.push_back(positions[indices[x] * 3 + 1]);
458  seam_positions.push_back(positions[indices[x] * 3 + 2]);
459  }
460  if (normals)
461  {
462  seam_normals.push_back(normals[indices[x] * 3 ]);
463  seam_normals.push_back(normals[indices[x] * 3 + 1]);
464  seam_normals.push_back(normals[indices[x] * 3 + 2]);
465  }
466  if (textures)
467  {
468  seam_textures.push_back(textures[indices[x] * 2 ]);
469  seam_textures.push_back(textures[indices[x] * 2 + 1]);
470  }
471 
472  seam_indices[x] = seam_positions.size() / 3 - 1;
473  }
474 
475  // if v1 is in left back quadrant, but v0 isn't, duplicate v1
476  if (VuoPoint3d_dotProduct(v1, left) > 0
477  && VuoPoint3d_dotProduct(v1, forward) < 0
478  && VuoPoint3d_dotProduct(v0, forward) >= 0)
479  {
480  if(positions)
481  {
482  seam_positions.push_back(positions[indices[y] * 3 ]);
483  seam_positions.push_back(positions[indices[y] * 3 + 1]);
484  seam_positions.push_back(positions[indices[y] * 3 + 2]);
485  }
486  if(normals)
487  {
488  seam_normals.push_back(normals[indices[y] * 3 ]);
489  seam_normals.push_back(normals[indices[y] * 3 + 1]);
490  seam_normals.push_back(normals[indices[y] * 3 + 2]);
491  }
492  if(textures)
493  {
494  seam_textures.push_back(textures[indices[y] * 2 ]);
495  seam_textures.push_back(textures[indices[y] * 2 + 1]);
496  }
497 
498  seam_indices[y] = seam_positions.size() / 3 - 1;
499  }
500  }
501  }
502 
503  float *newPositions = (float *)malloc(sizeof(float) * seam_positions.size());
504  std::copy(seam_positions.begin(), seam_positions.end(), newPositions);
505 
506  float *newNormals = nullptr;
507  if (normals)
508  {
509  newNormals = (float *)malloc(sizeof(float) * seam_normals.size());
510  std::copy(seam_normals.begin(), seam_normals.end(), newNormals);
511  }
512 
513  float *newTextureCoordinates = nullptr;
514  if (textures)
515  {
516  newTextureCoordinates = (float *)malloc(sizeof(float) * seam_textures.size());
517  std::copy(seam_textures.begin(), seam_textures.end(), newTextureCoordinates);
518  }
519 
520  VuoMesh_setCPUBuffers(mesh, seam_positions.size() / 3,
521  newPositions, newNormals, newTextureCoordinates, nullptr,
522  elementCount, seam_indices);
523 }
524 
528 static int compare(const void* lhs, const void* rhs)
529 {
530  return ( *(int*)lhs - *(int*)rhs );
531 }
532 
537 {
538  unsigned int vertexCount, elementCount, *elements;
539  float *positions, *normals, *textureCoordinates;
540  VuoMesh_getCPUBuffers(mesh, &vertexCount, &positions, &normals, &textureCoordinates, nullptr, &elementCount, &elements);
541  if (!vertexCount || !elementCount)
542  return;
543 
544  unsigned int* sortedTriangles = (unsigned int*)malloc(sizeof(unsigned int) * elementCount);
545  memcpy(sortedTriangles, elements, sizeof(unsigned int) * elementCount);
546 
547  // sort indices from smallest to largest
548  qsort(sortedTriangles, elementCount, sizeof(unsigned int), compare);
549 
550  // lop off any elements that are referencing out of bounds vertices
551  unsigned int last = elementCount-1;
552 
553  while( sortedTriangles[last] > vertexCount )
554  last--;
555 
556  // if there are invalid indices, reallocate the the elements array with only the good'ns.
557  if(last < elementCount-1)
558  {
559  unsigned int* pruned_elements = (unsigned int*)malloc(sizeof(unsigned int) * (last+1));
560 
561  unsigned int n = 0;
562 
563  for(unsigned int i = 0; i < elementCount; i++)
564  {
565  if (elements[i] < vertexCount)
566  pruned_elements[n++] = elements[i];
567  }
568 
569  elementCount = last+1;
570  elements = pruned_elements;
571  }
572 
573  // generate list of unused indices
574  std::vector<unsigned int> unused;
575 
576  int lastVal = -1;
577  for(int i = 0; i < elementCount && sortedTriangles[i] < elementCount; i++)
578  {
579  for(int n = lastVal+1; n < sortedTriangles[i]; n++)
580  {
581  unused.push_back(n);
582  }
583 
584  lastVal = sortedTriangles[i];
585  }
586  free(sortedTriangles);
587 
588  // rebuild index array by substracting the amount of vertices removed in the range lower
589  // than element[i]
590  int unusedCount = unused.size();
591  for(int i = 0; i < elementCount; i++)
592  {
593  unsigned int vertex = elements[i];
594 
595  if(vertex > vertexCount)
596  continue;
597 
598  int n = 0;
599 
600  for(auto& val : unused)
601  {
602  if(val < vertex)
603  n++;
604  else
605  break;
606  }
607 
608  elements[i] -= n;
609  }
610 
611  // rebuild vertex arrays without the unused values
612  // not using vertexCount-unusedCount because unused may contain duplicates
613  std::vector<float> new_positions = std::vector<float>(vertexCount * 3);
614  std::vector<float> new_normals = std::vector<float>(normals ? vertexCount * 3 : 0);
615  std::vector<float> new_textureCoordinates = std::vector<float>(textureCoordinates ? vertexCount * 2 : 0);
616 
617  unsigned int curIndex = 0, n = 0;
618  for(int i = 0; i < vertexCount; i++)
619  {
620  if(curIndex >= unusedCount || i < unused[curIndex])
621  {
622  new_positions[n * 3 ] = positions[i * 3 ];
623  new_positions[n * 3 + 1] = positions[i * 3 + 1];
624  new_positions[n * 3 + 2] = positions[i * 3 + 2];
625  if (normals)
626  {
627  new_normals[n * 3 ] = normals[i * 3 ];
628  new_normals[n * 3 + 1] = normals[i * 3 + 1];
629  new_normals[n * 3 + 2] = normals[i * 3 + 2];
630  }
631  if (textureCoordinates)
632  {
633  new_textureCoordinates[n * 2 ] = textureCoordinates[i * 2 ];
634  new_textureCoordinates[n * 2 + 1] = textureCoordinates[i * 2 + 1];
635  }
636 
637  n++;
638  }
639  else
640  {
641  do {
642  curIndex++;
643  } while(curIndex < unusedCount && unused[curIndex] <= i);
644  }
645  }
646 
647  // Re-apply points to mesh
648  unsigned int culledVertexCount = new_positions.size();
649 
650  float *newPositions = (float *)malloc(sizeof(float) * 3 * culledVertexCount);
651  std::copy(new_positions.begin(), new_positions.end(), newPositions);
652 
653  float *newNormals = nullptr;
654  if (normals)
655  {
656  newNormals = (float *)malloc(sizeof(float) * 3 * culledVertexCount);
657  std::copy(new_normals.begin(), new_normals.end(), newNormals);
658  }
659 
660  float *newTextureCoordinates = nullptr;
661  if (textureCoordinates)
662  {
663  newTextureCoordinates = (float *)malloc(sizeof(float) * 2 * culledVertexCount);
664  std::copy(new_textureCoordinates.begin(), new_textureCoordinates.end(), newTextureCoordinates);
665  }
666 
667  VuoMesh_setCPUBuffers(mesh, culledVertexCount,
668  newPositions, newNormals, newTextureCoordinates, nullptr,
669  elementCount, elements);
670 }