diff --git a/README.md b/README.md index ebe0f0ad..3194a880 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Content pipeline tools for optimizing [glTF](https://www.khronos.org/gltf) asset Supports common operations including: * Converting glTF to glb (and reverse) * Saving buffers/textures as embedded or separate files -* Converting glTF 1.0 models to glTF 2.0 (using the [KHR_techniques_webgl](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_techniques_webgl) and [EXT_blend](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/EXT_blend) extensions) +* Converting glTF 1.0 models to glTF 2.0 (using the [KHR_techniques_webgl](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_techniques_webgl) and [KHR_blend](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_blend) extensions) `gltf-pipeline` can be used as a command-line tool or Node.js module. diff --git a/lib/moveTechniqueRenderStates.js b/lib/moveTechniqueRenderStates.js new file mode 100644 index 00000000..cfe2b230 --- /dev/null +++ b/lib/moveTechniqueRenderStates.js @@ -0,0 +1,133 @@ +'use strict'; + +var Cesium = require('cesium'); +var addExtensionsUsed = require('./addExtensionsUsed'); +var ForEach = require('./ForEach'); + +var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; +var WebGLConstants = Cesium.WebGLConstants; + +module.exports = moveTechniqueRenderStates; + +var defaultBlendEquation = [ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD +]; + +var defaultBlendFactors = [ + WebGLConstants.ONE, + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.ZERO +]; + +function isStateEnabled(renderStates, state) { + var enabled = renderStates.enable; + if (!defined(enabled)) { + return false; + } + + return (enabled.indexOf(state) > -1); +} + +var supportedBlendFactors = [ + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.SRC_COLOR, + WebGLConstants.ONE_MINUS_SRC_COLOR, + WebGLConstants.SRC_ALPHA, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.DST_ALPHA, + WebGLConstants.ONE_MINUS_DST_ALPHA, + WebGLConstants.DST_COLOR, + WebGLConstants.ONE_MINUS_DST_COLOR +]; + +// If any of the blend factors are not supported, return the default +function getSupportedBlendFactors(value, defaultValue) { + if (!defined(value)) { + return defaultValue; + } + + for (var i = 0; i < 4; i++) { + if (supportedBlendFactors.indexOf(value[i]) === -1) { + return defaultValue; + } + } + + return value; +} + +/** + * Move glTF 1.0 technique render states to glTF 2.0 materials properties and KHR_blend extension. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The updated glTF asset. + * + * @private + */ +function moveTechniqueRenderStates(gltf) { + var blendingForTechnique = {}; + var materialPropertiesForTechnique = {}; + var techniquesLegacy = gltf.techniques; + if (!defined(techniquesLegacy)) { + return gltf; + } + + ForEach.technique(gltf, function (techniqueLegacy, techniqueIndex) { + var renderStates = techniqueLegacy.states; + if (defined(renderStates)) { + var materialProperties = materialPropertiesForTechnique[techniqueIndex] = {}; + + // If BLEND is enabled, the material should have alpha mode BLEND + if (isStateEnabled(renderStates, WebGLConstants.BLEND)) { + materialProperties.alphaMode = 'BLEND'; + + var blendFunctions = renderStates.functions; + if (defined(blendFunctions) && (defined(blendFunctions.blendEquationSeparate) + || defined(blendFunctions.blendFuncSeparate))) { + blendingForTechnique[techniqueIndex] = { + blendEquation: defaultValue(blendFunctions.blendEquationSeparate, defaultBlendEquation), + blendFactors: getSupportedBlendFactors(blendFunctions.blendFuncSeparate, defaultBlendFactors) + }; + } + } + + // If CULL_FACE is not enabled, the material should be doubleSided + if (!isStateEnabled(renderStates, WebGLConstants.CULL_FACE)) { + materialProperties.doubleSided = true; + } + + delete techniqueLegacy.states; + } + }); + + if (Object.keys(blendingForTechnique).length > 0) { + if (!defined(gltf.extensions)) { + gltf.extensions = {}; + } + + addExtensionsUsed(gltf, 'KHR_blend'); + } + + ForEach.material(gltf, function (material) { + if (defined(material.technique)) { + var materialProperties = materialPropertiesForTechnique[material.technique]; + ForEach.objectLegacy(materialProperties, function (value, property) { + material[property] = value; + }); + + var blending = blendingForTechnique[material.technique]; + if (defined(blending)) { + if (!defined(material.extensions)) { + material.extensions = {}; + } + + material.extensions.KHR_blend = blending; + } + } + }); + + return gltf; +} diff --git a/lib/updateVersion.js b/lib/updateVersion.js index 5227ec35..afa23133 100644 --- a/lib/updateVersion.js +++ b/lib/updateVersion.js @@ -6,8 +6,9 @@ var findAccessorMinMax = require('./findAccessorMinMax'); var ForEach = require('./ForEach'); var getAccessorByteStride = require('./getAccessorByteStride'); var numberOfComponentsForType = require('./numberOfComponentsForType'); -var removeUnusedElements = require('./removeUnusedElements'); +var moveTechniqueRenderStates = require('./moveTechniqueRenderStates'); var moveTechniquesToExtension = require('./moveTechniquesToExtension'); +var removeUnusedElements = require('./removeUnusedElements'); var Cartesian3 = Cesium.Cartesian3; var clone = Cesium.clone; @@ -842,6 +843,8 @@ function glTF10to20(gltf) { underscoreApplicationSpecificSemantics(gltf); // Clamp camera parameters clampCameraParameters(gltf); + // Move legacy technique render states to material properties and add KHR_blend extension blending functions + moveTechniqueRenderStates(gltf); // Add material techniques to KHR_techniques_webgl extension, removing shaders, programs, and techniques moveTechniquesToExtension(gltf); // Remove empty arrays diff --git a/specs/lib/moveTechniqueRenderStatesSpec.js b/specs/lib/moveTechniqueRenderStatesSpec.js new file mode 100644 index 00000000..08d59d41 --- /dev/null +++ b/specs/lib/moveTechniqueRenderStatesSpec.js @@ -0,0 +1,217 @@ +'use strict'; +var Cesium = require('cesium'); +var moveTechniqueRenderStates = require('../../lib/moveTechniqueRenderStates'); + +var WebGLConstants = Cesium.WebGLConstants; + +var gltf = { + programs: { + program_0: { + attributes: [ + 'a_normal', + 'a_position', + 'a_texcoord0' + ], + fragmentShader: 'BoxTextured0FS', + vertexShader: 'BoxTextured0VS' + } + }, + shaders: { + BoxTextured0FS: { + type: WebGLConstants.FRAGMENT_SHADER, + uri: 'BoxTextured0FS.glsl' + }, + BoxTextured0VS: { + type: WebGLConstants.VERTEX_SHADER, + uri: 'BoxTextured0VS.glsl' + } + }, + techniques: { + technique0: { + states: { + enable: [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ] + } + } + }, + materials: [ + { + technique: 'technique0' + } + ] +}; + +describe('moveTechniqueRenderStates', function() { + it('sets material.doubleSided property if CULL_FACE is not enabled', function () { + var baseGltf = JSON.parse(JSON.stringify(gltf)); + var gltfWithUpdatedMaterials = moveTechniqueRenderStates(baseGltf); + var material = gltfWithUpdatedMaterials.materials[0]; + expect(material.doubleSided).toBeUndefined(); + + var gltfDoubleSided = JSON.parse(JSON.stringify(gltf)); + gltfDoubleSided.techniques.technique0.states = { + enable : [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND + ] + }; + + gltfWithUpdatedMaterials = moveTechniqueRenderStates(gltfDoubleSided); + material = gltfWithUpdatedMaterials.materials[0]; + expect(material.doubleSided).toBe(true); + }); + + it('sets alphaMode and moves technique render state blending functions to material KHR_blend extension', function() { + var gltfWithBlendFunctions = JSON.parse(JSON.stringify(gltf)); + + gltfWithBlendFunctions.techniques.technique0.states = { + enable: [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ], + functions: { + blendEquationSeparate: [ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD + ], + blendFuncSeparate: [ + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA + ] + } + }; + + var gltfWithBlendExtension = moveTechniqueRenderStates(gltfWithBlendFunctions); + expect(gltfWithBlendExtension.extensionsUsed.indexOf('KHR_blend')).toBeGreaterThan(-1); + expect(gltfWithBlendExtension.extensionsRequired).toBeUndefined(); + + var material = gltfWithBlendExtension.materials[0]; + expect(material.alphaMode).toBe('BLEND'); + expect(material.extensions).toBeDefined(); + var materialBlending = material.extensions.KHR_blend; + expect(materialBlending).toBeDefined(); + expect(materialBlending.blendEquation).toEqual([ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD + ]); + expect(materialBlending.blendFactors).toEqual([ + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA + ]); + }); + + it('provides defaults for extension properties if not provided', function () { + var gltfWithBlendFunctions = JSON.parse(JSON.stringify(gltf)); + gltfWithBlendFunctions.techniques.technique0.states = { + enable: [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ], + functions: { + blendFuncSeparate: [ + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA + ] + } + }; + + var gltfWithBlendExtension = moveTechniqueRenderStates(gltfWithBlendFunctions); + var materialBlending = gltfWithBlendExtension.materials[0].extensions.KHR_blend; + expect(materialBlending).toBeDefined(); + expect(materialBlending.blendEquation).toEqual([ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD + ]); + expect(materialBlending.blendFactors).toEqual([ + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.ONE, + WebGLConstants.ONE_MINUS_SRC_ALPHA + ]); + + gltfWithBlendFunctions.techniques.technique0.states = { + enable: [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ], + functions: { + blendEquationSeparate: [ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD + ] + } + }; + + gltfWithBlendExtension = moveTechniqueRenderStates(gltfWithBlendFunctions); + materialBlending = gltfWithBlendExtension.materials[0].extensions.KHR_blend; + expect(materialBlending).toBeDefined(); + expect(materialBlending.blendEquation).toEqual([ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD + ]); + expect(materialBlending.blendFactors).toEqual([ + WebGLConstants.ONE, + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.ZERO + ]); + }); + + it('falls back to default blending factors if unsupported factor is found', function () { + var gltfWithBlendFunctions = JSON.parse(JSON.stringify(gltf)); + gltfWithBlendFunctions.techniques.technique0.states = { + enable: [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ], + functions : { + blendFuncSeparate: [ + WebGLConstants.SRC_ALPHA_SATURATE, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.SRC_ALPHA_SATURATE, + WebGLConstants.ONE_MINUS_SRC_ALPHA + ] + } + }; + + var gltfWithBlendExtension = moveTechniqueRenderStates(gltfWithBlendFunctions); + + var materialBlending = gltfWithBlendExtension.materials[0].extensions.KHR_blend; + expect(materialBlending).toBeDefined(); + expect(materialBlending.blendFactors).toEqual([ + WebGLConstants.ONE, + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.ZERO + ]); + }); + + it('does not set alphaMode or add KHR_blend if no blending is found in render states', function () { + var gltfWithoutBlending = JSON.parse(JSON.stringify(gltf)); + gltfWithoutBlending.techniques.technique0.states.enable = [ + WebGLConstants.DEPTH_TEST, + WebGLConstants.CULL_FACE + ]; + + var updatedGltf = moveTechniqueRenderStates(gltfWithoutBlending); + expect(updatedGltf.extensionsUsed).toBeUndefined(); + expect(updatedGltf.extensionsRequired).toBeUndefined(); + + var material = updatedGltf.materials[0]; + expect(material.alphaMode).toBeUndefined(); + expect(material.extensions).toBeUndefined(); + }); +}); diff --git a/specs/lib/updateVersionSpec.js b/specs/lib/updateVersionSpec.js index 1a17091b..f681c63a 100644 --- a/specs/lib/updateVersionSpec.js +++ b/specs/lib/updateVersionSpec.js @@ -343,9 +343,17 @@ describe('updateVersion', function() { techniques: { technique: { states: { - enable: [ WebGLConstants.SCISSOR_TEST ], + enable: [ + WebGLConstants.SCISSOR_TEST, + WebGLConstants.BLEND, + WebGLConstants.CULL_FACE + ], functions: { blendColor: [-1.0, 0.0, 0.0, 2.0], + blendEquationSeparate: [ + WebGLConstants.FUNC_SUBTRACT, + WebGLConstants.FUNC_SUBTRACT + ], depthRange: [1.0, -1.0], scissor: [0.0, 0.0, 0.0, 0.0] } @@ -562,6 +570,7 @@ describe('updateVersion', function() { 'KHR_materials_common', 'WEB3D_quantized_attributes', 'UNKOWN_EXTENSION', + 'KHR_blend', 'KHR_techniques_webgl' ]); var extensionsRequired = gltf.extensionsRequired; @@ -586,6 +595,24 @@ describe('updateVersion', function() { var material = gltf.materials[0]; expect(material.extensions.KHR_techniques_webgl.values.u_lightAttenuation).toEqual(2); + // Expect material paramters to be updated + expect(material.doubleSided).toBeUndefined(); + expect(material.alphaMode).toBe('BLEND'); + + // Expect technique blending to be moved to material KHR_blend extension + var materialBlending = material.extensions.KHR_blend; + expect(materialBlending).toBeDefined(); + expect(materialBlending.blendEquation).toEqual([ + WebGLConstants.FUNC_SUBTRACT, + WebGLConstants.FUNC_SUBTRACT + ]); + expect(materialBlending.blendFactors).toEqual([ + WebGLConstants.ONE, + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.ZERO + ]); + // Expect techniques to be moved to asset KHR_techniques_webgl extension var technique = gltf.extensions.KHR_techniques_webgl.techniques[0]; expect(technique.uniforms.u_lightAttenuation.value).toEqual(1.0);