js/xfile/XParser.js
'use strict'
import Bone from '../base/Bone'
import Vector3 from '../base/Vector3'
import Vector4 from '../base/Vector4'
import Material from '../base/Material'
import RenderGroup from '../base/RenderGroup'
import Skin from '../base/Skin'
import TextureUV from '../base/TextureUV'
import XModel from './XModel'
import TextureBank from '../base/TextureBank'
import ObjectAssign from '../etc/ObjectAssign'
// patterns
const _integerPattern = new RegExp(/^(-|\+)?\d+;?/)
const _floatPattern = new RegExp(/^(-|\+)?(\d)*\.(\d)*;?/)
//const _commaOrSemicolonPattern = new RegExp(/^,|;/)
const _wordPattern = new RegExp(/^\w+/)
const _uuidPattern = new RegExp(/^<[\w-]+>/)
const _leftBracePattern = new RegExp(/^{/)
const _rightBracePattern = new RegExp(/^}/)
const _memberPattern = new RegExp(/^((array\s+\w+\s+\w+\[(\d+|\w+)\]|\w+\s+\w+)\s*;|\[[\w.]+\])/)
const _filenamePattern = new RegExp(/^"(.*)";?/)
/**
* XParser class
* @access public
*/
export default class XParser {
/**
* constructor
* @access public
* @constructor
*/
constructor() {
this._text = null
this._obj = null
this._parentDirName = null
this._indices = null
this._materialIndex = 0
this._normalArray = null
this._faceNormalArray = null
this._offset = 0
this._err = 0
this._parentDirName = './'
// this._skipPattern = new RegExp(/^\s+/)
this._partialText = ''
this._partialOffset = 0
this._partialStep = 1000
this._partialMinLength = 500
//this._partialMaxLength = 5000
}
/**
*
* @access public
* @param {String} dirName -
* @returns {void}
*/
setParentDirName(dirName) {
this._parentDirName = dirName
}
/**
*
* @access public
* @param {XModel} obj -
* @returns {void}
*/
setModel(obj) {
this._obj = obj
}
/**
*
* @access public
* @param {String} text -
* @returns {XModel} -
*/
parse(text) {
this._text = text
this._partialText = ''
if(!this._obj){
this._obj = new XModel()
}
this._offset = 0
this._partialOffset = 0
this._err = 0
this.addPartialText()
if(!this.XFileHeader(this._obj)){
console.error('header format error')
this._err = 1
return null
}
while(this.XObjectLong(this._obj)){ /* nothing to do */ }
if(this._err){
console.error('xobj format error:' + this._err)
return null
}
this.splitFaceNormals()
return this._obj
}
/**
*
* @access private
* @param {number} len -
* @returns {void}
*/
moveIndex(len) {
this._partialText = this._partialText.substring(len)
this._offset += len
}
/**
*
* @access private
* @param {RegExp} pattern -
* @returns {String} - matched string
*/
getString(pattern) {
this.skip()
const str = this._partialText.match(pattern)
/*
while(str == null){
if(this._partialText.length > this._partialMaxLength)
return null
this.addPartialText()
str = this._partialText.match(pattern)
}
*/
if(str === null)
return null
this.moveIndex(str[0].length)
if(this._partialText.length < this._partialMinLength)
this.addPartialText()
return str[0]
}
/**
* skip space, tab
* @access private
* @returns {void}
*/
skip() {
/*
const str = this._text.match(this._skipPattern)
if(str != null){
const len = str[0].length
this.moveIndex(len)
}
*/
let i = 0
let code = this._partialText.charCodeAt(i)
// 9: Horizontal Tab
// 10: Line Feed
// 11: Vertical Tab
// 12: New Page
// 13: Carriage Return
// 32: Space
while(code === 32 || (9 <= code && code <= 13)){
i++
code = this._partialText.charCodeAt(i)
if(i >= this._partialText.length){
this.addPartialText()
}
}
if(i>0){
this.moveIndex(i)
}
}
/**
*
* @access private
* @returns {void}
*/
addPartialText() {
if(this._partialOffset >= this._text.length)
return
let newOffset = this._partialOffset + this._partialStep
if(newOffset > this._text.length){
newOffset = this._text.length
}
this._partialText += this._text.substring(this._partialOffset, newOffset)
this._partialOffset = newOffset
}
/**
* get integer value
* @access private
* @returns {int} -
*/
getInteger() {
const str = this.getString(_integerPattern)
const val = parseInt(str, 10)
return val
}
/**
* get float value
* @access private
* @returns {float} -
*/
getFloat() {
const str = this.getString(_floatPattern)
const val = parseFloat(str)
return val
}
/**
* skip "," or ";"
* @access private
* @returns {void} -
*/
getCommaOrSemicolon() {
/*
return this.getString(this._commaOrSemicolonPattern)
*/
const code = this._partialText.charCodeAt(0)
if(code === 44 || code === 59){
this.moveIndex(1)
}
}
/**
* get string value
* @access private
* @returns {string} -
*/
getWord() {
return this.getString(_wordPattern)
}
/**
* get UUID string
* @access private
* @returns {string} - UUID
*/
getUUID() {
return this.getString(_uuidPattern)
}
/**
* get "{"
* @access private
* @returns {string} -
*/
getLeftBrace() {
return this.getString(_leftBracePattern)
}
/**
* get "}"
* @access private
* @returns {string} -
*/
getRightBrace() {
return this.getString(_rightBracePattern)
}
/**
* get member string
* @access private
* @returns {string} -
*/
getMember() {
return this.getString(_memberPattern)
}
/**
* get filename string
* @access private
* @returns {string} -
*/
getFilename() {
//const str = this.getString(_filenamePattern)
this.getString(_filenamePattern)
return RegExp.$1
}
/**
* get integer array
* @access private
* @returns {Array} -
*/
getIntegerArray() {
const n = this.getInteger()
const arr = []
for(let i=0; i<n; i++){
arr.push(this.getInteger())
this.getCommaOrSemicolon()
}
return arr
}
/**
* get float array
* @access private
* @returns {Array} -
*/
getFloatArray() {
const n = this.getInteger()
const arr = []
for(let i=0; i<n; i++){
arr.push(this.getInteger())
this.getCommaOrSemicolon()
}
return arr
}
/**
* get Vector3 value
* @access private
* @returns {Vector3} -
*/
getVector3() {
const v = new Vector3()
v.x = this.getFloat()
v.y = this.getFloat()
v.z = this.getFloat()
this.getCommaOrSemicolon()
return v
}
/**
* get Vector4 value
* @access private
* @returns {Vector4} -
*/
getVector4() {
const v = new Vector4()
v.x = this.getFloat()
v.y = this.getFloat()
v.z = this.getFloat()
v.w = this.getFloat()
this.getCommaOrSemicolon()
return v
}
/**
* copy vertices after loading xfile
* @access private
* @returns {void}
*/
splitFaceNormals() {
const v = this._faceNormalArray
const vnMap = []
const normals = []
const skins = this._obj.skinArray
const vertexCount = skins.length
// textureCoordsの設定
if(skins[0].textureUV === null){
for(let i=0; i<vertexCount; i++){
skins[i].textureUV = new TextureUV()
}
}
// 法線の設定
if(this._faceNormalArray === null){
// 法線が指定されていない場合、自分で計算する。
const ins = this._indices
const numIns = ins.length
const used = []
let n = null
const n1 = new Vector3()
const n2 = new Vector3()
for(let i=0; i<numIns; i++){
if(ins[i].length === 4){
// 四角形
const ii = ins[i]
let s = skins[ii[0]]
n = new Vector3()
if(used[ii[0]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[0]] = true
n1.sub(skins[ii[2]].position, skins[ii[0]].position)
n2.sub(skins[ii[1]].position, skins[ii[0]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
n = new Vector3()
s = skins[ii[1]]
if(used[ii[1]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[1]] = true
n1.sub(skins[ii[0]].position, skins[ii[1]].position)
n2.sub(skins[ii[2]].position, skins[ii[1]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
n = new Vector3()
s = skins[ii[2]]
if(used[ii[2]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[2]] = true
n1.sub(skins[ii[1]].position, skins[ii[2]].position)
n2.sub(skins[ii[0]].position, skins[ii[2]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
n = new Vector3()
s = skins[ii[3]]
if(used[ii[3]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
if(!(skins[ii[3]] instanceof Object)){
console.log('skins[ii[3]] not instance!')
console.log('i: ' + i + ', ii[3]: ' + ii[3])
}
used[ii[3]] = true
n1.sub(skins[ii[2]].position, skins[ii[3]].position)
n2.sub(skins[ii[0]].position, skins[ii[3]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
}else if(ins[i].length === 3){
// 三角形
const ii = ins[i]
let s = skins[ii[0]]
n = new Vector3()
if(used[ii[0]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[0]] = true
n1.sub(skins[ii[2]].position, skins[ii[0]].position)
n2.sub(skins[ii[1]].position, skins[ii[0]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
n = new Vector3()
s = skins[ii[1]]
if(used[ii[1]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[1]] = true
n1.sub(skins[ii[0]].position, skins[ii[1]].position)
n2.sub(skins[ii[2]].position, skins[ii[1]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
n = new Vector3()
s = skins[ii[2]]
if(used[ii[2]]){
//s = Object.assign(new s.constructor(), s)
s = ObjectAssign(new s.constructor(), s)
skins[skins.length] = s
}
used[ii[2]] = true
n1.sub(skins[ii[1]].position, skins[ii[2]].position)
n2.sub(skins[ii[0]].position, skins[ii[2]].position)
n.cross(n1, n2)
n.normalize()
s.normal = n
}else{
// 未対応
}
}
}else{
// 同じ頂点に違う法線が指定されている場合、別頂点とする。
for(let i=0; i<vertexCount; i++){
vnMap[i] = new Map()
normals[i] = -1
}
const vSize = v.length
const ins = this._indices
for(let i=0; i<vSize; i++){
const vi = v[i]
const ii = ins[i]
for(let j=0; j<vi.length; j++){
if(normals[ii[j]] === -1){
// 未登録
normals[ii[j]] = vi[j]
vnMap[ii[j]].set(vi[j], ii[j])
}else if(normals[ii[j]] === vi[j]){
// 登録済み
}else{
let newNo = vnMap[ii[j]].get(vi[j])
if(newNo === null){
// 未登録
newNo = vnMap.length
vnMap[ii[j]].set(vi[j], newNo)
normals[newNo] = vi[j]
//this._obj.skinArray[newNo] = this._obj.skinArray[ii[j]].clone()
const s = this._obj.skinArray[ii[j]]
//this._obj.skinArray[newNo] = Object.assign(new s.constructor(), s)
this._obj.skinArray[newNo] = ObjectAssign(new s.constructor(), s)
}else{
// 登録済み
}
}
}
}
for(let i=0; i<normals.length; i++){
this._obj.skinArray[i].normal = this._normalArray[normals[i]]
}
}
}
/**
* check header format
* @access private
* @returns {bool} - true if right header format
*/
XFileHeader() {
const text = this._partialText
if(!text.match(/^xof (\d\d\d\d)([ \w][ \w][ \w][ \w])(\d\d\d\d)/)){
return false
}
this.moveIndex(16)
this.version = RegExp.$1
this.format = RegExp.$2
this.floatSize = RegExp.$3
return true
}
/**
* read Object value
* @access private
* @returns {Object} - XObject
*/
XObjectLong(){
const id = this.getWord()
if(id === null){
return null
}
switch(id){
case 'template':
return this.Template()
case 'Header':
return this.Header()
case 'Mesh':
return this.Mesh()
case 'MeshMaterialList':
return this.MeshMaterialList()
case 'MeshNormals':
return this.MeshNormals()
case 'MeshTextureCoords':
return this.MeshTextureCoords()
case 'MeshVertexColors':
return this.MeshVertexColors()
default:
console.error('unknown type:' + id)
break
}
return false
}
/**
* read ColorRGB value
* @access private
* @returns {Vector4} - ColorRGB object
*/
ColorRGB() {
const color = new Vector4()
color.x = this.getFloat()
color.y = this.getFloat()
color.z = this.getFloat()
color.w = 1.0
this.getCommaOrSemicolon()
return color
}
/**
* read ColorRGBA value
* @access private
* @returns {Vector4} - ColorRGBA object
*/
ColorRGBA() {
const color = new Vector4()
color.x = this.getFloat()
color.y = this.getFloat()
color.z = this.getFloat()
color.w = this.getFloat()
this.getCommaOrSemicolon()
return color
}
/**
* read Coords2d object
* @access private
* @returns {TextureUV} - Coords2d object
*/
Coords2d() {
const v = new TextureUV()
v.u = this.getFloat()
v.v = this.getFloat()
this.getCommaOrSemicolon()
return v
}
/**
* read Template object
* @access private
* @returns {bool} - true if right template format
*/
Template() {
this.getWord() // name
this.getLeftBrace()
this.getUUID() // UUID
let member = null
do{
member = this.getMember()
}while(member !== null)
this.getRightBrace()
return true
}
/**
* read Header object
* @access private
* @returns {bool} - true if right header format
*/
Header() {
this.getLeftBrace()
this.getInteger() // major
this.getInteger() // minor
this.getInteger() // flags
this.getRightBrace()
return true
}
/**
* read IndexedColor object
* @access private
* @returns {Vector4} - ColorRGBA object
*/
IndexedColor() {
const index = this.getInteger()
const color = this.ColorRGBA()
color.index = index
return color
}
/**
* read Material object
* @access private
* @returns {Material} - Material object
*/
Material() {
this.getLeftBrace()
const material = new Material()
material.ambient = this.ColorRGBA()
material.diffuse = material.ambient
material.shininess = this.getFloat()
material.specular = this.ColorRGB()
material.emission = this.ColorRGB()
material.edge = 0
material.texture = null
const name = this.getWord()
if(name === 'TextureFilename'){
const texture = this.TextureFilename()
if(texture !== null){
material.texture = texture
//material.textureFileName = texture.fileName
}
}
this.getRightBrace()
return material
}
/**
* read Mesh object
* @access private
* @returns {bool} -
*/
Mesh() {
this.getLeftBrace()
// vertices
const nVertices = this.getInteger()
const skinArray = []
for(let i=0; i<nVertices; i++){
const skin = new Skin()
const pos = new Vector3()
pos.x = this.getFloat()
pos.y = this.getFloat()
pos.z = -this.getFloat()
skin.position = pos
skin.boneIndex[0] = 0
skin.boneIndex[1] = -1
skin.boneIndex[2] = -1
skin.boneIndex[3] = -1
skin.skinWeight[0] = 1
skin.skinWeight[1] = 0
skin.skinWeight[2] = 0
skin.skinWeight[3] = 0
skinArray.push(skin)
this.getCommaOrSemicolon()
}
this._obj.skinArray = skinArray
this._obj.dynamicSkinOffset = -1
// faces
const nFaces = this.getInteger()
const faces = []
for(let i=0; i<nFaces; i++){
const face = this.getIntegerArray()
faces.push(face)
}
this._indices = faces
this.getRightBrace()
return true
}
/**
* read MeshMaterial[] object
* @access private
* @returns {bool} -
*/
MeshMaterialList() {
this.getLeftBrace()
// materials
const nMaterials = this.getInteger()
this._materialIndex = 0
const baseBone = new Bone()
const renderGroups = []
for(let i=0; i<nMaterials; i++){
const group = new RenderGroup()
group.boneArray = []
group.boneArray[0] = baseBone
renderGroups.push(group)
}
this._obj.renderGroupArray = renderGroups
this._obj.boneArray.push(baseBone)
this._obj.rootBone.addChild(baseBone)
// face materials
const nFaceIndices = this.getInteger()
const indices = this._indices
for(let i=0; i<nFaceIndices; i++){
const index = this.getInteger()
this.getCommaOrSemicolon()
const gind = renderGroups[index].indices
const ind = indices[i]
if(ind.length === 3){
gind.push(ind[0])
gind.push(ind[2])
gind.push(ind[1])
}else if(indices[i].length === 4){
gind.push(ind[0])
gind.push(ind[2])
gind.push(ind[1])
gind.push(ind[0])
gind.push(ind[3])
gind.push(ind[2])
}else{
// FIXME: 未対応
}
}
// materials
let material = null
let name = this.getWord()
while(name === 'Material'){
material = this.Material()
this._obj.materialArray.push(material)
this._obj.renderGroupArray[this._materialIndex].material = material
this._materialIndex++
name = this.getWord()
}
this.getRightBrace()
return true
}
/**
* read MeshNormals object
* @access private
* @returns {bool} -
*/
MeshNormals() {
this.getLeftBrace()
const nNormals = this.getInteger()
this._normalArray = []
for(let i=0; i<nNormals; i++){
const v = this.getVector3()
v.z = -v.z
this._normalArray.push(v)
}
const nFaceNormals = this.getInteger()
this._faceNormalArray = []
for(let i=0; i<nFaceNormals; i++){
const v = this.getIntegerArray()
this._faceNormalArray.push(v)
}
this.getRightBrace()
return true
}
/**
* read MeshTextureCoords object
* @access private
* @returns {bool} -
*/
MeshTextureCoords() {
this.getLeftBrace()
const skins = this._obj.skinArray
const nTextureCoords = this.getInteger()
for(let i=0; i<nTextureCoords; i++){
skins[i].textureUV = this.Coords2d()
}
this.getRightBrace()
return true
}
/**
* read MeshVertexColors object
* @access private
* @returns {bool} -
*/
MeshVertexColors() {
this.getLeftBrace()
const nVertexColors = this.getInteger()
for(let i=0; i<nVertexColors; i++){
const v = this.IndexedColor()
// FIXME: not implemented.
}
this.getRightBrace()
return true
}
/**
* read TextureFilename object
* @access private
* @returns {String} - texture file name
*/
TextureFilename() {
this.getLeftBrace()
let name = this.getFilename()
name = name.replace('\\\\', '/')
this.getRightBrace()
const texture = TextureBank.getTexture(this._parentDirName + name)
return texture
}
}