ply.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { RuntimeContext, Task } from '../../mol-task';
  8. import { ShapeProvider } from '../../mol-model/shape/provider';
  9. import { Color } from '../../mol-util/color';
  10. import { PlyFile, PlyTable, PlyList } from '../../mol-io/reader/ply/schema';
  11. import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
  12. import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
  13. import { Shape } from '../../mol-model/shape';
  14. import { ChunkedArray } from '../../mol-data/util';
  15. import { arrayMax, fillSerial } from '../../mol-util/array';
  16. import { Column } from '../../mol-data/db';
  17. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  18. import { ColorNames } from '../../mol-util/color/names';
  19. import { deepClone } from '../../mol-util/object';
  20. import { stringToWords } from '../../mol-util/string';
  21. // TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html
  22. // TODO support missing face element
  23. function createPlyShapeParams(plyFile?: PlyFile) {
  24. const vertex = plyFile && plyFile.getElement('vertex') as PlyTable;
  25. const material = plyFile && plyFile.getElement('material') as PlyTable;
  26. const defaultValues = { group: '', vRed: '', vGreen: '', vBlue: '', mRed: '', mGreen: '', mBlue: '' };
  27. const groupOptions: [string, string][] = [['', '']];
  28. const colorOptions: [string, string][] = [['', '']];
  29. if (vertex) {
  30. for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) {
  31. const name = vertex.propertyNames[i];
  32. const type = vertex.propertyTypes[i];
  33. if (
  34. type === 'uchar' || type === 'uint8' ||
  35. type === 'ushort' || type === 'uint16' ||
  36. type === 'uint' || type === 'uint32' ||
  37. type === 'int'
  38. ) groupOptions.push([name, name]);
  39. if (type === 'uchar' || type === 'uint8') colorOptions.push([name, name]);
  40. }
  41. // TODO hardcoded as convenience for data provided by MegaMol
  42. if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid';
  43. else if (vertex.propertyNames.includes('material_index')) defaultValues.group = 'material_index';
  44. if (vertex.propertyNames.includes('red')) defaultValues.vRed = 'red';
  45. if (vertex.propertyNames.includes('green')) defaultValues.vGreen = 'green';
  46. if (vertex.propertyNames.includes('blue')) defaultValues.vBlue = 'blue';
  47. }
  48. const materialOptions: [string, string][] = [['', '']];
  49. if (material) {
  50. for (let i = 0, il = material.propertyNames.length; i < il; ++i) {
  51. const name = material.propertyNames[i];
  52. const type = material.propertyTypes[i];
  53. if (type === 'uchar' || type === 'uint8') materialOptions.push([name, name]);
  54. }
  55. if (material.propertyNames.includes('red')) defaultValues.mRed = 'red';
  56. if (material.propertyNames.includes('green')) defaultValues.mGreen = 'green';
  57. if (material.propertyNames.includes('blue')) defaultValues.mBlue = 'blue';
  58. }
  59. const defaultColoring = defaultValues.vRed && defaultValues.vGreen && defaultValues.vBlue ? 'vertex' :
  60. defaultValues.mRed && defaultValues.mGreen && defaultValues.mBlue ? 'material' : 'uniform';
  61. return {
  62. ...Mesh.Params,
  63. coloring: PD.MappedStatic(defaultColoring, {
  64. vertex: PD.Group({
  65. red: PD.Select(defaultValues.vRed, colorOptions, { label: 'Red Property' }),
  66. green: PD.Select(defaultValues.vGreen, colorOptions, { label: 'Green Property' }),
  67. blue: PD.Select(defaultValues.vBlue, colorOptions, { label: 'Blue Property' }),
  68. }, { isFlat: true }),
  69. material: PD.Group({
  70. red: PD.Select(defaultValues.mRed, materialOptions, { label: 'Red Property' }),
  71. green: PD.Select(defaultValues.mGreen, materialOptions, { label: 'Green Property' }),
  72. blue: PD.Select(defaultValues.mBlue, materialOptions, { label: 'Blue Property' }),
  73. }, { isFlat: true }),
  74. uniform: PD.Group({
  75. color: PD.Color(ColorNames.grey)
  76. }, { isFlat: true })
  77. }),
  78. grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', {
  79. vertex: PD.Group({
  80. group: PD.Select(defaultValues.group, groupOptions, { label: 'Group Property' }),
  81. }, { isFlat: true }),
  82. none: PD.Group({ })
  83. }),
  84. };
  85. }
  86. export const PlyShapeParams = createPlyShapeParams();
  87. export type PlyShapeParams = typeof PlyShapeParams
  88. function addVerticesRange(begI: number, endI: number, state: MeshBuilder.State, vertex: PlyTable, groupIds: ArrayLike<number>) {
  89. const { vertices, normals, groups } = state;
  90. const x = vertex.getProperty('x');
  91. const y = vertex.getProperty('y');
  92. const z = vertex.getProperty('z');
  93. if (!x || !y || !z) throw new Error('missing coordinate properties');
  94. const nx = vertex.getProperty('nx');
  95. const ny = vertex.getProperty('ny');
  96. const nz = vertex.getProperty('nz');
  97. const hasNormals = !!nx && !!ny && !!nz;
  98. for (let i = begI; i < endI; ++i) {
  99. ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i));
  100. if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i));
  101. ChunkedArray.add(groups, groupIds[i]);
  102. }
  103. }
  104. function addFacesRange(begI: number, endI: number, state: MeshBuilder.State, face: PlyList) {
  105. const { indices } = state;
  106. for (let i = begI; i < endI; ++i) {
  107. const { entries, count } = face.value(i);
  108. if (count === 3) {
  109. // triangle
  110. ChunkedArray.add3(indices, entries[0], entries[1], entries[2]);
  111. } else if (count === 4) {
  112. // quadrilateral
  113. ChunkedArray.add3(indices, entries[2], entries[1], entries[0]);
  114. ChunkedArray.add3(indices, entries[2], entries[0], entries[3]);
  115. }
  116. }
  117. }
  118. async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) {
  119. const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh);
  120. const x = vertex.getProperty('x');
  121. const y = vertex.getProperty('y');
  122. const z = vertex.getProperty('z');
  123. if (!x || !y || !z) throw new Error('missing coordinate properties');
  124. const nx = vertex.getProperty('nx');
  125. const ny = vertex.getProperty('ny');
  126. const nz = vertex.getProperty('nz');
  127. const hasNormals = !!nx && !!ny && !!nz;
  128. const updateChunk = 100000;
  129. for (let i = 0, il = vertex.rowCount; i < il; i += updateChunk) {
  130. addVerticesRange(i, Math.min(i + updateChunk, il), builderState, vertex, groupIds);
  131. if (ctx.shouldUpdate) {
  132. await ctx.update({ message: 'adding ply mesh vertices', current: i, max: il });
  133. }
  134. }
  135. for (let i = 0, il = face.rowCount; i < il; i += updateChunk) {
  136. addFacesRange(i, Math.min(i + updateChunk, il), builderState, face);
  137. if (ctx.shouldUpdate) {
  138. await ctx.update({ message: 'adding ply mesh faces', current: i, max: il });
  139. }
  140. }
  141. const m = MeshBuilder.getMesh(builderState);
  142. if (!hasNormals) Mesh.computeNormals(m);
  143. return m;
  144. }
  145. const int = Column.Schema.int;
  146. type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number>, label: string }
  147. function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping {
  148. const { grouping } = props;
  149. const { rowCount } = vertex;
  150. const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined;
  151. const label = grouping.name === 'vertex' ? stringToWords(grouping.params.group) : 'Vertex';
  152. const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount));
  153. const maxId = column ? arrayMax(ids) : rowCount - 1; // assumes uint ids
  154. const map = new Uint32Array(maxId + 1);
  155. for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i;
  156. return { ids, map, label };
  157. }
  158. type Coloring = { kind: 'vertex' | 'material' | 'uniform', red: Column<number>, green: Column<number>, blue: Column<number> }
  159. function getColoring(vertex: PlyTable, material: PlyTable | undefined, props: PD.Values<PlyShapeParams>): Coloring {
  160. const { coloring } = props;
  161. const { rowCount } = vertex;
  162. let red: Column<number>, green: Column<number>, blue: Column<number>;
  163. if (coloring.name === 'vertex') {
  164. red = vertex.getProperty(coloring.params.red) || Column.ofConst(127, rowCount, int);
  165. green = vertex.getProperty(coloring.params.green) || Column.ofConst(127, rowCount, int);
  166. blue = vertex.getProperty(coloring.params.blue) || Column.ofConst(127, rowCount, int);
  167. } else if (coloring.name === 'material') {
  168. red = (material && material.getProperty(coloring.params.red)) || Column.ofConst(127, rowCount, int);
  169. green = (material && material.getProperty(coloring.params.green)) || Column.ofConst(127, rowCount, int);
  170. blue = (material && material.getProperty(coloring.params.blue)) || Column.ofConst(127, rowCount, int);
  171. } else {
  172. const [r, g, b] = Color.toRgb(coloring.params.color);
  173. red = Column.ofConst(r, rowCount, int);
  174. green = Column.ofConst(g, rowCount, int);
  175. blue = Column.ofConst(b, rowCount, int);
  176. }
  177. return { kind: coloring.name, red, green, blue };
  178. }
  179. function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) {
  180. const { kind, red, green, blue } = coloring;
  181. const { ids, map, label } = grouping;
  182. return Shape.create(
  183. 'ply-mesh', plyFile, mesh,
  184. (groupId: number) => {
  185. const idx = kind === 'material' ? groupId : map[groupId];
  186. return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx));
  187. },
  188. () => 1, // size: constant
  189. (groupId: number) => {
  190. return `${label} ${ids[groupId]}`;
  191. }
  192. );
  193. }
  194. function makeShapeGetter() {
  195. let _plyFile: PlyFile | undefined;
  196. let _props: PD.Values<PlyShapeParams> | undefined;
  197. let _shape: Shape<Mesh>;
  198. let _mesh: Mesh;
  199. let _coloring: Coloring;
  200. let _grouping: Grouping;
  201. const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => {
  202. const vertex = plyFile.getElement('vertex') as PlyTable;
  203. if (!vertex) throw new Error('missing vertex element');
  204. const face = plyFile.getElement('face') as PlyList;
  205. if (!face) throw new Error('missing face element');
  206. const material = plyFile.getElement('material') as PlyTable;
  207. let newMesh = false;
  208. let newColor = false;
  209. if (!_plyFile || _plyFile !== plyFile) {
  210. newMesh = true;
  211. }
  212. if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) {
  213. newMesh = true;
  214. }
  215. if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) {
  216. newColor = true;
  217. }
  218. if (newMesh) {
  219. _coloring = getColoring(vertex, material, props);
  220. _grouping = getGrouping(vertex, props);
  221. _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry);
  222. _shape = createShape(plyFile, _mesh, _coloring, _grouping);
  223. } else if (newColor) {
  224. _coloring = getColoring(vertex, material, props);
  225. _shape = createShape(plyFile, _mesh, _coloring, _grouping);
  226. }
  227. _plyFile = plyFile;
  228. _props = deepClone(props);
  229. return _shape;
  230. };
  231. return getShape;
  232. }
  233. export function shapeFromPly(source: PlyFile, params?: {}) {
  234. return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => {
  235. return {
  236. label: 'Mesh',
  237. data: source,
  238. params: createPlyShapeParams(source),
  239. getShape: makeShapeGetter(),
  240. geometryUtils: Mesh.Utils
  241. };
  242. });
  243. }