Add metallicRoughness and specularGlosiness output

This commit is contained in:
Sean Lilley 2017-05-03 17:59:24 -04:00
parent d6d0d392c6
commit c9ad66fcdb
10 changed files with 469 additions and 205 deletions

View File

@ -45,6 +45,9 @@ Using obj2gltf as a command-line tool:
|`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.|No, default `false`|
|`--checkTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.|No, default `false`|
|`--secure`|Prevent the converter from reading image or mtl files outside of the input obj directory.|No, default `false`|
|`--packOcclusion`|Pack the occlusion texture in the red channel of metallic-roughness texture.|No, default `false`|
|`--inputMetallicRoughness`|The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.|No, default `false`|
|`--inputSpecularGlossiness`|The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the `KHR_materials_pbrSpecularGlossiness` extension.|No, default `false`|
## Build Instructions

View File

@ -90,6 +90,21 @@ var argv = yargs
describe: 'Prevent the converter from reading image or mtl files outside of the input obj directory.',
type: 'boolean',
default: defaults.secure
},
packOcclusion : {
describe: 'Pack the occlusion texture in the red channel of metallic-roughness texture.',
type: 'boolean',
default: defaults.packOcclusion
},
inputMetallicRoughness : {
describe: 'The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.',
type: 'boolean',
default : defaults.metallicRoughness
},
inputSpecularGlossiness : {
describe: 'The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.',
type: 'boolean',
default : defaults.specularGlossiness
}
}).parse(args);
@ -113,7 +128,8 @@ var options = {
ao : argv.ao,
bypassPipeline : argv.bypassPipeline,
checkTransparency : argv.checkTransparency,
secure : argv.secure
secure : argv.secure,
packOcclusion : argv.packOcclusion
};
console.time('Total');
@ -123,5 +139,5 @@ obj2gltf(objPath, gltfPath, options)
console.timeEnd('Total');
})
.catch(function(error) {
console.log(error.message);
console.log(error);
});

View File

