OBJLoader.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. import {
  2. BufferGeometry,
  3. FileLoader,
  4. Float32BufferAttribute,
  5. Group,
  6. LineBasicMaterial,
  7. LineSegments,
  8. Loader,
  9. Material,
  10. Mesh,
  11. MeshPhongMaterial,
  12. Points,
  13. PointsMaterial,
  14. Vector3
  15. } from '../lib/three.module.js'
  16. // o object_name | g group_name
  17. const _object_pattern = /^[og]\s*(.+)?/
  18. // mtllib file_reference
  19. const _material_library_pattern = /^mtllib /
  20. // usemtl material_name
  21. const _material_use_pattern = /^usemtl /
  22. // usemap map_name
  23. const _map_use_pattern = /^usemap /
  24. const _vA = new Vector3()
  25. const _vB = new Vector3()
  26. const _vC = new Vector3()
  27. const _ab = new Vector3()
  28. const _cb = new Vector3()
  29. function ParserState() {
  30. const state = {
  31. objects: [],
  32. object: {},
  33. vertices: [],
  34. normals: [],
  35. colors: [],
  36. uvs: [],
  37. materials: {},
  38. materialLibraries: [],
  39. startObject: function(name, fromDeclaration) {
  40. // If the current object (initial from reset) is not from a g/o declaration in the parsed
  41. // file. We need to use it for the first parsed g/o to keep things in sync.
  42. if (this.object && this.object.fromDeclaration === false) {
  43. this.object.name = name
  44. this.object.fromDeclaration = fromDeclaration !== false
  45. return
  46. }
  47. const previousMaterial = this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined
  48. if (this.object && typeof this.object._finalize === 'function') {
  49. this.object._finalize(true)
  50. }
  51. this.object = {
  52. name: name || '',
  53. fromDeclaration: fromDeclaration !== false,
  54. geometry: {
  55. vertices: [],
  56. normals: [],
  57. colors: [],
  58. uvs: [],
  59. hasUVIndices: false
  60. },
  61. materials: [],
  62. smooth: true,
  63. startMaterial: function(name, libraries) {
  64. const previous = this._finalize(false)
  65. // New usemtl declaration overwrites an inherited material, except if faces were declared
  66. // after the material, then it must be preserved for proper MultiMaterial continuation.
  67. if (previous && (previous.inherited || previous.groupCount <= 0)) {
  68. this.materials.splice(previous.index, 1)
  69. }
  70. const material = {
  71. index: this.materials.length,
  72. name: name || '',
  73. mtllib: Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : '',
  74. smooth: previous !== undefined ? previous.smooth : this.smooth,
  75. groupStart: previous !== undefined ? previous.groupEnd : 0,
  76. groupEnd: -1,
  77. groupCount: -1,
  78. inherited: false,
  79. clone: function(index) {
  80. const cloned = {
  81. index: typeof index === 'number' ? index : this.index,
  82. name: this.name,
  83. mtllib: this.mtllib,
  84. smooth: this.smooth,
  85. groupStart: 0,
  86. groupEnd: -1,
  87. groupCount: -1,
  88. inherited: false
  89. }
  90. cloned.clone = this.clone.bind(cloned)
  91. return cloned
  92. }
  93. }
  94. this.materials.push(material)
  95. return material
  96. },
  97. currentMaterial: function() {
  98. if (this.materials.length > 0) {
  99. return this.materials[this.materials.length - 1]
  100. }
  101. return undefined
  102. },
  103. _finalize: function(end) {
  104. const lastMultiMaterial = this.currentMaterial()
  105. if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) {
  106. lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3
  107. lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart
  108. lastMultiMaterial.inherited = false
  109. }
  110. // Ignore objects tail materials if no face declarations followed them before a new o/g started.
  111. if (end && this.materials.length > 1) {
  112. for (let mi = this.materials.length - 1; mi >= 0; mi--) {
  113. if (this.materials[mi].groupCount <= 0) {
  114. this.materials.splice(mi, 1)
  115. }
  116. }
  117. }
  118. // Guarantee at least one empty material, this makes the creation later more straight forward.
  119. if (end && this.materials.length === 0) {
  120. this.materials.push({
  121. name: '',
  122. smooth: this.smooth
  123. })
  124. }
  125. return lastMultiMaterial
  126. }
  127. }
  128. // Inherit previous objects material.
  129. // Spec tells us that a declared material must be set to all objects until a new material is declared.
  130. // If a usemtl declaration is encountered while this new object is being parsed, it will
  131. // overwrite the inherited material. Exception being that there was already face declarations
  132. // to the inherited material, then it will be preserved for proper MultiMaterial continuation.
  133. if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') {
  134. const declared = previousMaterial.clone(0)
  135. declared.inherited = true
  136. this.object.materials.push(declared)
  137. }
  138. this.objects.push(this.object)
  139. },
  140. finalize: function() {
  141. if (this.object && typeof this.object._finalize === 'function') {
  142. this.object._finalize(true)
  143. }
  144. },
  145. parseVertexIndex: function(value, len) {
  146. const index = parseInt(value, 10)
  147. return (index >= 0 ? index - 1 : index + len / 3) * 3
  148. },
  149. parseNormalIndex: function(value, len) {
  150. const index = parseInt(value, 10)
  151. return (index >= 0 ? index - 1 : index + len / 3) * 3
  152. },
  153. parseUVIndex: function(value, len) {
  154. const index = parseInt(value, 10)
  155. return (index >= 0 ? index - 1 : index + len / 2) * 2
  156. },
  157. addVertex: function(a, b, c) {
  158. const src = this.vertices
  159. const dst = this.object.geometry.vertices
  160. dst.push(src[a + 0], src[a + 1], src[a + 2])
  161. dst.push(src[b + 0], src[b + 1], src[b + 2])
  162. dst.push(src[c + 0], src[c + 1], src[c + 2])
  163. },
  164. addVertexPoint: function(a) {
  165. const src = this.vertices
  166. const dst = this.object.geometry.vertices
  167. dst.push(src[a + 0], src[a + 1], src[a + 2])
  168. },
  169. addVertexLine: function(a) {
  170. const src = this.vertices
  171. const dst = this.object.geometry.vertices
  172. dst.push(src[a + 0], src[a + 1], src[a + 2])
  173. },
  174. addNormal: function(a, b, c) {
  175. const src = this.normals
  176. const dst = this.object.geometry.normals
  177. dst.push(src[a + 0], src[a + 1], src[a + 2])
  178. dst.push(src[b + 0], src[b + 1], src[b + 2])
  179. dst.push(src[c + 0], src[c + 1], src[c + 2])
  180. },
  181. addFaceNormal: function(a, b, c) {
  182. const src = this.vertices
  183. const dst = this.object.geometry.normals
  184. _vA.fromArray(src, a)
  185. _vB.fromArray(src, b)
  186. _vC.fromArray(src, c)
  187. _cb.subVectors(_vC, _vB)
  188. _ab.subVectors(_vA, _vB)
  189. _cb.cross(_ab)
  190. _cb.normalize()
  191. dst.push(_cb.x, _cb.y, _cb.z)
  192. dst.push(_cb.x, _cb.y, _cb.z)
  193. dst.push(_cb.x, _cb.y, _cb.z)
  194. },
  195. addColor: function(a, b, c) {
  196. const src = this.colors
  197. const dst = this.object.geometry.colors
  198. if (src[a] !== undefined) dst.push(src[a + 0], src[a + 1], src[a + 2])
  199. if (src[b] !== undefined) dst.push(src[b + 0], src[b + 1], src[b + 2])
  200. if (src[c] !== undefined) dst.push(src[c + 0], src[c + 1], src[c + 2])
  201. },
  202. addUV: function(a, b, c) {
  203. const src = this.uvs
  204. const dst = this.object.geometry.uvs
  205. dst.push(src[a + 0], src[a + 1])
  206. dst.push(src[b + 0], src[b + 1])
  207. dst.push(src[c + 0], src[c + 1])
  208. },
  209. addDefaultUV: function() {
  210. const dst = this.object.geometry.uvs
  211. dst.push(0, 0)
  212. dst.push(0, 0)
  213. dst.push(0, 0)
  214. },
  215. addUVLine: function(a) {
  216. const src = this.uvs
  217. const dst = this.object.geometry.uvs
  218. dst.push(src[a + 0], src[a + 1])
  219. },
  220. addFace: function(a, b, c, ua, ub, uc, na, nb, nc) {
  221. const vLen = this.vertices.length
  222. let ia = this.parseVertexIndex(a, vLen)
  223. let ib = this.parseVertexIndex(b, vLen)
  224. let ic = this.parseVertexIndex(c, vLen)
  225. this.addVertex(ia, ib, ic)
  226. this.addColor(ia, ib, ic)
  227. // normals
  228. if (na !== undefined && na !== '') {
  229. const nLen = this.normals.length
  230. ia = this.parseNormalIndex(na, nLen)
  231. ib = this.parseNormalIndex(nb, nLen)
  232. ic = this.parseNormalIndex(nc, nLen)
  233. this.addNormal(ia, ib, ic)
  234. } else {
  235. this.addFaceNormal(ia, ib, ic)
  236. }
  237. // uvs
  238. if (ua !== undefined && ua !== '') {
  239. const uvLen = this.uvs.length
  240. ia = this.parseUVIndex(ua, uvLen)
  241. ib = this.parseUVIndex(ub, uvLen)
  242. ic = this.parseUVIndex(uc, uvLen)
  243. this.addUV(ia, ib, ic)
  244. this.object.geometry.hasUVIndices = true
  245. } else {
  246. // add placeholder values (for inconsistent face definitions)
  247. this.addDefaultUV()
  248. }
  249. },
  250. addPointGeometry: function(vertices) {
  251. this.object.geometry.type = 'Points'
  252. const vLen = this.vertices.length
  253. for (let vi = 0, l = vertices.length; vi < l; vi++) {
  254. const index = this.parseVertexIndex(vertices[vi], vLen)
  255. this.addVertexPoint(index)
  256. this.addColor(index)
  257. }
  258. },
  259. addLineGeometry: function(vertices, uvs) {
  260. this.object.geometry.type = 'Line'
  261. const vLen = this.vertices.length
  262. const uvLen = this.uvs.length
  263. for (let vi = 0, l = vertices.length; vi < l; vi++) {
  264. this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen))
  265. }
  266. for (let uvi = 0, l = uvs.length; uvi < l; uvi++) {
  267. this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen))
  268. }
  269. }
  270. }
  271. state.startObject('', false)
  272. return state
  273. }
  274. //
  275. class OBJLoader extends Loader {
  276. constructor(manager) {
  277. super(manager)
  278. this.materials = null
  279. }
  280. load(url, onLoad, onProgress, onError) {
  281. const scope = this
  282. const loader = new FileLoader(this.manager)
  283. loader.setPath(this.path)
  284. loader.setRequestHeader(this.requestHeader)
  285. loader.setWithCredentials(this.withCredentials)
  286. loader.load(
  287. url,
  288. function(text) {
  289. try {
  290. onLoad(scope.parse(text))
  291. } catch (e) {
  292. if (onError) {
  293. onError(e)
  294. } else {
  295. console.error(e)
  296. }
  297. scope.manager.itemError(url)
  298. }
  299. },
  300. onProgress,
  301. onError
  302. )
  303. }
  304. setMaterials(materials) {
  305. this.materials = materials
  306. return this
  307. }
  308. parse(text) {
  309. const state = new ParserState()
  310. if (text.indexOf('\r\n') !== -1) {
  311. // This is faster than String.split with regex that splits on both
  312. text = text.replace(/\r\n/g, '\n')
  313. }
  314. if (text.indexOf('\\\n') !== -1) {
  315. // join lines separated by a line continuation character (\)
  316. text = text.replace(/\\\n/g, '')
  317. }
  318. const lines = text.split('\n')
  319. let line = '',
  320. lineFirstChar = ''
  321. let lineLength = 0
  322. let result = []
  323. // Faster to just trim left side of the line. Use if available.
  324. const trimLeft = typeof ''.trimLeft === 'function'
  325. for (let i = 0, l = lines.length; i < l; i++) {
  326. line = lines[i]
  327. line = trimLeft ? line.trimLeft() : line.trim()
  328. lineLength = line.length
  329. if (lineLength === 0) continue
  330. lineFirstChar = line.charAt(0)
  331. // @todo invoke passed in handler if any
  332. if (lineFirstChar === '#') continue
  333. if (lineFirstChar === 'v') {
  334. const data = line.split(/\s+/)
  335. switch (data[0]) {
  336. case 'v':
  337. state.vertices.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]))
  338. if (data.length >= 7) {
  339. state.colors.push(parseFloat(data[4]), parseFloat(data[5]), parseFloat(data[6]))
  340. } else {
  341. // if no colors are defined, add placeholders so color and vertex indices match
  342. state.colors.push(undefined, undefined, undefined)
  343. }
  344. break
  345. case 'vn':
  346. state.normals.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]))
  347. break
  348. case 'vt':
  349. state.uvs.push(parseFloat(data[1]), parseFloat(data[2]))
  350. break
  351. }
  352. } else if (lineFirstChar === 'f') {
  353. const lineData = line.substr(1).trim()
  354. const vertexData = lineData.split(/\s+/)
  355. const faceVertices = []
  356. // Parse the face vertex data into an easy to work with format
  357. for (let j = 0, jl = vertexData.length; j < jl; j++) {
  358. const vertex = vertexData[j]
  359. if (vertex.length > 0) {
  360. const vertexParts = vertex.split('/')
  361. faceVertices.push(vertexParts)
  362. }
  363. }
  364. // Draw an edge between the first vertex and all subsequent vertices to form an n-gon
  365. const v1 = faceVertices[0]
  366. for (let j = 1, jl = faceVertices.length - 1; j < jl; j++) {
  367. const v2 = faceVertices[j]
  368. const v3 = faceVertices[j + 1]
  369. state.addFace(v1[0], v2[0], v3[0], v1[1], v2[1], v3[1], v1[2], v2[2], v3[2])
  370. }
  371. } else if (lineFirstChar === 'l') {
  372. const lineParts = line
  373. .substring(1)
  374. .trim()
  375. .split(' ')
  376. let lineVertices = []
  377. const lineUVs = []
  378. if (line.indexOf('/') === -1) {
  379. lineVertices = lineParts
  380. } else {
  381. for (let li = 0, llen = lineParts.length; li < llen; li++) {
  382. const parts = lineParts[li].split('/')
  383. if (parts[0] !== '') lineVertices.push(parts[0])
  384. if (parts[1] !== '') lineUVs.push(parts[1])
  385. }
  386. }
  387. state.addLineGeometry(lineVertices, lineUVs)
  388. } else if (lineFirstChar === 'p') {
  389. const lineData = line.substr(1).trim()
  390. const pointData = lineData.split(' ')
  391. state.addPointGeometry(pointData)
  392. } else if ((result = _object_pattern.exec(line)) !== null) {
  393. // o object_name
  394. // or
  395. // g group_name
  396. // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
  397. // let name = result[ 0 ].substr( 1 ).trim();
  398. const name = (' ' + result[0].substr(1).trim()).substr(1)
  399. state.startObject(name)
  400. } else if (_material_use_pattern.test(line)) {
  401. // material
  402. state.object.startMaterial(line.substring(7).trim(), state.materialLibraries)
  403. } else if (_material_library_pattern.test(line)) {
  404. // mtl file
  405. state.materialLibraries.push(line.substring(7).trim())
  406. } else if (_map_use_pattern.test(line)) {
  407. // the line is parsed but ignored since the loader assumes textures are defined MTL files
  408. // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
  409. console.warn('THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.')
  410. } else if (lineFirstChar === 's') {
  411. result = line.split(' ')
  412. // smooth shading
  413. // @todo Handle files that have varying smooth values for a set of faces inside one geometry,
  414. // but does not define a usemtl for each face set.
  415. // This should be detected and a dummy material created (later MultiMaterial and geometry groups).
  416. // This requires some care to not create extra material on each smooth value for "normal" obj files.
  417. // where explicit usemtl defines geometry groups.
  418. // Example asset: examples/models/obj/cerberus/Cerberus.obj
  419. /*
  420. * http://paulbourke.net/dataformats/obj/
  421. * or
  422. * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
  423. *
  424. * From chapter "Grouping" Syntax explanation "s group_number":
  425. * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
  426. * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
  427. * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
  428. * than 0."
  429. */
  430. if (result.length > 1) {
  431. const value = result[1].trim().toLowerCase()
  432. state.object.smooth = value !== '0' && value !== 'off'
  433. } else {
  434. // ZBrush can produce "s" lines #11707
  435. state.object.smooth = true
  436. }
  437. const material = state.object.currentMaterial()
  438. if (material) material.smooth = state.object.smooth
  439. } else {
  440. // Handle null terminated files without exception
  441. if (line === '\0') continue
  442. console.warn('THREE.OBJLoader: Unexpected line: "' + line + '"')
  443. }
  444. }
  445. state.finalize()
  446. const container = new Group()
  447. container.materialLibraries = [].concat(state.materialLibraries)
  448. const hasPrimitives = !(state.objects.length === 1 && state.objects[0].geometry.vertices.length === 0)
  449. if (hasPrimitives === true) {
  450. for (let i = 0, l = state.objects.length; i < l; i++) {
  451. const object = state.objects[i]
  452. const geometry = object.geometry
  453. const materials = object.materials
  454. const isLine = geometry.type === 'Line'
  455. const isPoints = geometry.type === 'Points'
  456. let hasVertexColors = false
  457. // Skip o/g line declarations that did not follow with any faces
  458. if (geometry.vertices.length === 0) continue
  459. const buffergeometry = new BufferGeometry()
  460. buffergeometry.setAttribute('position', new Float32BufferAttribute(geometry.vertices, 3))
  461. if (geometry.normals.length > 0) {
  462. buffergeometry.setAttribute('normal', new Float32BufferAttribute(geometry.normals, 3))
  463. }
  464. if (geometry.colors.length > 0) {
  465. hasVertexColors = true
  466. buffergeometry.setAttribute('color', new Float32BufferAttribute(geometry.colors, 3))
  467. }
  468. if (geometry.hasUVIndices === true) {
  469. buffergeometry.setAttribute('uv', new Float32BufferAttribute(geometry.uvs, 2))
  470. }
  471. // Create materials
  472. const createdMaterials = []
  473. for (let mi = 0, miLen = materials.length; mi < miLen; mi++) {
  474. const sourceMaterial = materials[mi]
  475. const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors
  476. let material = state.materials[materialHash]
  477. if (this.materials !== null) {
  478. material = this.materials.create(sourceMaterial.name)
  479. // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
  480. if (isLine && material && !(material instanceof LineBasicMaterial)) {
  481. const materialLine = new LineBasicMaterial()
  482. Material.prototype.copy.call(materialLine, material)
  483. materialLine.color.copy(material.color)
  484. material = materialLine
  485. } else if (isPoints && material && !(material instanceof PointsMaterial)) {
  486. const materialPoints = new PointsMaterial({ size: 10, sizeAttenuation: false })
  487. Material.prototype.copy.call(materialPoints, material)
  488. materialPoints.color.copy(material.color)
  489. materialPoints.map = material.map
  490. material = materialPoints
  491. }
  492. }
  493. if (material === undefined) {
  494. if (isLine) {
  495. material = new LineBasicMaterial()
  496. } else if (isPoints) {
  497. material = new PointsMaterial({ size: 1, sizeAttenuation: false })
  498. } else {
  499. material = new MeshPhongMaterial()
  500. }
  501. material.name = sourceMaterial.name
  502. material.flatShading = sourceMaterial.smooth ? false : true
  503. material.vertexColors = hasVertexColors
  504. state.materials[materialHash] = material
  505. }
  506. createdMaterials.push(material)
  507. }
  508. // Create mesh
  509. let mesh
  510. if (createdMaterials.length > 1) {
  511. for (let mi = 0, miLen = materials.length; mi < miLen; mi++) {
  512. const sourceMaterial = materials[mi]
  513. buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi)
  514. }
  515. if (isLine) {
  516. mesh = new LineSegments(buffergeometry, createdMaterials)
  517. } else if (isPoints) {
  518. mesh = new Points(buffergeometry, createdMaterials)
  519. } else {
  520. mesh = new Mesh(buffergeometry, createdMaterials)
  521. }
  522. } else {
  523. if (isLine) {
  524. mesh = new LineSegments(buffergeometry, createdMaterials[0])
  525. } else if (isPoints) {
  526. mesh = new Points(buffergeometry, createdMaterials[0])
  527. } else {
  528. mesh = new Mesh(buffergeometry, createdMaterials[0])
  529. }
  530. }
  531. mesh.name = object.name
  532. container.add(mesh)
  533. }
  534. } else {
  535. // if there is only the default parser state object with no geometry data, interpret data as point cloud
  536. if (state.vertices.length > 0) {
  537. const material = new PointsMaterial({ size: 1, sizeAttenuation: false })
  538. const buffergeometry = new BufferGeometry()
  539. buffergeometry.setAttribute('position', new Float32BufferAttribute(state.vertices, 3))
  540. if (state.colors.length > 0 && state.colors[0] !== undefined) {
  541. buffergeometry.setAttribute('color', new Float32BufferAttribute(state.colors, 3))
  542. material.vertexColors = true
  543. }
  544. const points = new Points(buffergeometry, material)
  545. container.add(points)
  546. }
  547. }
  548. return container
  549. }
  550. }
  551. export { OBJLoader }