import * as THREE from 'three' ; import potpack , { PotPackItem } from 'potpack' ; const tmpOrigin = new THREE . Vector3 ( ) ; const tmpU = new THREE . Vector3 ( ) ; const tmpV = new THREE . Vector3 ( ) ; const tmpW = new THREE . Vector3 ( ) ; const tmpNormal = new THREE . Vector3 ( ) ; const tmpUAxis = new THREE . Vector3 ( ) ; const tmpVAxis = new THREE . Vector3 ( ) ; const tmpWLocal = new THREE . Vector2 ( ) ; const tmpMinLocal = new THREE . Vector2 ( ) ; const tmpMaxLocal = new THREE . Vector2 ( ) ; const tmpVert = new THREE . Vector3 ( ) ; const tmpVert2 = new THREE . Vector3 ( ) ; const tmpNormal2 = new THREE . Vector3 ( ) ; function findVertex ( posArray , normalArray , groupIndexArray , vertexIndex ) { tmpVert . fromArray ( posArray , vertexIndex * 3 ) ; tmpNormal . fromArray ( normalArray , vertexIndex * 3 ) ; const gi = groupIndexArray [ vertexIndex ] ; for ( let vStart = 0 ; vStart < vertexIndex ; vStart += 1 ) { tmpVert2 . fromArray ( posArray , vStart * 3 ) ; tmpNormal2 . fromArray ( normalArray , vStart * 3 ) ; const gi2 = groupIndexArray [ vStart ] ; if ( tmpVert2 . equals ( tmpVert ) && tmpNormal2 . equals ( tmpNormal ) && gi === gi2 ) { return vStart ; } } return vertexIndex ; } function convertGeometryToIndexed ( buffer ) { const posArray = buffer . attributes . position . array ; const posVertexCount = Math . floor ( posArray . length / 3 ) ; const faceCount = Math . floor ( posVertexCount / 3 ) ; const normalArray = buffer . attributes . normal . array ; const origGroups = buffer . groups || [ ] ; const groupIndexArray = new Array ( posVertexCount ) ; for ( const group of origGroups ) { groupIndexArray . fill ( group . materialIndex , group . start , Math . min ( posVertexCount , group . start + group . count ) ) ; } const indexAttr = new THREE . Uint16BufferAttribute ( faceCount * 3 , 3 ) ; indexAttr . count = faceCount * 3 ; for ( let faceIndex = 0 ; faceIndex < faceCount ; faceIndex += 1 ) { const vStart = faceIndex * 3 ; const a = findVertex ( posArray , normalArray , groupIndexArray , vStart ) ; const b = findVertex ( posArray , normalArray , groupIndexArray , vStart + 1 ) ; const c = findVertex ( posArray , normalArray , groupIndexArray , vStart + 2 ) ; indexAttr . setXYZ ( faceIndex , a , b , c ) ; } buffer . setIndex ( indexAttr ) ; } function guessOrthogonalOrigin ( indexArray , vStart , posArray ) { let minAbsDot = 1 ; let minI = 0 ; for ( let i = 0 ; i < 3 ; i += 1 ) { tmpOrigin . fromArray ( posArray , indexArray [ vStart + i ] * 3 ) ; tmpU . fromArray ( posArray , indexArray [ vStart + ( ( i + 2 ) % 3 ) ] * 3 ) ; tmpV . fromArray ( posArray , indexArray [ vStart + ( ( i + 1 ) % 3 ) ] * 3 ) ; tmpU . sub ( tmpOrigin ) ; tmpV . sub ( tmpOrigin ) ; tmpU . normalize ( ) ; tmpV . normalize ( ) ; const absDot = Math . abs ( tmpU . dot ( tmpV ) ) ; if ( minAbsDot > absDot ) { minAbsDot = absDot ; minI = i ; } } return minI ; } export const AUTO_UV2_OPT_OUT_FLAG = Symbol ( 'auto-UV2 opt out flag' ) ; function traverseAutoUV2Items ( object , callback ) { if ( Object . prototype . hasOwnProperty . call ( object . userData , AUTO_UV2_OPT_OUT_FLAG ) ) { return ; } callback ( object ) ; for ( const childObject of object . children ) { traverseAutoUV2Items ( childObject , callback ) ; } } const MAX_AUTO_SIZE = 512 ; function autoSelectSize ( layoutSize ) { for ( let size = 4 ; size <= MAX_AUTO_SIZE ; size *= 2 ) { if ( layoutSize < size ) { return size ; } } throw new Error ( `minimum lightmap dimension for auto-UV2 is ${ layoutSize } which is too large: please reduce texelsPerUnit and/or polygon count` ) ; } interface AutoUVBox extends PotPackItem { uv2Attr : THREE . Float32BufferAttribute ; uAxis : THREE . Vector3 ; vAxis : THREE . Vector3 ; posArray : ArrayLike < number > ; posIndices : number [ ] ; posLocalX : number [ ] ; posLocalY : number [ ] ; } export interface AutoUV2Settings { texelsPerUnit : number ; } export function computeAutoUV2Layout ( initialWidth , initialHeight , scene , { texelsPerUnit } ) { const layoutBoxes = [ ] ; let hasPredefinedUV2 = false ; traverseAutoUV2Items ( scene , ( mesh ) => { if ( ! ( mesh instanceof THREE . Mesh ) ) { return ; } const buffer = mesh . geometry ; if ( ! ( buffer instanceof THREE . BufferGeometry ) ) { throw new Error ( 'expecting buffer geometry' ) ; } if ( ! buffer . index ) { convertGeometryToIndexed ( buffer ) ; } const indexAttr = buffer . index ; if ( ! indexAttr ) { throw new Error ( 'unexpected missing geometry index attr' ) ; } const indexArray = indexAttr . array ; const faceCount = Math . floor ( indexArray . length / 3 ) ; const posArray = buffer . attributes . position . array ; const normalArray = buffer . attributes . normal . array ; const vertexBoxMap = new Array ( posArray . length / 3 ) ; if ( buffer . attributes . uv2 ) { if ( layoutBoxes . length > 0 ) { throw new Error ( 'found a mesh with "uv2" attribute in a scene with auto-calculated UV2 data: please do not mix-and-match' ) ; } hasPredefinedUV2 = true ; return ; } if ( hasPredefinedUV2 ) { throw new Error ( 'found a mesh with missing "uv2" attribute in a scene with predefined UV2 data: please do not mix-and-match' ) ; } const uv2Attr = new THREE . Float32BufferAttribute ( ( 2 * posArray . length ) / 3 , 2 ) ; buffer . setAttribute ( 'uv2' , uv2Attr ) ; for ( let vStart = 0 ; vStart < faceCount * 3 ; vStart += 3 ) { let existingBox ; for ( let i = 0 ; i < 3 ; i += 1 ) { const possibleBox = vertexBoxMap [ indexArray [ vStart + i ] ] ; if ( ! possibleBox ) { continue ; } if ( existingBox && existingBox !== possibleBox ) { existingBox . posIndices . push ( ... possibleBox . posIndices ) ; existingBox . posLocalX . push ( ... possibleBox . posLocalX ) ; existingBox . posLocalY . push ( ... possibleBox . posLocalY ) ; for ( const index of possibleBox . posIndices ) { vertexBoxMap [ index ] = existingBox ; } const removedBoxIndex = layoutBoxes . indexOf ( possibleBox ) ; if ( removedBoxIndex === - 1 ) { throw new Error ( 'unexpected orphaned layout box' ) ; } layoutBoxes . splice ( removedBoxIndex , 1 ) ; } else { existingBox = possibleBox ; } } if ( ! existingBox ) { const originFI = guessOrthogonalOrigin ( indexArray , vStart , posArray ) ; const vOrigin = vStart + originFI ; const vU = vStart + ( ( originFI + 2 ) % 3 ) ; const vV = vStart + ( ( originFI + 1 ) % 3 ) ; tmpOrigin . fromArray ( posArray , indexArray [ vOrigin ] * 3 ) ; tmpU . fromArray ( posArray , indexArray [ vU ] * 3 ) ; tmpV . fromArray ( posArray , indexArray [ vV ] * 3 ) ; tmpU . sub ( tmpOrigin ) ; tmpV . sub ( tmpOrigin ) ; tmpNormal . fromArray ( normalArray , indexArray [ vOrigin ] * 3 ) ; tmpUAxis . crossVectors ( tmpV , tmpNormal ) ; tmpVAxis . crossVectors ( tmpNormal , tmpUAxis ) ; tmpUAxis . normalize ( ) ; tmpVAxis . normalize ( ) ; existingBox = { x : 0 , y : 0 , w : 0 , h : 0 , uv2Attr , uAxis : tmpUAxis . clone ( ) , vAxis : tmpVAxis . clone ( ) , posArray , posIndices : [ ] , posLocalX : [ ] , posLocalY : [ ] } ; layoutBoxes . push ( existingBox ) ; } for ( let i = 0 ; i < 3 ; i += 1 ) { const index = indexArray [ vStart + i ] ; if ( vertexBoxMap [ index ] ) { continue ; } vertexBoxMap [ index ] = existingBox ; existingBox . posIndices . push ( index ) ; existingBox . posLocalX . push ( 0 ) ; existingBox . posLocalY . push ( 0 ) ; } } } ) ; for ( const layoutBox of layoutBoxes ) { const { uAxis , vAxis , posArray , posIndices , posLocalX , posLocalY } = layoutBox ; tmpMinLocal . set ( Number . POSITIVE_INFINITY , Number . POSITIVE_INFINITY ) ; tmpMaxLocal . set ( Number . NEGATIVE_INFINITY , Number . NEGATIVE_INFINITY ) ; for ( let i = 0 ; i < posIndices . length ; i += 1 ) { const index = posIndices [ i ] ; tmpW . fromArray ( posArray , index * 3 ) ; tmpWLocal . set ( tmpW . dot ( uAxis ) , tmpW . dot ( vAxis ) ) ; tmpMinLocal . min ( tmpWLocal ) ; tmpMaxLocal . max ( tmpWLocal ) ; posLocalX [ i ] = tmpWLocal . x ; posLocalY [ i ] = tmpWLocal . y ; } const realWidth = tmpMaxLocal . x - tmpMinLocal . x ; const realHeight = tmpMaxLocal . y - tmpMinLocal . y ; if ( realWidth < 0 || realHeight < 0 ) { throw new Error ( 'zero-point polygon?' ) ; } const boxWidthInTexels = Math . ceil ( realWidth * texelsPerUnit ) ; const boxHeightInTexels = Math . ceil ( realHeight * texelsPerUnit ) ; layoutBox . w = boxWidthInTexels + 2 ; layoutBox . h = boxHeightInTexels + 2 ; for ( let i = 0 ; i < posIndices . length ; i += 1 ) { posLocalX [ i ] = ( posLocalX [ i ] - tmpMinLocal . x ) / realWidth ; posLocalY [ i ] = ( posLocalY [ i ] - tmpMinLocal . y ) / realHeight ; } } if ( layoutBoxes . length === 0 ) { return [ initialWidth || 0 , initialHeight || 0 ] ; } const { w : layoutWidth , h : layoutHeight } = potpack ( layoutBoxes ) ; if ( ( initialWidth && layoutWidth > initialWidth ) || ( initialHeight && layoutHeight > initialHeight ) ) { throw new Error ( `minimum lightmap size for auto-UV2 is ${ layoutWidth }x${ layoutHeight } which is too large to fit provided ${ initialWidth }x${ initialHeight }: please reduce texelsPerUnit and/or polygon count` ) ; } const finalWidth = initialWidth || autoSelectSize ( layoutWidth ) ; const finalHeight = initialHeight || autoSelectSize ( layoutHeight ) ; for ( const layoutBox of layoutBoxes ) { const { x , y , w , h , uv2Attr , posIndices , posLocalX , posLocalY } = layoutBox ; const ix = x + 1 ; const iy = y + 1 ; const iw = w - 2 ; const ih = h - 2 ; for ( let i = 0 ; i < posIndices . length ; i += 1 ) { uv2Attr . setXY ( posIndices [ i ] , ( ix + posLocalX [ i ] * iw ) / finalWidth , ( iy + posLocalY [ i ] * ih ) / finalHeight ) ; } } return [ finalWidth , finalHeight ] ; }