@ -3,6 +3,7 @@
module.exports = Material;
function Material() {
this.name = '';
this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka
this.emissiveColor = [0.0, 0.0, 0.0, 1.0]; // Ke
this.diffuseColor = [0.5, 0.5, 0.5, 1.0]; // Kd

View File

@ -4,8 +4,8 @@ var path = require('path');
var PNG = require('pngjs').PNG;
var Material = require('./Material');
var CesiumMath = Cesium.Math;
var defined = Cesium.defined;
var defaultValue = Cesium.defaultValue;
var WebGLConstants = Cesium.WebGLConstants;
module.exports = createGltf;
@ -15,6 +15,9 @@ module.exports = createGltf;
*
* @param {Object} objData Output of obj.js, containing an array of nodes containing geometry information, materials, and images.
* @param {Object} options An object with the following properties:
* @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture.
* @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.
* @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.
* @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log.
* @returns {Object} A glTF asset.
*
@ -81,7 +84,7 @@ function createGltf(objData, options) {
}
}
if (Object.keys(gltf.images).length > 0) {
if (gltf.images.length > 0) {
gltf.samplers.push({
magFilter : WebGLConstants.LINEAR,
minFilter : WebGLConstants.LINEAR,
@ -137,23 +140,27 @@ function addBuffers(gltf, bufferState) {
}
function getImage(images, imagePath) {
if (!defined(imagePath) || !defined(images[imagePath])) {
return undefined;
var imagesLength = images.length;
for (var i = 0; i < imagesLength; ++i) {
var image = images[i];
if (image.path === imagePath) {
return image;
}
}
return images[imagePath];
return undefined;
}
function getImageName(imagePath) {
return path.basename(imagePath, path.extname(imagePath));
function getImageName(image) {
return path.basename(image.path, image.extension);
}
function getTextureName(imagePath) {
return getImageName(imagePath);
function getTextureName(image) {
return getImageName(image);
}
function addTexture(gltf, image, imagePath) {
var imageName = getImageName(imagePath);
var textureName = getTextureName(imagePath);
function addTexture(gltf, image) {
var imageName = getImageName(image);
var textureName = getTextureName(image);
var imageIndex = gltf.images.length;
var textureIndex = gltf.textures.length;
@ -176,34 +183,28 @@ function addTexture(gltf, image, imagePath) {
return textureIndex;
}
function getTextureIndex(gltf, imagePath) {
var name = getTextureName(imagePath);
function getTexture(gltf, image) {
if (!defined(image)) {
return undefined;
}
var textureIndex;
var name = getTextureName(image);
var textures = gltf.textures;
var length = textures.length;
for (var i = 0; i < length; ++i) {
if (textures[i].name === name) {
return i;
textureIndex = i;
break;
}
}
}
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);
textureIndex = addTexture(gltf, image);
}
return textureIndex;
}
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);
@ -211,6 +212,17 @@ function addColors(left, right) {
return [red, green, blue];
}
function getEmissiveFactor(material) {
// If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0]
// Then add the ambient color to the emissive color to get the emissive factor.
var ambientColor = material.ambientColor;
var emissiveColor = material.emissiveColor;
if (ambientColor[0] === 1.0 && ambientColor[1] === 1.0 && ambientColor[2] === 1.0) {
ambientColor = [0.0, 0.0, 0.0, 1.0];
}
return addColors(ambientColor, emissiveColor);
}
function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, targetHeight) {
// Nearest neighbor sampling
var targetPixels = Buffer.alloc(targetWidth * targetHeight);
@ -230,25 +242,20 @@ function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, tar
return targetPixels;
}
var scratchColor = new Array(3);
function getGrayscaleChannel(image, targetWidth, targetHeight) {
function getImageChannel(image, index, targetWidth, targetHeight) {
var pixels = image.decoded; // RGBA
var width = image.width;
var height = image.height;
var pixelsLength = width * height;
var grayPixels = Buffer.alloc(pixelsLength);
var channel = 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);
var value = pixels.readUInt8(i * 4 + index);
channel.writeUInt8(value, i);
}
if (width !== targetWidth || height !== targetHeight) {
grayPixels = resizeChannel(grayPixels, width, height, targetWidth, targetHeight);
channel = resizeChannel(channel, width, height, targetWidth, targetHeight);
}
return grayPixels;
return channel;
}
function writeChannel(pixels, channel, index, width, height) {
@ -259,204 +266,392 @@ function writeChannel(pixels, channel, index, width, height) {
}
}
function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, options) {
if (!defined(metallicImage) && !defined(roughnessImage)) {
return undefined;
function getMinimumDimensions(images, options) {
var i;
var image;
var width = Number.POSITIVE_INFINITY;
var height = Number.POSITIVE_INFINITY;
var length = images.length;
for (i = 0; i < length; ++i) {
image = images[i];
if (defined(image)) {
width = Math.min(image.width, width);
height = Math.min(image.height, height);
}
}
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;
for (i = 0; i < length; ++i) {
image = images[i];
if (defined(image)) {
if (image.width !== width || image.height !== height) {
options.logger('Image ' + image.path + ' will be scaled from ' + image.width + 'x' + image.height + ' to ' + width + 'x' + height + '.');
}
}
}
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);
}
return [width, height];
}
function encodePng(pixels, width, height, inputChannels, outputChannels) {
var pngInput = {
data : pixels,
width : width,
height : height
};
// Constants defined by pngjs
var rgbColorType = 2;
var rgbaColorType = 4;
var colorType = outputChannels === 4 ? rgbaColorType : rgbColorType;
var inputColorType = inputChannels === 4 ? rgbaColorType : rgbColorType;
var inputHasAlpha = inputChannels === 4;
var pngOptions = {
width : width,
height : height,
colorType : 2, // RGB
inputHasAlpha : true
colorType : colorType,
inputColorType : inputColorType,
inputHasAlpha : inputHasAlpha
};
var encoded = PNG.sync.write(pngInput, pngOptions);
return PNG.sync.write(pngInput, pngOptions);
}
function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options) {
var packMetallic = defined(metallicImage);
var packRoughness = defined(roughnessImage);
var packOcclusion = defined(occlusionImage) && options.packOcclusion;
if (!packMetallic && !packRoughness) {
return undefined;
}
if (packMetallic && !defined(metallicImage.decoded)) {
options.logger('Could not get decoded image data for ' + metallicImage.path + '. The material will be created without a metallicRoughness texture.');
return undefined;
}
if (packRoughness && !defined(roughnessImage.decoded)) {
options.logger('Could not get decoded image data for ' + roughnessImage.path + '. The material will be created without a metallicRoughness texture.');
return undefined;
}
if (packOcclusion && !defined(occlusionImage.decoded)) {
options.logger('Could not get decoded image data for ' + occlusionImage.path + '. The occlusion texture will not be packed in the metallicRoughness texture.');
return undefined;
}
var dimensions = getMinimumDimensions([metallicImage, roughnessImage, occlusionImage], options);
var width = dimensions[0];
var height = dimensions[1];
var pixelsLength = width * height;
var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white
if (packMetallic) {
// Write into the B channel
var metallicChannel = getImageChannel(metallicImage, 0, width, height);
writeChannel(pixels, metallicChannel, 2, width, height);
}
if (packRoughness) {
// Write into the G channel
var roughnessChannel = getImageChannel(roughnessImage, 0, width, height);
writeChannel(pixels, roughnessChannel, 1, width, height);
}
if (packOcclusion) {
// Write into the R channel
var occlusionChannel = getImageChannel(occlusionImage, 0, width, height);
writeChannel(pixels, occlusionChannel, 0, width, height);
}
var imageName = materialName + '-' + 'MetallicRoughness';
if (packOcclusion) {
imageName += 'Occlusion';
}
var pngSource = encodePng(pixels, width, height, 4, 3);
var image = {
transparent : false,
source : encoded,
source : pngSource,
path : imageName,
extension : '.png'
};
var imageName = materialName + '-' + 'MetallicRoughness';
return addTexture(gltf, image, imageName);
return addTexture(gltf, image);
}
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);
function createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options) {
var packSpecular = defined(specularImage);
var packGlossiness = defined(glossinessImage);
// Emissive and ambient represent roughly the same concept, so chose whichever is defined.
emissiveTexture = defaultValue(emissiveTexture, ambientTexture);
if (!packSpecular && !packGlossiness) {
return undefined;
}
if (packSpecular && !defined(specularImage.decoded)) {
options.logger('Could not get decoded image data for ' + specularImage.path + '. The material will be created without a specularGlossiness texture.');
return undefined;
}
if (packGlossiness && !defined(glossinessImage.decoded)) {
options.logger('Could not get decoded image data for ' + glossinessImage.path + '. The material will be created without a specularGlossiness texture.');
return undefined;
}
var dimensions = getMinimumDimensions([specularImage, glossinessImage], options);
var width = dimensions[0];
var height = dimensions[1];
var pixelsLength = width * height;
var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white
if (packSpecular) {
// Write into the R, G, B channels
var redChannel = getImageChannel(specularImage, 0, width, height);
var greenChannel = getImageChannel(specularImage, 1, width, height);
var blueChannel = getImageChannel(specularImage, 2, width, height);
writeChannel(pixels, redChannel, 0, width, height);
writeChannel(pixels, greenChannel, 1, width, height);
writeChannel(pixels, blueChannel, 2, width, height);
}
if (packGlossiness) {
// Write into the A channel
var glossinessChannel = getImageChannel(glossinessImage, 0, width, height);
writeChannel(pixels, glossinessChannel, 3, width, height);
}
var imageName = materialName + '-' + 'SpecularGlossiness';
var pngSource = encodePng(pixels, width, height, 4, 4);
var image = {
transparent : false,
source : pngSource,
path : imageName,
extension : '.png'
};
return addTexture(gltf, image);
}
function createSpecularGlossinessMaterial(gltf, images, material, options) {
var materialName = material.name;
var emissiveImage = getImage(images, material.emissiveTexture);
var normalImage = getImage(images, material.normalTexture);
var occlusionImage = getImage(images, material.ambientTexture);
var diffuseImage = getImage(images, material.diffuseTexture);
var specularImage = getImage(images, material.specularTexture);
var glossinessImage = getImage(images, material.specularShininessTexture);
var emissiveTexture = getTexture(gltf, emissiveImage);
var normalTexture = getTexture(gltf, normalImage);
var occlusionTexture = getTexture(gltf, occlusionImage);
var diffuseTexture = getTexture(gltf, diffuseImage);
var specularGlossinessTexture = createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options);
var emissiveFactor = getEmissiveFactor(material);
var diffuseFactor = material.diffuseColor;
var specularFactor = material.specularColor;
var glossinessFactor = material.specularShininess;
if (defined(emissiveTexture)) {
emissiveFactor = [1.0, 1.0, 1.0];
}
if (defined(diffuseTexture)) {
diffuseFactor = [1.0, 1.0, 1.0, 1.0];
}
if (defined(specularImage)) {
specularFactor = 1.0;
}
if (defined(glossinessImage)) {
glossinessFactor = 1.0;
}
var alpha = material.alpha;
diffuseFactor[3] = alpha;
var transparent = alpha < 1.0;
if (defined(diffuseImage)) {
transparent |= diffuseImage.transparent;
}
var doubleSided = transparent;
var alphaMode = transparent ? 'BLEND' : 'OPAQUE';
var gltfMaterial = {
name : materialName,
extensions : {
KHR_materials_pbrSpecularGlossiness: {
diffuseTexture : diffuseTexture,
specularGlossinessTexture : specularGlossinessTexture,
diffuseFactor : diffuseFactor,
specularFactor : specularFactor,
glossinessFactor : glossinessFactor
}
},
emissiveTexture : emissiveTexture,
normalTexture : normalTexture,
occlusionTexture : occlusionTexture,
emissiveFactor : emissiveFactor,
alphaMode : alphaMode,
doubleSided : doubleSided
};
return gltfMaterial;
}
function createMetallicRoughnessMaterial(gltf, images, material, options) {
var materialName = material.name;
var emissiveImage = getImage(images, material.emissiveTexture);
var normalImage = getImage(images, material.normalTexture);
var occlusionImage = getImage(images, material.ambientTexture);
var baseColorImage = getImage(images, material.diffuseTexture);
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];
var emissiveTexture = getTexture(gltf, emissiveImage);
var normalTexture = getTexture(gltf, normalImage);
var baseColorTexture = getTexture(gltf, baseColorImage);
var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options);
if (!defined(baseColorTexture)) {
baseColorFactor = material.diffuseColor;
var packOcclusion = defined(occlusionImage) || options.packOcclusion;
var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage);
var emissiveFactor = getEmissiveFactor(material);
var baseColorFactor = material.diffuseColor;
var metallicFactor = material.specularColor[0];
var roughnessFactor = material.specularShininess;
if (defined(emissiveTexture)) {
emissiveFactor = [1.0, 1.0, 1.0];
}
if (!defined(metallicImage)) {
metallicFactor = luminance(material.specularColor);
if (defined(baseColorTexture)) {
baseColorFactor = [1.0, 1.0, 1.0, 1.0];
}
if (!defined(roughnessImage)) {
var specularShininess = material.specularShininess;
if (specularShininess > 1.0) {
specularShininess /= 1000.0;
}
roughnessFactor = specularShininess;
if (defined(metallicImage)) {
metallicFactor = 1.0;
}
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);
if (defined(roughnessImage)) {
roughnessFactor = 1.0;
}
var alpha = material.alpha;
baseColorFactor[3] = alpha;
var transparent = alpha < 1.0;
if (defined(material.diffuseTexture)) {
transparent |= images[material.diffuseTexture].transparent;
if (defined(baseColorImage)) {
transparent |= baseColorImage.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,
name : materialName,
pbrMetallicRoughness : {
baseColorTexture : baseColorTexture,
metallicRoughnessTexture : metallicRoughnessTexture,
baseColorFactor : baseColorFactor,
metallicFactor : metallicFactor,
roughnessFactor : roughnessFactor,
metallicRoughnessTexture : metallicRoughnessTexture
roughnessFactor : roughnessFactor
},
normalTexture : normalTexture,
emissiveTexture : emissiveTexture,
normalTexture : normalTexture,
occlusionTexture : occlusionTexture,
emissiveFactor : emissiveFactor,
alphaMode : alphaMode,
doubleSided : doubleSided,
extras : {
_obj2gltf : {
hasNormals : hasNormals
}
}
doubleSided : doubleSided
};
return gltfMaterial;
}
function convertTraditionalToMetallicRoughness(material) {
// Translate the blinn-phong model to the pbr metallic-roughness model
// Roughness factor is a combination of specular intensity and shininess
// Metallic factor is 0.0
var specularIntensity = material.specularColor[0];
var specularShininess = material.specularShininess;
// Transform from 0-1000 range to 0-1 range. Then invert.
var roughnessFactor = specularShininess;
roughnessFactor = roughnessFactor / 1000.0;
roughnessFactor = 1.0 - roughnessFactor;
roughnessFactor = CesiumMath.clamp(roughnessFactor, 0.0, 1.0);
// Low specular intensity values should produce a rough material even if shininess is high.
if (specularIntensity < 0.1) {
roughnessFactor *= specularIntensity;
}
var metallicFactor = 0.0;
material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0];
material.specularShiness = roughnessFactor;
}
function addMaterial(gltf, images, material, options) {
var gltfMaterial;
if (options.inputSpecularGlossiness) {
gltfMaterial = createSpecularGlossinessMaterial(gltf, images, material, options);
} else if (options.inputMetallicRoughness) {
gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options);
} else {
convertTraditionalToMetallicRoughness(material);
gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options);
}
var materialIndex = gltf.materials.length;
gltf.materials.push(gltfMaterial);
return materialIndex;
}
function getMaterialIndex(gltf, name) {
function getMaterialIndex(gltf, materialName) {
var materials = gltf.materials;
var length = materials.length;
for (var i = 0; i < length; ++i) {
if (materials[i].name === name) {
if (materials[i].name === materialName) {
return i;
}
}
return undefined;
}
function getMaterial(gltf, materials, images, materialName, hasNormals, options) {
function getMaterial(gltf, materials, images, materialName, 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);
var material;
var materialsLength = materials.length;
for (var i = 0; i < materialsLength; ++i) {
if (materials[i].name === materialName) {
material = materials[i];
}
}
if (!defined(material)) {
material = new Material();
material.name = materialName;
}
var materialIndex = getMaterialIndex(gltf, materialName);
if (!defined(materialIndex)) {
materialIndex = addMaterial(gltf, images, material, materialName, hasNormals, options);
materialIndex = addMaterial(gltf, images, material, options);
}
return materialIndex;
@ -555,7 +750,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti
var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices);
primitive.indices = undefined; // Unload resources
var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options);
var materialIndex = getMaterial(gltf, materials, images, primitive.material, options);
gltfPrimitives.push({
attributes : attributes,

View File

@ -36,6 +36,7 @@ function loadImage(imagePath, options) {
transparent : false,
source : data,
extension : extension,
path : imagePath,
decoded : undefined,
width : undefined,
height : undefined

View File

@ -18,14 +18,15 @@ function loadMtl(mtlPath) {
var values;
var value;
var mtlDirectory = path.dirname(mtlPath);
var materials = {};
var materials = [];
function parseLine(line) {
line = line.trim();
if (/^newmtl /i.test(line)) {
var name = line.substring(7).trim();
material = new Material();
materials[name] = material;
material.name = name;
materials.push(material);
} else if (/^Ka /i.test(line)) {
values = line.substring(3).trim().split(' ');
material.ambientColor = [

View File

@ -307,7 +307,7 @@ function loadMaterials(mtlPaths, objPath, options) {
var secure = options.secure;
var logger = options.logger;
var objDirectory = path.dirname(objPath);
var materials = {};
var materials = [];
return Promise.map(mtlPaths, function(mtlPath) {
mtlPath = path.resolve(objDirectory, mtlPath);
if (secure && outsideDirectory(mtlPath, objPath)) {
@ -316,19 +316,21 @@ function loadMaterials(mtlPaths, objPath, options) {
}
return loadMtl(mtlPath)
.then(function(materialsInMtl) {
materials = Object.assign(materials, materialsInMtl);
materials = materials.concat(materialsInMtl);
})
.catch(function() {
logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.');
});
}, {concurrency : 10})
.thenReturn(materials);
.then(function() {
return materials;
});
}
function loadImages(imagesOptions, objPath, options) {
var secure = options.secure;
var logger = options.logger;
var images = {};
var images = [];
return Promise.map(imagesOptions, function(imageOptions) {
var imagePath = imageOptions.imagePath;
if (secure && outsideDirectory(imagePath, objPath)) {
@ -337,53 +339,54 @@ function loadImages(imagesOptions, objPath, options) {
}
return loadImage(imagePath, imageOptions)
.then(function(image) {
images[imagePath] = image;
images.push(image);
})
.catch(function() {
logger('Could not read image file at ' + imagePath + '. Material will ignore this image.');
});
}, {concurrency : 10})
.thenReturn(images);
.then(function() {
return images;
});
}
function getImagesOptions(materials, options) {
var imagesOptions = [];
for (var name in materials) {
if (materials.hasOwnProperty(name)) {
var material = materials[name];
if (defined(material.ambientTexture)) {
imagesOptions.push({
imagePath : material.ambientTexture
});
}
if (defined(material.emissiveTexture)) {
imagesOptions.push({
imagePath : material.emissiveTexture
});
}
if (defined(material.diffuseTexture)) {
imagesOptions.push({
imagePath : material.diffuseTexture,
checkTransparency : options.checkTransparency
});
}
if (defined(material.specularTexture)) {
imagesOptions.push({
imagePath : material.specularTexture,
decode : true
});
}
if (defined(material.specularShininessTexture)) {
imagesOptions.push({
imagePath : material.specularShininessTexture,
decode : true
});
}
if (defined(material.normalTexture)) {
imagesOptions.push({
imagePath : material.normalTexture
});
}
var materialsLength = materials.length;
for (var i = 0; i < materialsLength; ++i) {
var material = materials[i];
if (defined(material.ambientTexture)) {
imagesOptions.push({
imagePath : material.ambientTexture
});
}
if (defined(material.emissiveTexture)) {
imagesOptions.push({
imagePath : material.emissiveTexture
});
}
if (defined(material.diffuseTexture)) {
imagesOptions.push({
imagePath : material.diffuseTexture,
checkTransparency : options.checkTransparency
});
}
if (defined(material.specularTexture)) {
imagesOptions.push({
imagePath : material.specularTexture,
decode : true
});
}
if (defined(material.specularShininessTexture)) {
imagesOptions.push({
imagePath : material.specularShininessTexture,
decode : true
});
}
if (defined(material.normalTexture)) {
imagesOptions.push({
imagePath : material.normalTexture
});
}
}
return imagesOptions;

View File

@ -35,6 +35,9 @@ module.exports = obj2gltf;
* @param {Boolean} [options.bypassPipeline=false] Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.
* @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel.
* @param {Boolean} [options.secure=false] Prevent the converter from reading image or mtl files outside of the input obj directory.
* @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture.
* @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.
* @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.
* @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log.
*/
function obj2gltf(objPath, gltfPath, options) {
@ -53,15 +56,20 @@ function obj2gltf(objPath, gltfPath, options) {
var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline);
var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency);
var secure = defaultValue(options.secure, defaults.secure);
var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion);
var inputMetallicRoughness = defaultValue(options.inputMetallicRoughness, defaults.inputMetallicRoughness);
var inputSpecularGlossiness = defaultValue(options.inputSpecularGlossiness, defaults.inputSpecularGlossiness);
var logger = defaultValue(options.logger, defaults.logger);
options.separate = separate;
options.separateTextures = separateTextures;
options.checkTransparency = checkTransparency;
options.secure = secure;
options.packOcclusion = packOcclusion;
options.inputMetallicRoughness = inputMetallicRoughness;
options.inputSpecularGlossiness = inputSpecularGlossiness;
options.logger = logger;
if (!defined(objPath)) {
throw new DeveloperError('objPath is required');
}
@ -81,6 +89,10 @@ function obj2gltf(objPath, gltfPath, options) {
throw new DeveloperError('--bypassPipeline does not convert to binary glTF');
}
if (inputMetallicRoughness && inputSpecularGlossiness) {
throw new DeveloperError('--inputMetallicRoughness and --inputSpecularGlossiness cannot both be set.');
}
gltfPath = path.join(path.dirname(gltfPath), modelName + extension);
var aoOptions = ao ? {} : undefined;
@ -201,6 +213,25 @@ obj2gltf.defaults = {
* @default false
*/
secure: false,
/**
* Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture.
* @type Boolean
* @default false
*/
packOcclusion: false,
/**
* The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.
* @type Boolean
* @default false
*/
inputMetallicRoughness: false,
/**
* The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.
* @type Boolean
* @default false
*/
inputSpecularGlossiness: false,
/**
* @private
*/

View File

@ -61,6 +61,7 @@ function writeUris(gltf, gltfPath, options) {
return Promise.all(promises)
.then(function() {
deleteExtras(gltf);
cleanup(gltf);
return gltf;
});
}
@ -82,6 +83,18 @@ function deleteExtras(gltf) {
}
}
function cleanup(gltf) {
// Remove empty arrays from top-level items
for (var key in gltf) {
if (gltf.hasOwnProperty(key)) {
var property = gltf[key];
if (Array.isArray(property) && property.length === 0) {
delete gltf[key];
}
}
}
}
function writeSeparateBuffer(gltf, gltfPath) {
var buffer = gltf.buffers[0];
var source = buffer.extras._obj2gltf.source;

View File

@ -33,7 +33,7 @@
"gltf-pipeline": "^0.1.0-alpha11",
"jpeg-js": "^0.2.0",
"mime": "^1.3.4",
"pngjs": "^3.0.1",
"pngjs": "^3.2.0",
"yargs": "^7.0.1"
},
"devDependencies": {