mirror of
https://github.com/CesiumGS/obj2gltf.git
synced 2024-11-23 08:34:14 -05:00
Add metallicRoughness and specularGlosiness output
This commit is contained in:
parent
d6d0d392c6
commit
c9ad66fcdb
@ -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
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -36,6 +36,7 @@ function loadImage(imagePath, options) {
|
||||
transparent : false,
|
||||
source : data,
|
||||
extension : extension,
|
||||
path : imagePath,
|
||||
decoded : undefined,
|
||||
width : undefined,
|
||||
height : undefined
|
||||
|
@ -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 = [
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user