obj2gltf/lib/createGltf.js

599 lines
19 KiB
JavaScript
Raw Normal View History

2017-03-13 15:28:51 -04:00
'use strict';
2016-07-22 14:09:13 -04:00
var Cesium = require('cesium');
2015-10-16 17:32:23 -04:00
var path = require('path');
2017-04-18 11:56:08 -04:00
var PNG = require('pngjs').PNG;
2017-04-10 17:57:56 -04:00
var Material = require('./Material');
2016-07-22 14:09:13 -04:00
2016-06-09 13:33:08 -04:00
var defined = Cesium.defined;
var defaultValue = Cesium.defaultValue;
var WebGLConstants = Cesium.WebGLConstants;
2015-10-16 17:32:23 -04:00
module.exports = createGltf;
2017-03-13 15:28:51 -04:00
/**
* Create a glTF from obj data.
*
* @param {Object} objData Output of obj.js, containing an array of nodes containing geometry information, materials, and images.
2017-04-18 11:56:08 -04:00
* @param {Object} options An object with the following properties:
* @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log.
* @returns {Object} A glTF asset.
2017-03-13 15:28:51 -04:00
*
* @private
*/
2017-04-18 11:56:08 -04:00
function createGltf(objData, options) {
2017-03-13 15:28:51 -04:00
var nodes = objData.nodes;
var materials = objData.materials;
var images = objData.images;
2015-10-16 17:32:23 -04:00
2016-06-09 13:33:08 -04:00
var gltf = {
2017-04-18 11:56:08 -04:00
accessors : [],
2016-06-09 13:33:08 -04:00
asset : {},
2017-04-18 11:56:08 -04:00
buffers : [],
bufferViews : [],
images : [],
materials : [],
meshes : [],
nodes : [],
samplers : [],
scene : 0,
scenes : [],
textures : []
2016-06-09 13:33:08 -04:00
};
gltf.asset = {
2017-03-13 15:28:51 -04:00
generator : 'obj2gltf',
2017-04-18 11:56:08 -04:00
version: '2.0'
2016-06-09 13:33:08 -04:00
};
2017-04-18 11:56:08 -04:00
gltf.scenes.push({
2017-03-13 15:28:51 -04:00
nodes : []
2017-04-18 11:56:08 -04:00
});
var bufferState = {
vertexBuffers : [],
vertexBufferByteOffset : 0,
vertexBufferViewIndex : 0,
indexBuffers : [],
indexBufferByteOffset : 0,
indexBufferViewIndex : 1
2016-06-09 13:33:08 -04:00
};
2017-04-18 11:56:08 -04:00
var uint32Indices = requiresUint32Indices(nodes);
2016-06-09 13:33:08 -04:00
2017-04-18 11:56:08 -04:00
var nodesLength = nodes.length;
for (var i = 0; i < nodesLength; ++i) {
var node = nodes[i];
var meshes = node.meshes;
var meshesLength = meshes.length;
var meshIndex;
2017-03-13 15:28:51 -04:00
2017-04-18 11:56:08 -04:00
if (meshesLength === 1) {
meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, meshes[0], options);
addNode(gltf, node.name, meshIndex);
} else {
// Add meshes as child nodes
var parentIndex = addNode(gltf, node.name);
for (var j = 0; j < meshesLength; ++j) {
var mesh = meshes[j];
meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options);
addNode(gltf, mesh.name, meshIndex, parentIndex);
2017-03-13 15:28:51 -04:00
}
2017-04-18 11:56:08 -04:00
}
2016-06-09 13:33:08 -04:00
}
2015-10-16 17:32:23 -04:00
2017-04-18 11:56:08 -04:00
if (Object.keys(gltf.images).length > 0) {
gltf.samplers.push({
2017-03-13 15:28:51 -04:00
magFilter : WebGLConstants.LINEAR,
minFilter : WebGLConstants.LINEAR,
wrapS : WebGLConstants.REPEAT,
wrapT : WebGLConstants.REPEAT
2017-04-18 11:56:08 -04:00
});
2017-03-13 15:28:51 -04:00
}
2017-04-18 11:56:08 -04:00
addBuffers(gltf, bufferState);
return gltf;
}
function addBuffers(gltf, bufferState) {
var bufferName = 'buffer';
var vertexBufferViewName = 'bufferView_vertex';
var indexBufferViewName = 'bufferView_index';
var vertexBuffers = bufferState.vertexBuffers;
var indexBuffers = bufferState.indexBuffers;
var vertexBufferByteLength = bufferState.vertexBufferByteOffset;
var indexBufferByteLength = bufferState.indexBufferByteOffset;
var buffers = [];
buffers = buffers.concat(vertexBuffers, indexBuffers);
var buffer = Buffer.concat(buffers);
gltf.buffers.push({
name : bufferName,
byteLength : buffer.byteLength,
extras : {
_obj2gltf : {
source : buffer
2017-03-13 15:28:51 -04:00
}
2015-10-16 17:32:23 -04:00
}
2017-04-18 11:56:08 -04:00
});
gltf.bufferViews.push({
name : vertexBufferViewName,
buffer : 0,
byteLength : vertexBufferByteLength,
byteOffset : 0,
target : WebGLConstants.ARRAY_BUFFER
});
gltf.bufferViews.push({
name : indexBufferViewName,
buffer : 0,
byteLength : indexBufferByteLength,
byteOffset : vertexBufferByteLength,
target : WebGLConstants.ELEMENT_ARRAY_BUFFER
});
}
function getImage(images, imagePath) {
if (!defined(imagePath) || !defined(images[imagePath])) {
return undefined;
2016-06-09 13:33:08 -04:00
}
2017-04-18 11:56:08 -04:00
return images[imagePath];
}
2015-10-16 17:32:23 -04:00
2017-04-18 11:56:08 -04:00
function getImageName(imagePath) {
return path.basename(imagePath, path.extname(imagePath));
}
2017-03-13 15:28:51 -04:00
2017-04-18 11:56:08 -04:00
function getTextureName(imagePath) {
return getImageName(imagePath);
}
2017-03-13 15:28:51 -04:00
2017-04-18 11:56:08 -04:00
function addTexture(gltf, image, imagePath) {
var imageName = getImageName(imagePath);
var textureName = getTextureName(imagePath);
var imageIndex = gltf.images.length;
var textureIndex = gltf.textures.length;
2016-06-09 13:33:08 -04:00
2017-04-18 11:56:08 -04:00
gltf.images.push({
name : imageName,
extras : {
_obj2gltf : {
source : image.source,
extension : image.extension
2017-03-13 15:28:51 -04:00
}
2016-06-09 13:33:08 -04:00
}
2017-04-18 11:56:08 -04:00
});
gltf.textures.push({
name : textureName,
sampler : 0,
source : imageIndex
});
return textureIndex;
}
function getTextureIndex(gltf, imagePath) {
var name = getTextureName(imagePath);
var textures = gltf.textures;
var length = textures.length;
for (var i = 0; i < length; ++i) {
if (textures[i].name === name) {
return i;
}
2016-06-09 13:33:08 -04:00
}
2017-04-18 11:56:08 -04:00
}
2016-06-09 13:33:08 -04:00
2017-04-18 11:56:08 -04:00
function getTexture(gltf, images, imagePath) {
var image = getImage(images, imagePath);
if (!defined(image)) {
return undefined;
}
var textureIndex = getTextureIndex(gltf, imagePath);
if (!defined(textureIndex)) {
textureIndex = addTexture(gltf, image, imagePath);
}
return textureIndex;
}
2017-03-13 15:28:51 -04:00
2017-04-18 11:56:08 -04:00
function luminance(color) {
var value = 0.2125 * color[0] + 0.7154 * color[1] + 0.0721 * color[2];
return Math.min(value, 1.0); // Clamp just to handle edge cases
}
function addColors(left, right) {
var red = Math.min(left[0] + right[0], 1.0);
var green = Math.min(left[1] + right[1], 1.0);
var blue = Math.min(left[2] + right[2], 1.0);
return [red, green, blue];
}
function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, targetHeight) {
// Nearest neighbor sampling
var targetPixels = Buffer.alloc(targetWidth * targetHeight);
var widthRatio = sourceWidth / targetWidth;
var heightRatio = sourceHeight / targetHeight;
for (var y = 0; y < targetHeight; ++y) {
for (var x = 0; x < targetWidth; ++x) {
var targetIndex = y * targetWidth + x;
var sourceY = Math.round(y * heightRatio);
var sourceX = Math.round(x * widthRatio);
var sourceIndex = sourceY * sourceWidth + sourceX;
var sourceValue = sourcePixels.readUInt8(sourceIndex);
targetPixels.writeUInt8(sourceValue, targetIndex);
}
}
return targetPixels;
}
var scratchColor = new Array(3);
function getGrayscaleChannel(image, targetWidth, targetHeight) {
var pixels = image.decoded; // RGBA
var width = image.width;
var height = image.height;
var pixelsLength = width * height;
var grayPixels = Buffer.alloc(pixelsLength);
for (var i = 0; i < pixelsLength; ++i) {
scratchColor[0] = pixels.readUInt8(i * 4);
scratchColor[1] = pixels.readUInt8(i * 4 + 1);
scratchColor[2] = pixels.readUInt8(i * 4 + 2);
var value = luminance(scratchColor) * 255;
grayPixels.writeUInt8(value, i);
}
if (width !== targetWidth || height !== targetHeight) {
grayPixels = resizeChannel(grayPixels, width, height, targetWidth, targetHeight);
}
return grayPixels;
}
function writeChannel(pixels, channel, index, width, height) {
var pixelsLength = width * height;
for (var i = 0; i < pixelsLength; ++i) {
var value = channel.readUInt8(i);
pixels.writeUInt8(value, i * 4 + index);
}
}
function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, options) {
if (!defined(metallicImage) && !defined(roughnessImage)) {
return undefined;
}
if (defined(metallicImage) && !defined(metallicImage.decoded)) {
options.logger('Could not get decoded image data for ' + metallicImage + '. The material will be created without a metallicRoughness texture.');
return undefined;
}
if (defined(roughnessImage) && !defined(roughnessImage.decoded)) {
options.logger('Could not get decoded image data for ' + roughnessImage + '. The material will be created without a metallicRoughness texture.');
return undefined;
}
var width;
var height;
if (defined(metallicImage) && defined(roughnessImage)) {
width = Math.min(metallicImage.width, roughnessImage.width);
height = Math.min(metallicImage.height, roughnessImage.height);
} else if (defined(metallicImage)) {
width = metallicImage.width;
height = metallicImage.height;
} else if (defined(roughnessImage)) {
width = roughnessImage.width;
height = roughnessImage.height;
}
var pixelsLength = width * height;
var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white
if (defined(metallicImage)) {
// Write into the B channel
var metallicChannel = getGrayscaleChannel(metallicImage, width, height);
writeChannel(pixels, metallicChannel, 2, width, height);
}
if (defined(roughnessImage)) {
// Write into the G channel
var roughnessChannel = getGrayscaleChannel(roughnessImage, width, height);
writeChannel(pixels, roughnessChannel, 1, width, height);
}
var pngInput = {
data : pixels,
width : width,
height : height
};
var pngOptions = {
width : width,
height : height,
colorType : 2, // RGB
inputHasAlpha : true
};
var encoded = PNG.sync.write(pngInput, pngOptions);
var image = {
transparent : false,
source : encoded,
extension : '.png'
};
var imageName = materialName + '-' + 'MetallicRoughness';
return addTexture(gltf, image, imageName);
}
function addMaterial(gltf, images, material, name, hasNormals, options) {
// Translate the traditional diffuse/specular material to pbr metallic roughness.
// Specular intensity is extracted from the specular color and treated as the metallic factor.
// Specular shininess is typically an exponent from 0 to 1000, and is converted to a 0-1 range as the roughness factor.
var ambientTexture = getTexture(gltf, images, material.ambientTexture);
var emissiveTexture = getTexture(gltf, images, material.emissiveTexture);
var baseColorTexture = getTexture(gltf, images, material.diffuseTexture);
var normalTexture = getTexture(gltf, images, material.normalTexture);
// Emissive and ambient represent roughly the same concept, so chose whichever is defined.
emissiveTexture = defaultValue(emissiveTexture, ambientTexture);
var metallicImage = getImage(images, material.specularTexture);
var roughnessImage = getImage(images, material.specularShininessTexture);
var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, name, metallicImage, roughnessImage, options);
var baseColorFactor = [1.0, 1.0, 1.0, 1.0];
var metallicFactor = 1.0;
var roughnessFactor = 1.0;
var emissiveFactor = [1.0, 1.0, 1.0];
if (!defined(baseColorTexture)) {
baseColorFactor = material.diffuseColor;
}
if (!defined(metallicImage)) {
metallicFactor = luminance(material.specularColor);
}
if (!defined(roughnessImage)) {
var specularShininess = material.specularShininess;
if (specularShininess > 1.0) {
specularShininess /= 1000.0;
}
roughnessFactor = specularShininess;
}
if (!defined(emissiveTexture)) {
// If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0]
var ambientColor = material.ambientColor;
if (ambientColor[0] === 1.0 && ambientColor[1] === 1.0 && ambientColor[2] === 1.0) {
ambientColor = [0.0, 0.0, 0.0, 1.0];
}
emissiveFactor = addColors(material.emissiveColor, ambientColor);
}
var alpha = material.alpha;
baseColorFactor[3] = alpha;
var transparent = alpha < 1.0;
if (defined(material.diffuseTexture)) {
transparent |= images[material.diffuseTexture].transparent;
}
var doubleSided = transparent;
var alphaMode = transparent ? 'BLEND' : 'OPAQUE';
if (!hasNormals) {
// TODO : what is the lighting like for models that don't have normals? Can pbrMetallicRoughness just be undefined? Is setting the baseColor to black a good approach here?
emissiveTexture = baseColorTexture;
emissiveFactor = baseColorFactor.slice(0, 3);
baseColorTexture = undefined;
baseColorFactor = [0.0, 0.0, 0.0, baseColorFactor[3]];
metallicRoughnessTexture = undefined;
metallicFactor = 0.0;
roughnessFactor = 0.0;
normalTexture = undefined;
}
var gltfMaterial = {
name : name,
pbrMetallicRoughness : {
baseColorTexture : baseColorTexture,
baseColorFactor : baseColorFactor,
metallicFactor : metallicFactor,
roughnessFactor : roughnessFactor,
metallicRoughnessTexture : metallicRoughnessTexture
},
normalTexture : normalTexture,
emissiveTexture : emissiveTexture,
emissiveFactor : emissiveFactor,
alphaMode : alphaMode,
doubleSided : doubleSided,
extras : {
_obj2gltf : {
2017-04-18 11:56:08 -04:00
hasNormals : hasNormals
}
}
2017-03-13 15:28:51 -04:00
};
2017-04-18 11:56:08 -04:00
var materialIndex = gltf.materials.length;
gltf.materials.push(gltfMaterial);
return materialIndex;
}
function getMaterialIndex(gltf, name) {
var materials = gltf.materials;
var length = materials.length;
for (var i = 0; i < length; ++i) {
if (materials[i].name === name) {
return i;
}
}
return undefined;
}
function getMaterial(gltf, materials, images, materialName, hasNormals, options) {
if (!defined(materialName)) {
// Create a default material if the primitive does not specify one
materialName = 'default';
}
var material = materials[materialName];
material = defined(material) ? material : new Material();
var materialIndex = getMaterialIndex(gltf, materialName);
// Check if this material has already been added but with incompatible shading
if (defined(materialIndex)) {
var gltfMaterial = gltf.materials[materialIndex];
var normalShading = gltfMaterial.extras._obj2gltf.hasNormals;
if (hasNormals !== normalShading) {
materialName += (hasNormals ? '_shaded' : '_constant');
materialIndex = getMaterialIndex(gltf, materialName);
}
}
if (!defined(materialIndex)) {
materialIndex = addMaterial(gltf, images, material, materialName, hasNormals, options);
}
return materialIndex;
}
function addVertexAttribute(gltf, bufferState, array, components) {
var buffer = array.toFloatBuffer();
var count = array.length / components;
var minMax = array.getMinMax(components);
var type = (components === 3 ? 'VEC3' : 'VEC2');
var accessor = {
bufferView : bufferState.vertexBufferViewIndex,
byteOffset : bufferState.vertexBufferByteOffset,
componentType : WebGLConstants.FLOAT,
count : count,
min : minMax.min,
max : minMax.max,
type : type
2017-03-13 15:28:51 -04:00
};
2017-04-18 11:56:08 -04:00
bufferState.vertexBufferByteOffset += buffer.length;
bufferState.vertexBuffers.push(buffer);
var accessorIndex = gltf.accessors.length;
gltf.accessors.push(accessor);
return accessorIndex;
}
function addIndexArray(gltf, bufferState, array, uint32Indices) {
var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer();
var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT;
var count = array.length;
var minMax = array.getMinMax(1);
var accessor = {
bufferView : bufferState.indexBufferViewIndex,
byteOffset : bufferState.indexBufferByteOffset,
componentType : componentType,
count : count,
min : minMax.min,
max : minMax.max,
type : 'SCALAR'
2017-03-13 15:28:51 -04:00
};
2017-04-18 11:56:08 -04:00
bufferState.indexBufferByteOffset += buffer.length;
bufferState.indexBuffers.push(buffer);
var accessorIndex = gltf.accessors.length;
gltf.accessors.push(accessor);
return accessorIndex;
}
function requiresUint32Indices(nodes) {
var nodesLength = nodes.length;
for (var i = 0; i < nodesLength; ++i) {
var meshes = nodes[i].meshes;
var meshesLength = meshes.length;
for (var j = 0; j < meshesLength; ++j) {
// Reserve the 65535 index for primitive restart
var vertexCount = meshes[j].positions.length / 3;
if (vertexCount > 65534) {
return true;
}
}
}
return false;
}
function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options) {
var hasPositions = mesh.positions.length > 0;
var hasNormals = mesh.normals.length > 0;
var hasUVs = mesh.uvs.length > 0;
var attributes = {};
if (hasPositions) {
attributes.POSITION = addVertexAttribute(gltf, bufferState, mesh.positions, 3);
}
if (hasNormals) {
attributes.NORMAL = addVertexAttribute(gltf, bufferState, mesh.normals, 3);
}
if (hasUVs) {
attributes.TEXCOORD_0 = addVertexAttribute(gltf, bufferState, mesh.uvs, 2);
}
// Unload resources
mesh.positions = undefined;
mesh.normals = undefined;
mesh.uvs = undefined;
var gltfPrimitives = [];
var primitives = mesh.primitives;
var primitivesLength = primitives.length;
for (var i = 0; i < primitivesLength; ++i) {
var primitive = primitives[i];
var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices);
primitive.indices = undefined; // Unload resources
var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options);
gltfPrimitives.push({
attributes : attributes,
indices : indexAccessorIndex,
material : materialIndex,
mode : WebGLConstants.TRIANGLES
});
}
var gltfMesh = {
name : mesh.name,
primitives : gltfPrimitives
};
var meshIndex = gltf.meshes.length;
gltf.meshes.push(gltfMesh);
return meshIndex;
}
function addNode(gltf, name, meshIndex, parentIndex) {
var node = {
name : name,
mesh : meshIndex
};
var nodeIndex = gltf.nodes.length;
gltf.nodes.push(node);
if (defined(parentIndex)) {
var parentNode = gltf.nodes[parentIndex];
if (!defined(parentNode.children)) {
parentNode.children = [];
}
parentNode.children.push(nodeIndex);
} else {
gltf.scenes[gltf.scene].nodes.push(nodeIndex);
}
return nodeIndex;
2015-10-16 17:32:23 -04:00
}