private void ExportAnimatedNode(GLTF gltf, BabylonNode node, GLTFNode gltfNode, GLTFAnimation gltfAnimation, int startFrame, int endFrame, BabylonAnimationGroup animGroup = null) { bool nodeHasAnimations = node.animations != null && node.animations.Length > 0 && node.animations[0] != null; bool nodeHasExtraAnimations = node.extraAnimations != null && node.extraAnimations.Count > 0 && node.extraAnimations[0] != null; BabylonMesh meshNode = node as BabylonMesh; BabylonMorphTargetManager morphTargetManager = null; bool nodeHasAnimatedMorphTargets = false; if (meshNode != null && meshNode.morphTargetManagerId != null) { morphTargetManager = GetBabylonMorphTargetManager(babylonScene, meshNode); if (morphTargetManager != null) { nodeHasAnimatedMorphTargets = morphTargetManager.targets.Any(target => target.animations != null && target.animations.Length > 0 && target.animations[0] != null); } } if (!nodeHasAnimations && !nodeHasExtraAnimations && !nodeHasAnimatedMorphTargets) { return; } if (nodeHasAnimations && node.animations[0].property == "_matrix") { ExportBoneAnimation(gltfAnimation, startFrame, endFrame, gltf, node, gltfNode, animGroup); } else { ExportNodeAnimation(gltfAnimation, startFrame, endFrame, gltf, node, gltfNode, babylonScene, animGroup); } if (nodeHasAnimatedMorphTargets) { ExportMorphTargetWeightAnimation(morphTargetManager, gltf, gltfNode, gltfAnimation.ChannelList, gltfAnimation.SamplerList, startFrame, endFrame, babylonScene); } }
private bool _isBabylonMorphTargetManagerAnimationValid(BabylonMorphTargetManager babylonMorphTargetManager) { bool hasAnimation = false; bool areAnimationsValid = true; foreach (var babylonMorphTarget in babylonMorphTargetManager.targets) { if (babylonMorphTarget.animations != null && babylonMorphTarget.animations.Length > 0) { hasAnimation = true; // Ensure target has only one animation if (babylonMorphTarget.animations.Length > 1) { areAnimationsValid = false; RaiseWarning("GLTFExporter.Animation | Only one animation is supported for morph targets", 3); continue; } // Ensure the target animation property is 'influence' bool targetHasInfluence = false; foreach (BabylonAnimation babylonAnimation in babylonMorphTarget.animations) { if (babylonAnimation.property == "influence") { targetHasInfluence = true; } } if (targetHasInfluence == false) { areAnimationsValid = false; RaiseWarning("GLTFExporter.Animation | Only 'influence' animation is supported for morph targets", 3); continue; } } } return(hasAnimation && areAnimationsValid); }
private BabylonNode ExportMesh(IIGameScene scene, IIGameNode meshNode, BabylonScene babylonScene) { if (IsMeshExportable(meshNode) == false) { return(null); } RaiseMessage(meshNode.Name, 1); // Instances var tabs = Loader.Global.NodeTab.Create(); Loader.Global.IInstanceMgr.InstanceMgr.GetInstances(meshNode.MaxNode, tabs); if (tabs.Count > 1) { // For a mesh with instances, we distinguish between master and instance meshes: // - a master mesh stores all the info of the mesh (transform, hierarchy, animations + vertices, indices, materials, bones...) // - an instance mesh only stores the info of the node (transform, hierarchy, animations) // Check if this mesh has already been exported BabylonMesh babylonMasterMesh = null; var index = 0; while (babylonMasterMesh == null && index < tabs.Count) { #if MAX2017 var indexer = index; #else var indexer = new IntPtr(index); #endif var tab = tabs[indexer]; babylonMasterMesh = babylonScene.MeshesList.Find(_babylonMesh => { // Same id return(_babylonMesh.id == tab.GetGuid().ToString() && // Mesh is not a dummy _babylonMesh.isDummy == false); }); index++; } if (babylonMasterMesh != null) { // Mesh already exported // Export this node as instance meshNode.MaxNode.MarkAsInstance(); var babylonInstanceMesh = new BabylonAbstractMesh { name = meshNode.Name, id = meshNode.MaxNode.GetGuid().ToString() }; // Add instance to master mesh List <BabylonAbstractMesh> list = babylonMasterMesh.instances != null?babylonMasterMesh.instances.ToList() : new List <BabylonAbstractMesh>(); list.Add(babylonInstanceMesh); babylonMasterMesh.instances = list.ToArray(); // Export transform / hierarchy / animations exportNode(babylonInstanceMesh, meshNode, scene, babylonScene); // Animations exportAnimation(babylonInstanceMesh, meshNode); return(babylonInstanceMesh); } } var gameMesh = meshNode.IGameObject.AsGameMesh(); bool initialized = gameMesh.InitializeData; // needed, the property is in fact a method initializing the exporter that has wrongly been auto // translated into a property because it has no parameters var babylonMesh = new BabylonMesh { name = meshNode.Name, id = meshNode.MaxNode.GetGuid().ToString() }; // Position / rotation / scaling / hierarchy exportNode(babylonMesh, meshNode, scene, babylonScene); // Sounds var soundName = meshNode.MaxNode.GetStringProperty("babylonjs_sound_filename", ""); if (!string.IsNullOrEmpty(soundName)) { var filename = Path.GetFileName(soundName); var meshSound = new BabylonSound { name = filename, autoplay = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_autoplay", 1), loop = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_loop", 1), volume = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_volume", 1.0f), playbackRate = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_playbackrate", 1.0f), connectedMeshId = babylonMesh.id, isDirectional = false, spatialSound = false, distanceModel = meshNode.MaxNode.GetStringProperty("babylonjs_sound_distancemodel", "linear"), maxDistance = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_maxdistance", 100f), rolloffFactor = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_rolloff", 1.0f), refDistance = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_refdistance", 1.0f), }; var isDirectional = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_directional"); if (isDirectional) { meshSound.isDirectional = true; meshSound.coneInnerAngle = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneinnerangle", 360f); meshSound.coneOuterAngle = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneouterangle", 360f); meshSound.coneOuterGain = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneoutergain", 1.0f); } babylonScene.SoundsList.Add(meshSound); if (isBabylonExported) { try { File.Copy(soundName, Path.Combine(babylonScene.OutputPath, filename), true); } catch { } } } // Misc. #if MAX2017 babylonMesh.isVisible = meshNode.MaxNode.Renderable; babylonMesh.receiveShadows = meshNode.MaxNode.RcvShadows; babylonMesh.applyFog = meshNode.MaxNode.ApplyAtmospherics; #else babylonMesh.isVisible = meshNode.MaxNode.Renderable == 1; babylonMesh.receiveShadows = meshNode.MaxNode.RcvShadows == 1; babylonMesh.applyFog = meshNode.MaxNode.ApplyAtmospherics == 1; #endif babylonMesh.pickable = meshNode.MaxNode.GetBoolProperty("babylonjs_checkpickable"); babylonMesh.showBoundingBox = meshNode.MaxNode.GetBoolProperty("babylonjs_showboundingbox"); babylonMesh.showSubMeshesBoundingBox = meshNode.MaxNode.GetBoolProperty("babylonjs_showsubmeshesboundingbox"); babylonMesh.alphaIndex = (int)meshNode.MaxNode.GetFloatProperty("babylonjs_alphaindex", 1000); // Actions babylonMesh.actions = ExportNodeAction(meshNode); // Collisions babylonMesh.checkCollisions = meshNode.MaxNode.GetBoolProperty("babylonjs_checkcollisions"); var isSkinned = gameMesh.IsObjectSkinned; var skin = gameMesh.IGameSkin; var unskinnedMesh = gameMesh; IGMatrix skinInitPoseMatrix = Loader.Global.GMatrix.Create(Loader.Global.Matrix3.Create(true)); List <int> boneIds = null; int maxNbBones = 0; if (isSkinned) { bonesCount = skin.TotalSkinBoneCount; var skinAlreadyStored = skins.Find(_skin => IsSkinEqualTo(_skin, skin)); if (skinAlreadyStored == null) { skins.Add(skin); } else { skin = skinAlreadyStored; } skinnedNodes.Add(meshNode); babylonMesh.skeletonId = skins.IndexOf(skin); skin.GetInitSkinTM(skinInitPoseMatrix); boneIds = SortBones(skin); skinSortedBones[skin] = boneIds; } // Mesh if (unskinnedMesh.IGameType == Autodesk.Max.IGameObject.ObjectTypes.Mesh && unskinnedMesh.MaxMesh != null) { if (unskinnedMesh.NumberOfFaces < 1) { RaiseError($"Mesh {babylonMesh.name} has no face", 2); } if (unskinnedMesh.NumberOfVerts < 3) { RaiseError($"Mesh {babylonMesh.name} has not enough vertices", 2); } if (unskinnedMesh.NumberOfVerts >= 65536) { RaiseWarning($"Mesh {babylonMesh.name} has tmore than 65536 vertices which means that it will require specific WebGL extension to be rendered. This may impact portability of your scene on low end devices.", 2); } if (skin != null) { for (var vertexIndex = 0; vertexIndex < unskinnedMesh.NumberOfVerts; vertexIndex++) { maxNbBones = Math.Max(maxNbBones, skin.GetNumberOfBones(vertexIndex)); } } // Physics var impostorText = meshNode.MaxNode.GetStringProperty("babylonjs_impostor", "None"); if (impostorText != "None") { switch (impostorText) { case "Sphere": babylonMesh.physicsImpostor = 1; break; case "Box": babylonMesh.physicsImpostor = 2; break; case "Plane": babylonMesh.physicsImpostor = 3; break; default: babylonMesh.physicsImpostor = 0; break; } babylonMesh.physicsMass = meshNode.MaxNode.GetFloatProperty("babylonjs_mass"); babylonMesh.physicsFriction = meshNode.MaxNode.GetFloatProperty("babylonjs_friction", 0.2f); babylonMesh.physicsRestitution = meshNode.MaxNode.GetFloatProperty("babylonjs_restitution", 0.2f); } // Material var mtl = meshNode.NodeMaterial; var multiMatsCount = 1; if (mtl != null) { babylonMesh.materialId = mtl.MaxMaterial.GetGuid().ToString(); if (!referencedMaterials.Contains(mtl)) { referencedMaterials.Add(mtl); } multiMatsCount = Math.Max(mtl.SubMaterialCount, 1); } babylonMesh.visibility = meshNode.MaxNode.GetVisibility(0, Tools.Forever); var vertices = new List <GlobalVertex>(); var indices = new List <int>(); var mappingChannels = unskinnedMesh.ActiveMapChannelNum; bool hasUV = false; bool hasUV2 = false; for (int i = 0; i < mappingChannels.Count; ++i) { #if MAX2017 var indexer = i; #else var indexer = new IntPtr(i); #endif var channelNum = mappingChannels[indexer]; if (channelNum == 1) { hasUV = true; } else if (channelNum == 2) { hasUV2 = true; } } var hasColor = unskinnedMesh.NumberOfColorVerts > 0; var hasAlpha = unskinnedMesh.GetNumberOfMapVerts(-2) > 0; var optimizeVertices = meshNode.MaxNode.GetBoolProperty("babylonjs_optimizevertices"); // Compute normals var subMeshes = new List <BabylonSubMesh>(); ExtractGeometry(vertices, indices, subMeshes, boneIds, skin, unskinnedMesh, hasUV, hasUV2, hasColor, hasAlpha, optimizeVertices, multiMatsCount, meshNode); if (vertices.Count >= 65536) { RaiseWarning($"Mesh {babylonMesh.name} has {vertices.Count} vertices. This may prevent your scene to work on low end devices where 32 bits indice are not supported", 2); if (!optimizeVertices) { RaiseError("You can try to optimize your object using [Try to optimize vertices] option", 2); } } RaiseMessage($"{vertices.Count} vertices, {indices.Count/3} faces", 2); // Buffers babylonMesh.positions = vertices.SelectMany(v => new[] { v.Position.X, v.Position.Y, v.Position.Z }).ToArray(); babylonMesh.normals = vertices.SelectMany(v => new[] { v.Normal.X, v.Normal.Y, v.Normal.Z }).ToArray(); if (hasUV) { babylonMesh.uvs = vertices.SelectMany(v => new[] { v.UV.X, 1 - v.UV.Y }).ToArray(); } if (hasUV2) { babylonMesh.uvs2 = vertices.SelectMany(v => new[] { v.UV2.X, 1 - v.UV2.Y }).ToArray(); } if (skin != null) { babylonMesh.matricesWeights = vertices.SelectMany(v => v.Weights.ToArray()).ToArray(); babylonMesh.matricesIndices = vertices.Select(v => v.BonesIndices).ToArray(); babylonMesh.numBoneInfluencers = maxNbBones; if (maxNbBones > 4) { babylonMesh.matricesWeightsExtra = vertices.SelectMany(v => v.WeightsExtra != null ? v.WeightsExtra.ToArray() : new[] { 0.0f, 0.0f, 0.0f, 0.0f }).ToArray(); babylonMesh.matricesIndicesExtra = vertices.Select(v => v.BonesIndicesExtra).ToArray(); } } if (hasColor) { babylonMesh.colors = vertices.SelectMany(v => v.Color.ToArray()).ToArray(); babylonMesh.hasVertexAlpha = hasAlpha; } babylonMesh.subMeshes = subMeshes.ToArray(); // Buffers - Indices babylonMesh.indices = indices.ToArray(); // ------------------------ // ---- Morph targets ----- // ------------------------ // Retreive modifiers with morpher flag List <IIGameModifier> modifiers = new List <IIGameModifier>(); for (int i = 0; i < meshNode.IGameObject.NumModifiers; i++) { var modifier = meshNode.IGameObject.GetIGameModifier(i); if (modifier.ModifierType == Autodesk.Max.IGameModifier.ModType.Morpher) { modifiers.Add(modifier); } } // Cast modifiers to morphers List <IIGameMorpher> morphers = modifiers.ConvertAll(new Converter <IIGameModifier, IIGameMorpher>(modifier => modifier.AsGameMorpher())); var hasMorphTarget = false; morphers.ForEach(morpher => { if (morpher.NumberOfMorphTargets > 0) { hasMorphTarget = true; } }); if (hasMorphTarget) { RaiseMessage("Export morph targets", 2); // Morph Target Manager var babylonMorphTargetManager = new BabylonMorphTargetManager(); babylonScene.MorphTargetManagersList.Add(babylonMorphTargetManager); babylonMesh.morphTargetManagerId = babylonMorphTargetManager.id; // Morph Targets var babylonMorphTargets = new List <BabylonMorphTarget>(); // All morphers are considered identical // Their targets are concatenated morphers.ForEach(morpher => { for (int i = 0; i < morpher.NumberOfMorphTargets; i++) { // Morph target var maxMorphTarget = morpher.GetMorphTarget(i); // Ensure target still exists (green color legend) if (maxMorphTarget != null) { var babylonMorphTarget = new BabylonMorphTarget { name = maxMorphTarget.Name }; babylonMorphTargets.Add(babylonMorphTarget); // TODO - Influence babylonMorphTarget.influence = 0f; // Target geometry var targetVertices = ExtractVertices(maxMorphTarget, optimizeVertices); babylonMorphTarget.positions = targetVertices.SelectMany(v => new[] { v.Position.X, v.Position.Y, v.Position.Z }).ToArray(); babylonMorphTarget.normals = targetVertices.SelectMany(v => new[] { v.Normal.X, v.Normal.Y, v.Normal.Z }).ToArray(); // Animations var animations = new List <BabylonAnimation>(); var morphWeight = morpher.GetMorphWeight(i); ExportFloatGameController(morphWeight, "influence", animations); if (animations.Count > 0) { babylonMorphTarget.animations = animations.ToArray(); } } } }); babylonMorphTargetManager.targets = babylonMorphTargets.ToArray(); } } // Animations // Done last to avoid '0 vertex found' error (unkown cause) exportAnimation(babylonMesh, meshNode); babylonScene.MeshesList.Add(babylonMesh); return(babylonMesh); }
/// <summary> /// The keys of each BabylonMorphTarget animation ARE NOT assumed to be identical. /// This function merges together all keys and binds to each an influence value for all targets. /// A target influence value is automatically computed when necessary. /// Computation rules are: /// - linear interpolation between target key range /// - constant value outside target key range /// </summary> /// <example> /// When: /// animation1.keys = {0, 25, 50, 100} /// animation2.keys = {50, 75, 100} /// /// Gives: /// mergedKeys = {0, 25, 50, 100, 75} /// range1=[0, 100] /// range2=[50, 100] /// for animation1, the value associated to key=75 is the interpolation of its values between 50 and 100 /// for animation2, the value associated to key=0 is equal to the one at key=50 since 0 is out of range [50, 100] (same for key=25)</example> /// <param name="babylonMorphTargetManager"></param> /// <returns>A map which for each frame, gives the influence value of all targets</returns> private Dictionary <int, List <float> > _getTargetManagerAnimationsData(BabylonMorphTargetManager babylonMorphTargetManager) { // Merge all keys into a single set (no duplicated frame) var mergedFrames = new HashSet <int>(); foreach (var babylonMorphTarget in babylonMorphTargetManager.targets) { if (babylonMorphTarget.animations != null) { var animation = babylonMorphTarget.animations[0]; foreach (BabylonAnimationKey animationKey in animation.keys) { mergedFrames.Add(animationKey.frame); } } } // For each frame, gives the influence value of all targets (gltf structure) var influencesPerFrame = new Dictionary <int, List <float> >(); foreach (var frame in mergedFrames) { influencesPerFrame.Add(frame, new List <float>()); } foreach (var babylonMorphTarget in babylonMorphTargetManager.targets) { // For a given target, for each frame, gives the influence value of the target (babylon structure) var influencePerFrameForTarget = new Dictionary <int, float>(); if (babylonMorphTarget.animations != null && babylonMorphTarget.animations.Length > 0) { var animation = babylonMorphTarget.animations[0]; if (animation.keys.Length == 1) { // Same influence for all frames var influence = animation.keys[0].values[0]; foreach (var frame in mergedFrames) { influencePerFrameForTarget.Add(frame, influence); } } else { // Retreive target animation key range [min, max] var babylonAnimationKeys = new List <BabylonAnimationKey>(animation.keys); babylonAnimationKeys.Sort(); var minAnimationKey = babylonAnimationKeys[0]; var maxAnimationKey = babylonAnimationKeys[babylonAnimationKeys.Count - 1]; foreach (var frame in mergedFrames) { // Surround the current frame with closest keys available for the target BabylonAnimationKey lowerAnimationKey = minAnimationKey; BabylonAnimationKey upperAnimationKey = maxAnimationKey; foreach (BabylonAnimationKey animationKey in animation.keys) { if (lowerAnimationKey.frame < animationKey.frame && animationKey.frame <= frame) { lowerAnimationKey = animationKey; } if (frame <= animationKey.frame && animationKey.frame < upperAnimationKey.frame) { upperAnimationKey = animationKey; } } // In case the target has a key for this frame // or the current frame is out of target animation key range if (lowerAnimationKey.frame == upperAnimationKey.frame) { influencePerFrameForTarget.Add(frame, lowerAnimationKey.values[0]); } else { // Interpolate influence values var t = 1.0f * (frame - lowerAnimationKey.frame) / (upperAnimationKey.frame - lowerAnimationKey.frame); var influence = Tools.Lerp(lowerAnimationKey.values[0], upperAnimationKey.values[0], t); influencePerFrameForTarget.Add(frame, influence); } } } } else { // Target is not animated // Fill all frames with 0 foreach (var frame in mergedFrames) { influencePerFrameForTarget.Add(frame, 0); } } // Switch from babylon to gltf storage representation foreach (var frame in mergedFrames) { List <float> influences = influencesPerFrame[frame]; influences.Add(influencePerFrameForTarget[frame]); } } return(influencesPerFrame); }
private bool ExportMorphTargetWeightAnimation(BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFNode gltfNode, List <GLTFChannel> channelList, List <GLTFAnimationSampler> samplerList) { if (!_isBabylonMorphTargetManagerAnimationValid(babylonMorphTargetManager)) { return(false); } RaiseMessage("GLTFExporter.Animation | Export animation of morph target manager with id: " + babylonMorphTargetManager.id, 2); var influencesPerFrame = _getTargetManagerAnimationsData(babylonMorphTargetManager); var frames = new List <int>(influencesPerFrame.Keys); frames.Sort(); // Mandatory otherwise gltf loader of babylon doesn't understand // Target var gltfTarget = new GLTFChannelTarget { node = gltfNode.index }; gltfTarget.path = "weights"; // Buffer var buffer = GLTFBufferService.Instance.GetBuffer(gltf); // --- Input --- var accessorInput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationInput", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor accessorInput.min = new float[] { float.MaxValue }; accessorInput.max = new float[] { float.MinValue }; foreach (var frame in frames) { var inputValue = frame / FPS_FACTOR; // Store values as bytes accessorInput.bytesList.AddRange(BitConverter.GetBytes(inputValue)); // Update min and max values GLTFBufferService.UpdateMinMaxAccessor(accessorInput, inputValue); } accessorInput.count = influencesPerFrame.Count; // --- Output --- GLTFAccessor accessorOutput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationWeights", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor foreach (var frame in frames) { var outputValues = influencesPerFrame[frame]; // Store values as bytes foreach (var outputValue in outputValues) { accessorOutput.count++; accessorOutput.bytesList.AddRange(BitConverter.GetBytes(outputValue)); } } // Animation sampler var gltfAnimationSampler = new GLTFAnimationSampler { input = accessorInput.index, output = accessorOutput.index }; gltfAnimationSampler.index = samplerList.Count; samplerList.Add(gltfAnimationSampler); // Channel var gltfChannel = new GLTFChannel { sampler = gltfAnimationSampler.index, target = gltfTarget }; channelList.Add(gltfChannel); return(true); }
private void _exportMorphTargets(BabylonMesh babylonMesh, BabylonSubMesh babylonSubMesh, BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFBuffer buffer, GLTFMeshPrimitive meshPrimitive) { var gltfMorphTargets = new List <GLTFMorphTarget>(); foreach (var babylonMorphTarget in babylonMorphTargetManager.targets) { var gltfMorphTarget = new GLTFMorphTarget(); // Positions if (babylonMorphTarget.positions != null) { var accessorTargetPositions = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer), "accessorTargetPositions", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.VEC3 ); gltfMorphTarget.Add(GLTFMorphTarget.Attribute.POSITION.ToString(), accessorTargetPositions.index); // Populate accessor int nbComponents = 3; // Vector3 int startIndex = babylonSubMesh.verticesStart * nbComponents; int endIndex = startIndex + babylonSubMesh.verticesCount * nbComponents; accessorTargetPositions.min = new float[] { float.MaxValue, float.MaxValue, float.MaxValue }; accessorTargetPositions.max = new float[] { float.MinValue, float.MinValue, float.MinValue }; for (int indexPosition = startIndex; indexPosition < endIndex; indexPosition += 3) { var positionTarget = Tools.SubArray(babylonMorphTarget.positions, indexPosition, 3); // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive var positionMesh = Tools.SubArray(babylonMesh.positions, indexPosition, 3); for (int indexCoordinate = 0; indexCoordinate < positionTarget.Length; indexCoordinate++) { positionTarget[indexCoordinate] = positionTarget[indexCoordinate] - positionMesh[indexCoordinate]; } positionTarget[2] *= -1; // Store values as bytes foreach (var coordinate in positionTarget) { accessorTargetPositions.bytesList.AddRange(BitConverter.GetBytes(coordinate)); } // Update min and max values GLTFBufferService.UpdateMinMaxAccessor(accessorTargetPositions, positionTarget); } accessorTargetPositions.count = babylonSubMesh.verticesCount; } // Normals if ((babylonMorphTarget.normals != null) && _exportMorphNormal) { var accessorTargetNormals = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer), "accessorTargetNormals", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.VEC3 ); gltfMorphTarget.Add(GLTFMorphTarget.Attribute.NORMAL.ToString(), accessorTargetNormals.index); // Populate accessor int nbComponents = 3; // Vector3 int startIndex = babylonSubMesh.verticesStart * nbComponents; int endIndex = startIndex + babylonSubMesh.verticesCount * nbComponents; for (int indexNormal = startIndex; indexNormal < endIndex; indexNormal += 3) { var normalTarget = Tools.SubArray(babylonMorphTarget.normals, indexNormal, 3); // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive var normalMesh = Tools.SubArray(babylonMesh.normals, indexNormal, 3); for (int indexCoordinate = 0; indexCoordinate < normalTarget.Length; indexCoordinate++) { normalTarget[indexCoordinate] = normalTarget[indexCoordinate] - normalMesh[indexCoordinate]; } normalTarget[2] *= -1; // Store values as bytes foreach (var coordinate in normalTarget) { accessorTargetNormals.bytesList.AddRange(BitConverter.GetBytes(coordinate)); } } accessorTargetNormals.count = babylonSubMesh.verticesCount; } // Tangents if ((babylonMorphTarget.tangents != null) && _exportMorphTangent) { var accessorTargetTangents = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer), "accessorTargetTangents", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.VEC3 ); gltfMorphTarget.Add(GLTFMeshPrimitive.Attribute.TANGENT.ToString(), accessorTargetTangents.index); // Populate accessor // Note that the w component for handedness is omitted when targeting TANGENT data since handedness cannot be displaced. int nbComponents = 4; // Vector4 int startIndex = babylonSubMesh.verticesStart * nbComponents; int endIndex = startIndex + babylonSubMesh.verticesCount * nbComponents; for (int indexTangent = startIndex; indexTangent < endIndex; indexTangent += 4) { var tangentTarget = Tools.SubArray(babylonMorphTarget.tangents, indexTangent, 3); // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive var tangentMesh = Tools.SubArray(babylonMesh.tangents, indexTangent, 3); for (int indexCoordinate = 0; indexCoordinate < tangentTarget.Length; indexCoordinate++) { tangentTarget[indexCoordinate] = tangentTarget[indexCoordinate] - tangentMesh[indexCoordinate]; } tangentTarget[2] *= -1; // Store values as bytes foreach (var coordinate in tangentTarget) { accessorTargetTangents.bytesList.AddRange(BitConverter.GetBytes(coordinate)); } } accessorTargetTangents.count = babylonSubMesh.verticesCount; } gltfMorphTargets.Add(gltfMorphTarget); } if (gltfMorphTargets.Count > 0) { meshPrimitive.targets = gltfMorphTargets.ToArray(); } }
public async Task ExportAsync(string outputFile, string outputFormat, bool generateManifest, bool onlySelected, Form callerForm) { var gameConversionManger = Loader.Global.ConversionManager; gameConversionManger.CoordSystem = Autodesk.Max.IGameConversionManager.CoordSystem.D3d; var gameScene = Loader.Global.IGameInterface; gameScene.InitialiseIGame(onlySelected); gameScene.SetStaticFrame(0); MaxSceneFileName = gameScene.SceneFileName; // Force output file extension to be babylon outputFile = Path.ChangeExtension(outputFile, "babylon"); IsCancelled = false; RaiseMessage("Exportation started", Color.Blue); ReportProgressChanged(0); var babylonScene = new BabylonScene(Path.GetDirectoryName(outputFile)); var rawScene = Loader.Core.RootNode; if (!Directory.Exists(babylonScene.OutputPath)) { RaiseError("Exportation stopped: Output folder does not exist"); ReportProgressChanged(100); return; } var watch = new Stopwatch(); watch.Start(); // Save scene if (AutoSave3dsMaxFile) { RaiseMessage("Saving 3ds max file"); var forceSave = Loader.Core.FileSave; callerForm?.BringToFront(); } // Producer babylonScene.producer = new BabylonProducer { name = "3dsmax", #if MAX2017 version = "2017", #else version = Loader.Core.ProductVersion.ToString(), #endif exporter_version = "0.4.5", file = Path.GetFileName(outputFile) }; // Global babylonScene.autoClear = true; babylonScene.clearColor = Loader.Core.GetBackGround(0, Tools.Forever).ToArray(); babylonScene.ambientColor = Loader.Core.GetAmbient(0, Tools.Forever).ToArray(); babylonScene.gravity = rawScene.GetVector3Property("babylonjs_gravity"); ExportQuaternionsInsteadOfEulers = rawScene.GetBoolProperty("babylonjs_exportquaternions", 1); if (Loader.Core.UseEnvironmentMap && Loader.Core.EnvironmentMap != null) { // Environment texture var environmentMap = Loader.Core.EnvironmentMap; // Copy image file to output if necessary var babylonTexture = ExportTexture(environmentMap, 1.0f, babylonScene, true); babylonScene.environmentTexture = babylonTexture.name; // Skybox babylonScene.createDefaultSkybox = rawScene.GetBoolProperty("babylonjs_createDefaultSkybox"); babylonScene.skyboxBlurLevel = rawScene.GetFloatProperty("babylonjs_skyboxBlurLevel"); } // Sounds var soundName = rawScene.GetStringProperty("babylonjs_sound_filename", ""); if (!string.IsNullOrEmpty(soundName)) { var filename = Path.GetFileName(soundName); var globalSound = new BabylonSound { autoplay = rawScene.GetBoolProperty("babylonjs_sound_autoplay", 1), loop = rawScene.GetBoolProperty("babylonjs_sound_loop", 1), name = filename }; babylonScene.SoundsList.Add(globalSound); try { File.Copy(soundName, Path.Combine(babylonScene.OutputPath, filename), true); } catch { } } // Root nodes RaiseMessage("Exporting nodes"); HashSet <IIGameNode> maxRootNodes = getRootNodes(gameScene); var progressionStep = 80.0f / maxRootNodes.Count; var progression = 10.0f; ReportProgressChanged((int)progression); referencedMaterials.Clear(); // Reseting is optionnal. It makes each morph target manager export starts from id = 0. BabylonMorphTargetManager.Reset(); foreach (var maxRootNode in maxRootNodes) { exportNodeRec(maxRootNode, babylonScene, gameScene); progression += progressionStep; ReportProgressChanged((int)progression); CheckCancelled(); } ; RaiseMessage(string.Format("Total meshes: {0}", babylonScene.MeshesList.Count), Color.Gray, 1); // Main camera BabylonCamera babylonMainCamera = null; ICameraObject maxMainCameraObject = null; if (babylonMainCamera == null && babylonScene.CamerasList.Count > 0) { // Set first camera as main one babylonMainCamera = babylonScene.CamerasList[0]; babylonScene.activeCameraID = babylonMainCamera.id; RaiseMessage("Active camera set to " + babylonMainCamera.name, Color.Green, 1, true); // Retreive camera node with same GUID var maxCameraNodesAsTab = gameScene.GetIGameNodeByType(Autodesk.Max.IGameObject.ObjectTypes.Camera); var maxCameraNodes = TabToList(maxCameraNodesAsTab); var maxMainCameraNode = maxCameraNodes.Find(_camera => _camera.MaxNode.GetGuid().ToString() == babylonMainCamera.id); maxMainCameraObject = (maxMainCameraNode.MaxNode.ObjectRef as ICameraObject); } if (babylonMainCamera == null) { RaiseWarning("No camera defined", 1); } else { RaiseMessage(string.Format("Total cameras: {0}", babylonScene.CamerasList.Count), Color.Gray, 1); } // Default light if (babylonScene.LightsList.Count == 0) { RaiseWarning("No light defined", 1); RaiseWarning("A default hemispheric light was added for your convenience", 1); ExportDefaultLight(babylonScene); } else { RaiseMessage(string.Format("Total lights: {0}", babylonScene.LightsList.Count), Color.Gray, 1); } // Materials RaiseMessage("Exporting materials"); var matsToExport = referencedMaterials.ToArray(); // Snapshot because multimaterials can export new materials foreach (var mat in matsToExport) { ExportMaterial(mat, babylonScene); CheckCancelled(); } RaiseMessage(string.Format("Total: {0}", babylonScene.MaterialsList.Count + babylonScene.MultiMaterialsList.Count), Color.Gray, 1); // Fog for (var index = 0; index < Loader.Core.NumAtmospheric; index++) { var atmospheric = Loader.Core.GetAtmospheric(index); if (atmospheric.Active(0) && atmospheric.ClassName == "Fog") { var fog = atmospheric as IStdFog; RaiseMessage("Exporting fog"); if (fog != null) { babylonScene.fogColor = fog.GetColor(0).ToArray(); babylonScene.fogMode = 3; } if (babylonMainCamera != null) { babylonScene.fogStart = maxMainCameraObject.GetEnvRange(0, 0, Tools.Forever); babylonScene.fogEnd = maxMainCameraObject.GetEnvRange(0, 1, Tools.Forever); } } } // Skeletons if (skins.Count > 0) { RaiseMessage("Exporting skeletons"); foreach (var skin in skins) { ExportSkin(skin, babylonScene); } } // Actions babylonScene.actions = ExportNodeAction(gameScene.GetIGameNode(rawScene)); // Output babylonScene.Prepare(false, false); if (outputFormat == "babylon" || outputFormat == "binary babylon") { RaiseMessage("Saving to output file"); var jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings()); var sb = new StringBuilder(); var sw = new StringWriter(sb, CultureInfo.InvariantCulture); await Task.Run(() => { using (var jsonWriter = new JsonTextWriterOptimized(sw)) { jsonWriter.Formatting = Formatting.None; jsonSerializer.Serialize(jsonWriter, babylonScene); } File.WriteAllText(outputFile, sb.ToString()); if (generateManifest) { File.WriteAllText(outputFile + ".manifest", "{\r\n\"version\" : 1,\r\n\"enableSceneOffline\" : true,\r\n\"enableTexturesOffline\" : true\r\n}"); } }); // Binary if (outputFormat == "binary babylon") { RaiseMessage("Generating binary files"); BabylonFileConverter.BinaryConverter.Convert(outputFile, Path.GetDirectoryName(outputFile) + "\\Binary", message => RaiseMessage(message, 1), error => RaiseError(error, 1)); } } ReportProgressChanged(100); // Export glTF if (outputFormat == "gltf" || outputFormat == "glb") { bool generateBinary = outputFormat == "glb"; ExportGltf(babylonScene, outputFile, generateBinary); } watch.Stop(); RaiseMessage(string.Format("Exportation done in {0:0.00}s", watch.ElapsedMilliseconds / 1000.0), Color.Blue); }
public void Export(ExportParameters exportParameters) { // Check input text is valid var scaleFactorFloat = 1.0f; string scaleFactor = exportParameters.scaleFactor; try { scaleFactor = scaleFactor.Replace(".", System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator); scaleFactor = scaleFactor.Replace(",", System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator); scaleFactorFloat = float.Parse(scaleFactor); } catch { RaiseError("Scale factor is not a valid number."); return; } long quality = 0L; string txtQuality = exportParameters.txtQuality; try { quality = long.Parse(txtQuality); if (quality < 0 || quality > 100) { throw new Exception(); } } catch { RaiseError("Quality is not a valid number. It should be an integer between 0 and 100."); RaiseError("This parameter set the quality of jpg compression."); return; } this.exportParameters = exportParameters; var gameConversionManger = Loader.Global.ConversionManager; gameConversionManger.CoordSystem = Autodesk.Max.IGameConversionManager.CoordSystem.D3d; var gameScene = Loader.Global.IGameInterface; if (exportParameters.exportNode == null) { gameScene.InitialiseIGame(false); } else { gameScene.InitialiseIGame(exportParameters.exportNode, true); } gameScene.SetStaticFrame(0); MaxSceneFileName = gameScene.SceneFileName; IsCancelled = false; string fileExportString = exportParameters.exportNode != null ? $"{exportParameters.exportNode.NodeName} | {exportParameters.outputPath}" : exportParameters.outputPath; RaiseMessage($"Exportation started: {fileExportString}", Color.Blue); ReportProgressChanged(0); string tempOutputDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); string outputDirectory = Path.GetDirectoryName(exportParameters.outputPath); string outputFileName = Path.GetFileName(exportParameters.outputPath); // Check directory exists if (!Directory.Exists(outputDirectory)) { RaiseError("Exportation stopped: Output folder does not exist"); ReportProgressChanged(100); return; } Directory.CreateDirectory(tempOutputDirectory); var outputBabylonDirectory = tempOutputDirectory; // Force output file extension to be babylon outputFileName = Path.ChangeExtension(outputFileName, "babylon"); var babylonScene = new BabylonScene(outputBabylonDirectory); var rawScene = Loader.Core.RootNode; var watch = new Stopwatch(); watch.Start(); string outputFormat = exportParameters.outputFormat; isBabylonExported = outputFormat == "babylon" || outputFormat == "binary babylon"; isGltfExported = outputFormat == "gltf" || outputFormat == "glb"; // Get scene parameters optimizeAnimations = !Loader.Core.RootNode.GetBoolProperty("babylonjs_donotoptimizeanimations"); exportNonAnimated = Loader.Core.RootNode.GetBoolProperty("babylonjs_animgroup_exportnonanimated"); // Save scene if (exportParameters.autoSave3dsMaxFile) { RaiseMessage("Saving 3ds max file"); var forceSave = Loader.Core.FileSave; callerForm?.BringToFront(); } // Producer babylonScene.producer = new BabylonProducer { name = "3dsmax", #if MAX2019 version = "2019", #elif MAX2018 version = "2018", #elif MAX2017 version = "2017", #else version = Loader.Core.ProductVersion.ToString(), #endif exporter_version = exporterVersion, file = outputFileName }; // Global babylonScene.autoClear = true; babylonScene.clearColor = Loader.Core.GetBackGround(0, Tools.Forever).ToArray(); babylonScene.ambientColor = Loader.Core.GetAmbient(0, Tools.Forever).ToArray(); babylonScene.gravity = rawScene.GetVector3Property("babylonjs_gravity"); ExportQuaternionsInsteadOfEulers = rawScene.GetBoolProperty("babylonjs_exportquaternions", 1); if (Loader.Core.UseEnvironmentMap && Loader.Core.EnvironmentMap != null) { // Environment texture var environmentMap = Loader.Core.EnvironmentMap; // Copy image file to output if necessary var babylonTexture = ExportEnvironmnentTexture(environmentMap, babylonScene); if (babylonTexture != null) { babylonScene.environmentTexture = babylonTexture.name; // Skybox babylonScene.createDefaultSkybox = rawScene.GetBoolProperty("babylonjs_createDefaultSkybox"); babylonScene.skyboxBlurLevel = rawScene.GetFloatProperty("babylonjs_skyboxBlurLevel"); } } // Sounds var soundName = rawScene.GetStringProperty("babylonjs_sound_filename", ""); if (!string.IsNullOrEmpty(soundName)) { var filename = Path.GetFileName(soundName); var globalSound = new BabylonSound { autoplay = rawScene.GetBoolProperty("babylonjs_sound_autoplay", 1), loop = rawScene.GetBoolProperty("babylonjs_sound_loop", 1), name = filename }; babylonScene.SoundsList.Add(globalSound); if (isBabylonExported) { try { File.Copy(soundName, Path.Combine(babylonScene.OutputPath, filename), true); } catch { } } } // Root nodes RaiseMessage("Exporting nodes"); HashSet <IIGameNode> maxRootNodes = getRootNodes(gameScene); var progressionStep = 80.0f / maxRootNodes.Count; var progression = 10.0f; ReportProgressChanged((int)progression); referencedMaterials.Clear(); Tools.guids.Clear(); // Reseting is optionnal. It makes each morph target manager export starts from id = 0. BabylonMorphTargetManager.Reset(); foreach (var maxRootNode in maxRootNodes) { BabylonNode node = exportNodeRec(maxRootNode, babylonScene, gameScene); // if we're exporting from a specific node, reset the pivot to {0,0,0} if (node != null && exportParameters.exportNode != null) { SetNodePosition(ref node, ref babylonScene, new float[] { 0, 0, 0 }); } progression += progressionStep; ReportProgressChanged((int)progression); CheckCancelled(); } ; RaiseMessage(string.Format("Total meshes: {0}", babylonScene.MeshesList.Count), Color.Gray, 1); // In 3DS Max the default camera look down (in the -z direction for the 3DS Max reference (+y for babylon)) // In Babylon the default camera look to the horizon (in the +z direction for the babylon reference) // In glTF the default camera look to the horizon (in the +Z direction for glTF reference) RaiseMessage("Update camera rotation and position", 1); for (int index = 0; index < babylonScene.CamerasList.Count; index++) { BabylonCamera camera = babylonScene.CamerasList[index]; FixCamera(ref camera, ref babylonScene); } // Light for glTF if (isGltfExported) { RaiseMessage("Update light rotation for glTF export", 1); for (int index = 0; index < babylonScene.LightsList.Count; index++) { BabylonNode light = babylonScene.LightsList[index]; FixNodeRotation(ref light, ref babylonScene, -Math.PI / 2); } } // Main camera BabylonCamera babylonMainCamera = null; ICameraObject maxMainCameraObject = null; if (babylonMainCamera == null && babylonScene.CamerasList.Count > 0) { // Set first camera as main one babylonMainCamera = babylonScene.CamerasList[0]; babylonScene.activeCameraID = babylonMainCamera.id; RaiseMessage("Active camera set to " + babylonMainCamera.name, Color.Green, 1, true); // Retreive camera node with same GUID var maxCameraNodesAsTab = gameScene.GetIGameNodeByType(Autodesk.Max.IGameObject.ObjectTypes.Camera); var maxCameraNodes = TabToList(maxCameraNodesAsTab); var maxMainCameraNode = maxCameraNodes.Find(_camera => _camera.MaxNode.GetGuid().ToString() == babylonMainCamera.id); maxMainCameraObject = (maxMainCameraNode.MaxNode.ObjectRef as ICameraObject); } if (babylonMainCamera == null) { RaiseWarning("No camera defined", 1); } else { RaiseMessage(string.Format("Total cameras: {0}", babylonScene.CamerasList.Count), Color.Gray, 1); } // Default light bool addDefaultLight = rawScene.GetBoolProperty("babylonjs_addDefaultLight", 1); if (addDefaultLight && babylonScene.LightsList.Count == 0) { RaiseWarning("No light defined", 1); RaiseWarning("A default hemispheric light was added for your convenience", 1); ExportDefaultLight(babylonScene); } else { RaiseMessage(string.Format("Total lights: {0}", babylonScene.LightsList.Count), Color.Gray, 1); } if (scaleFactorFloat != 1.0f) { RaiseMessage("A root node is added for scaling", 1); // Create root node for scaling BabylonMesh rootNode = new BabylonMesh { name = "root", id = Guid.NewGuid().ToString() }; rootNode.isDummy = true; float rootNodeScale = scaleFactorFloat; rootNode.scaling = new float[3] { rootNodeScale, rootNodeScale, rootNodeScale }; if (ExportQuaternionsInsteadOfEulers) { rootNode.rotationQuaternion = new float[] { 0, 0, 0, 1 }; } else { rootNode.rotation = new float[] { 0, 0, 0 }; } // Update all top nodes var babylonNodes = new List <BabylonNode>(); babylonNodes.AddRange(babylonScene.MeshesList); babylonNodes.AddRange(babylonScene.CamerasList); babylonNodes.AddRange(babylonScene.LightsList); foreach (BabylonNode babylonNode in babylonNodes) { if (babylonNode.parentId == null) { babylonNode.parentId = rootNode.id; } } // Store root node babylonScene.MeshesList.Add(rootNode); } // Materials if (exportParameters.exportMaterials) { RaiseMessage("Exporting materials"); var matsToExport = referencedMaterials.ToArray(); // Snapshot because multimaterials can export new materials foreach (var mat in matsToExport) { ExportMaterial(mat, babylonScene); CheckCancelled(); } RaiseMessage(string.Format("Total: {0}", babylonScene.MaterialsList.Count + babylonScene.MultiMaterialsList.Count), Color.Gray, 1); } else { RaiseMessage("Skipping material export."); } // Fog for (var index = 0; index < Loader.Core.NumAtmospheric; index++) { var atmospheric = Loader.Core.GetAtmospheric(index); if (atmospheric.Active(0) && atmospheric.ClassName == "Fog") { var fog = atmospheric as IStdFog; RaiseMessage("Exporting fog"); if (fog != null) { babylonScene.fogColor = fog.GetColor(0).ToArray(); babylonScene.fogMode = 3; } if (babylonMainCamera != null) { babylonScene.fogStart = maxMainCameraObject.GetEnvRange(0, 0, Tools.Forever); babylonScene.fogEnd = maxMainCameraObject.GetEnvRange(0, 1, Tools.Forever); } } } // Skeletons if (skins.Count > 0) { RaiseMessage("Exporting skeletons"); foreach (var skin in skins) { ExportSkin(skin, babylonScene); } } // Animation group if (isBabylonExported) { RaiseMessage("Export animation groups"); // add animation groups to the scene babylonScene.animationGroups = ExportAnimationGroups(babylonScene); // if there is animationGroup, then remove animations from nodes if (babylonScene.animationGroups.Count > 0) { foreach (BabylonNode node in babylonScene.MeshesList) { node.animations = null; } foreach (BabylonNode node in babylonScene.LightsList) { node.animations = null; } foreach (BabylonNode node in babylonScene.CamerasList) { node.animations = null; } foreach (BabylonSkeleton skel in babylonScene.SkeletonsList) { foreach (BabylonBone bone in skel.bones) { bone.animation = null; } } } } // Output babylonScene.Prepare(false, false); if (isBabylonExported) { RaiseMessage("Saving to output file"); var outputFile = Path.Combine(outputBabylonDirectory, outputFileName); var jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings()); var sb = new StringBuilder(); var sw = new StringWriter(sb, CultureInfo.InvariantCulture); using (var jsonWriter = new JsonTextWriterOptimized(sw)) { jsonWriter.Formatting = Formatting.None; jsonSerializer.Serialize(jsonWriter, babylonScene); } File.WriteAllText(outputFile, sb.ToString()); if (exportParameters.generateManifest) { File.WriteAllText(outputFile + ".manifest", "{\r\n\"version\" : 1,\r\n\"enableSceneOffline\" : true,\r\n\"enableTexturesOffline\" : true\r\n}"); } // Binary if (outputFormat == "binary babylon") { RaiseMessage("Generating binary files"); BabylonFileConverter.BinaryConverter.Convert(outputFile, outputBabylonDirectory + "\\Binary", message => RaiseMessage(message, 1), error => RaiseError(error, 1)); } } ReportProgressChanged(100); // Export glTF if (isGltfExported) { bool generateBinary = outputFormat == "glb"; ExportGltf(babylonScene, tempOutputDirectory, outputFileName, generateBinary); } // Move files to output directory var filePaths = Directory.GetFiles(tempOutputDirectory); if (outputFormat == "binary babylon") { var tempBinaryOutputDirectory = Path.Combine(tempOutputDirectory, "Binary"); var binaryFilePaths = Directory.GetFiles(tempBinaryOutputDirectory); foreach (var filePath in binaryFilePaths) { if (filePath.EndsWith(".binary.babylon")) { var file = Path.GetFileName(filePath); var tempFilePath = Path.Combine(tempBinaryOutputDirectory, file); var outputFile = Path.Combine(outputDirectory, file); moveFileToOutputDirectory(tempFilePath, outputFile, exportParameters); } else if (filePath.EndsWith(".babylonbinarymeshdata")) { var file = Path.GetFileName(filePath); var tempFilePath = Path.Combine(tempBinaryOutputDirectory, file); var outputFile = Path.Combine(outputDirectory, file); moveFileToOutputDirectory(tempFilePath, outputFile, exportParameters); } } } if (outputFormat == "glb") { foreach (var file_path in filePaths) { if (Path.GetExtension(file_path) == ".glb") { var file = Path.GetFileName(file_path); var tempFilePath = Path.Combine(tempOutputDirectory, file); var outputFile = Path.Combine(outputDirectory, file); moveFileToOutputDirectory(tempFilePath, outputFile, exportParameters); break; } } } else { foreach (var filePath in filePaths) { var file = Path.GetFileName(filePath); var outputPath = Path.Combine(outputDirectory, file); var tempFilePath = Path.Combine(tempOutputDirectory, file); moveFileToOutputDirectory(tempFilePath, outputPath, exportParameters); } } Directory.Delete(tempOutputDirectory, true); watch.Stop(); RaiseMessage(string.Format("Exportation done in {0:0.00}s: {1}", watch.ElapsedMilliseconds / 1000.0, fileExportString), Color.Blue); }
/// <summary> /// Export to file /// </summary> /// <param name="outputDirectory">The directory to store the generated files</param> /// <param name="outputFileName">The filename to use for the generated files</param> /// <param name="outputFormat">The format to use for the generated files</param> /// <param name="generateManifest">Specifies if a manifest file should be generated</param> /// <param name="onlySelected">Specifies if only the selected objects should be exported</param> /// <param name="autoSaveMayaFile">Specifies if the Maya scene should be auto-saved</param> /// <param name="exportHiddenObjects">Specifies if hidden objects should be exported</param> /// <param name="copyTexturesToOutput">Specifies if textures should be copied to the output directory</param> /// <param name="optimizeVertices">Specifies if vertices should be optimized on export</param> /// <param name="exportTangents">Specifies if tangents should be exported</param> /// <param name="scaleFactor">Scales the scene by this factor</param> /// <param name="exportSkin">Specifies if skins should be exported</param> /// <param name="quality">The texture quality</param> /// <param name="dracoCompression">Specifies if draco compression should be used</param> /// <param name="exportMorphNormal">Specifies if normals should be exported for morph targets</param> /// <param name="exportMorphTangent">Specifies if tangents should be exported for morph targets</param> /// <param name="exportKHRLightsPunctual">Specifies if the KHR_lights_punctual extension should be enabled</param> /// <param name="exportKHRTextureTransform">Specifies if the KHR_texture_transform extension should be enabled</param> public void Export(string outputDirectory, string outputFileName, string outputFormat, bool generateManifest, bool onlySelected, bool autoSaveMayaFile, bool exportHiddenObjects, bool copyTexturesToOutput, bool optimizeVertices, bool exportTangents, string scaleFactor, bool exportSkin, string quality, bool dracoCompression, bool exportMorphNormal, bool exportMorphTangent, bool exportKHRLightsPunctual, bool exportKHRTextureTransform) { // Check if the animation is running MGlobal.executeCommand("play -q - state", out int isPlayed); if (isPlayed == 1) { RaiseError("Stop the animation before exporting."); return; } // Check input text is valid var scaleFactorFloat = 1.0f; try { scaleFactor = scaleFactor.Replace(".", System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator); scaleFactor = scaleFactor.Replace(",", System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator); scaleFactorFloat = float.Parse(scaleFactor); } catch { RaiseError("Scale factor is not a valid number."); return; } try { _quality = long.Parse(quality); if (_quality < 0 || _quality > 100) { throw new Exception(); } } catch { RaiseError("Quality is not a valid number. It should be an integer between 0 and 100."); RaiseError("This parameter set the quality of jpg compression."); return; } RaiseMessage("Export started", Color.Blue); var progression = 0.0f; ReportProgressChanged(progression); // Store export options _onlySelected = onlySelected; _exportHiddenObjects = exportHiddenObjects; _optimizeVertices = optimizeVertices; _exportTangents = exportTangents; CopyTexturesToOutput = copyTexturesToOutput; isBabylonExported = outputFormat == "babylon" || outputFormat == "binary babylon"; _exportSkin = exportSkin; _dracoCompression = dracoCompression; _exportMorphNormal = exportMorphNormal; _exportMorphTangent = exportMorphTangent; _exportKHRLightsPunctual = exportKHRLightsPunctual; _exportKHRTextureTransform = exportKHRTextureTransform; // Check directory exists if (!Directory.Exists(outputDirectory)) { RaiseError("Export stopped: Output folder does not exist"); ReportProgressChanged(100); return; } var watch = new Stopwatch(); watch.Start(); var outputBabylonDirectory = outputDirectory; var babylonScene = new BabylonScene(outputBabylonDirectory); // Save scene if (autoSaveMayaFile) { RaiseMessage("Saving Maya file"); // Query expand file name string fileName = MGlobal.executeCommandStringResult($@"file -q -exn;"); // If scene has already been saved previously if (fileName.EndsWith(".ma") || fileName.EndsWith(".mb")) { // Name is already specified and this line will not fail MFileIO.save(); } else { // Open SaveAs dialog window MGlobal.executeCommand($@"fileDialog2;"); } } // Force output file extension to be babylon outputFileName = Path.ChangeExtension(outputFileName, "babylon"); // Store selected nodes MSelectionList selectedNodes = new MSelectionList(); MGlobal.getActiveSelectionList(selectedNodes); selectedNodeFullPaths = new List <string>(); MItSelectionList mItSelectionList = new MItSelectionList(selectedNodes); while (!mItSelectionList.isDone) { MDagPath mDagPath = new MDagPath(); try { mItSelectionList.getDagPath(mDagPath); selectedNodeFullPaths.Add(mDagPath.fullPathName); } catch { // selected object is not a DAG object // fail silently } mItSelectionList.next(); } if (selectedNodeFullPaths.Count > 0) { RaiseMessage("Selected nodes full path"); foreach (string selectedNodeFullPath in selectedNodeFullPaths) { RaiseMessage(selectedNodeFullPath, 1); } } // Producer babylonScene.producer = new BabylonProducer { name = "Maya", version = "2018", exporter_version = exporterVersion, file = outputFileName }; // Global babylonScene.autoClear = true; // TODO - Retreive colors from Maya //babylonScene.clearColor = Loader.Core.GetBackGround(0, Tools.Forever).ToArray(); //babylonScene.ambientColor = Loader.Core.GetAmbient(0, Tools.Forever).ToArray(); // TODO - Add custom properties _exportQuaternionsInsteadOfEulers = true; PrintDAG(true); PrintDAG(false); // Store the current frame. It can be change to find a proper one for the node/bone export double currentTime = Loader.GetCurrentTime(); // -------------------- // ------ Nodes ------- // -------------------- RaiseMessage("Exporting nodes"); // It makes each morph target manager export starts from id = 0. BabylonMorphTargetManager.Reset(); // Clear materials referencedMaterials.Clear(); multiMaterials.Clear(); // Get all nodes var dagIterator = new MItDag(MItDag.TraversalType.kDepthFirst, MFn.Type.kTransform); List <MDagPath> nodes = new List <MDagPath>(); while (!dagIterator.isDone) { MDagPath mDagPath = new MDagPath(); dagIterator.getPath(mDagPath); // Check if one of its descendant (direct or not) is a mesh/camera/light/locator if (isNodeRelevantToExportRec(mDagPath) // Ensure it's not one of the default cameras used as viewports in Maya && defaultCameraNames.Contains(mDagPath.partialPathName) == false) { nodes.Add(mDagPath); } else { // Skip descendants dagIterator.prune(); } dagIterator.next(); } // Export all nodes var progressionStep = 100.0f / nodes.Count; foreach (MDagPath mDagPath in nodes) { BabylonNode babylonNode = null; switch (getApiTypeOfDirectDescendants(mDagPath)) { case MFn.Type.kMesh: babylonNode = ExportMesh(mDagPath, babylonScene); break; case MFn.Type.kCamera: babylonNode = ExportCamera(mDagPath, babylonScene); break; case MFn.Type.kLight: // Lights api type are actually kPointLight, kSpotLight... babylonNode = ExportLight(mDagPath, babylonScene); break; case MFn.Type.kLocator: // Camera target babylonNode = ExportDummy(mDagPath, babylonScene); break; } // If node is not exported successfully if (babylonNode == null) { // Create a dummy (empty mesh) babylonNode = ExportDummy(mDagPath, babylonScene); } ; // Update progress bar progression += progressionStep; ReportProgressChanged(progression); CheckCancelled(); } RaiseMessage(string.Format("Total meshes: {0}", babylonScene.MeshesList.Count), Color.Gray, 1); // if nothing is enlightened, exclude all meshes foreach (BabylonLight light in babylonScene.LightsList) { if (light.includedOnlyMeshesIds.Length == 0) { light.excludedMeshesIds = babylonScene.MeshesList.Select(m => m.id).ToArray(); } } /* * Switch coordinate system at global level * * Add a root node with negative scaling * Pros - It's safer to use a root node * Cons - It's cleaner to switch at object level (as it is done now) * Use root node method when you want to be 100% sure of the output * Don't forget to also inverse winding order of mesh indices */ //// Switch from right to left handed coordinate system //MUuid mUuid = new MUuid(); //mUuid.generate(); //var rootNode = new BabylonMesh //{ // name = "root", // id = mUuid.asString(), // scaling = new float[] { 1, 1, -1 } //}; //foreach(var babylonMesh in babylonScene.MeshesList) //{ // // Add root meshes as child to root node // if (babylonMesh.parentId == null) // { // babylonMesh.parentId = rootNode.id; // } //} //babylonScene.MeshesList.Add(rootNode); // Main camera BabylonCamera babylonMainCamera = null; if (babylonScene.CamerasList.Count > 0) { // Set first camera as main one babylonMainCamera = babylonScene.CamerasList[0]; babylonScene.activeCameraID = babylonMainCamera.id; RaiseMessage("Active camera set to " + babylonMainCamera.name, Color.Green, 1, true); } if (babylonMainCamera == null) { RaiseWarning("No camera defined", 1); } else { RaiseMessage(string.Format("Total cameras: {0}", babylonScene.CamerasList.Count), Color.Gray, 1); } // Default light if (babylonScene.LightsList.Count == 0) { RaiseWarning("No light defined", 1); RaiseWarning("A default ambient light was added for your convenience", 1); ExportDefaultLight(babylonScene); } else { RaiseMessage(string.Format("Total lights: {0}", babylonScene.LightsList.Count), Color.Gray, 1); } if (scaleFactorFloat != 1.0f) { RaiseMessage("A root node is added for scaling", 1); // Create root node for scaling BabylonMesh rootNode = new BabylonMesh { name = "root", id = Tools.GenerateUUID() }; rootNode.isDummy = true; float rootNodeScale = scaleFactorFloat; rootNode.scaling = new float[3] { rootNodeScale, rootNodeScale, rootNodeScale }; if (ExportQuaternionsInsteadOfEulers) { rootNode.rotationQuaternion = new float[] { 0, 0, 0, 1 }; } else { rootNode.rotation = new float[] { 0, 0, 0 }; } // Update all top nodes var babylonNodes = new List <BabylonNode>(); babylonNodes.AddRange(babylonScene.MeshesList); babylonNodes.AddRange(babylonScene.CamerasList); babylonNodes.AddRange(babylonScene.LightsList); foreach (BabylonNode babylonNode in babylonNodes) { if (babylonNode.parentId == null) { babylonNode.parentId = rootNode.id; } } // Store root node babylonScene.MeshesList.Add(rootNode); } // -------------------- // ----- Materials ---- // -------------------- RaiseMessage("Exporting materials"); GenerateMaterialDuplicationDatas(babylonScene); foreach (var mat in referencedMaterials) { ExportMaterial(mat, babylonScene); CheckCancelled(); } foreach (var mat in multiMaterials) { ExportMultiMaterial(mat.Key, mat.Value, babylonScene); CheckCancelled(); } UpdateMeshesMaterialId(babylonScene); RaiseMessage(string.Format("Total: {0}", babylonScene.MaterialsList.Count + babylonScene.MultiMaterialsList.Count), Color.Gray, 1); // Export skeletons if (_exportSkin && skins.Count > 0) { progressSkin = 0; progressSkinStep = 100 / skins.Count; ReportProgressChanged(progressSkin); RaiseMessage("Exporting skeletons"); foreach (var skin in skins) { ExportSkin(skin, babylonScene); } } // set back the frame Loader.SetCurrentTime(currentTime); // Animation group if (isBabylonExported) { RaiseMessage("Export animation groups"); // add animation groups to the scene babylonScene.animationGroups = ExportAnimationGroups(babylonScene); // if there is animationGroup, then remove animations from nodes if (babylonScene.animationGroups.Count > 0) { // add animations of each nodes in the animGroup List <BabylonNode> babylonNodes = new List <BabylonNode>(); babylonNodes.AddRange(babylonScene.MeshesList); babylonNodes.AddRange(babylonScene.CamerasList); babylonNodes.AddRange(babylonScene.LightsList); foreach (BabylonNode node in babylonNodes) { node.animations = null; } foreach (BabylonSkeleton skel in babylonScene.SkeletonsList) { foreach (BabylonBone bone in skel.bones) { bone.animation = null; } } } } // Output babylonScene.Prepare(false, false); if (isBabylonExported) { Write(babylonScene, outputBabylonDirectory, outputFileName, outputFormat, generateManifest); } ReportProgressChanged(100); // Export glTF if (outputFormat == "gltf" || outputFormat == "glb") { bool generateBinary = outputFormat == "glb"; ExportGltf(babylonScene, outputDirectory, outputFileName, generateBinary); } watch.Stop(); RaiseMessage(string.Format("Export done in {0:0.00}s", watch.ElapsedMilliseconds / 1000.0), Color.Blue); }
private bool ExportMorphTargetWeightAnimation(BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFNode gltfNode, List <GLTFChannel> channelList, List <GLTFAnimationSampler> samplerList, int startFrame, int endFrame) { if (!_isBabylonMorphTargetManagerAnimationValid(babylonMorphTargetManager)) { return(false); } RaiseMessage("GLTFExporter.Animation | Export animation of morph target manager with id: " + babylonMorphTargetManager.id, 2); // Target var gltfTarget = new GLTFChannelTarget { node = gltfNode.index }; gltfTarget.path = "weights"; // Buffer var buffer = GLTFBufferService.Instance.GetBuffer(gltf); // --- Input --- var accessorInput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationInput", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor accessorInput.min = new float[] { float.MaxValue }; accessorInput.max = new float[] { float.MinValue }; var influencesPerFrame = _getTargetManagerAnimationsData(babylonMorphTargetManager); var frames = new List <int>(influencesPerFrame.Keys); frames.Sort(); // Mandatory otherwise gltf loader of babylon doesn't understand int numKeys = 0; foreach (var frame in frames) { if (frame < startFrame) { continue; } if (frame > endFrame) { continue; } numKeys++; var inputValue = frame / (float)Loader.Global.FrameRate; // Store values as bytes accessorInput.bytesList.AddRange(BitConverter.GetBytes(inputValue)); // Update min and max values GLTFBufferService.UpdateMinMaxAccessor(accessorInput, inputValue); } accessorInput.count = numKeys; // bail out if we have no keys to export (?) // todo [KeyInterpolation]: bail out only when there are no keyframes at all (?) and otherwise add the appropriate (interpolated) keyframes if (numKeys == 0) { return(false); } // --- Output --- GLTFAccessor accessorOutput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationWeights", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor foreach (var frame in frames) { if (frame < startFrame) { continue; } if (frame > endFrame) { continue; } var outputValues = influencesPerFrame[frame]; // Store values as bytes foreach (var outputValue in outputValues) { accessorOutput.count++; accessorOutput.bytesList.AddRange(BitConverter.GetBytes(outputValue)); } } // Animation sampler var gltfAnimationSampler = new GLTFAnimationSampler { input = accessorInput.index, output = accessorOutput.index }; gltfAnimationSampler.index = samplerList.Count; samplerList.Add(gltfAnimationSampler); // Channel var gltfChannel = new GLTFChannel { sampler = gltfAnimationSampler.index, target = gltfTarget }; channelList.Add(gltfChannel); return(true); }
/// <summary> /// Export to file /// </summary> /// <param name="outputDirectory">The directory to store the generated files</param> /// <param name="outputFileName">The filename to use for the generated files</param> /// <param name="outputFormat">The format to use for the generated files</param> /// <param name="generateManifest">Specifies if a manifest file should be generated</param> /// <param name="onlySelected">Specifies if only the selected objects should be exported</param> /// <param name="autoSaveMayaFile">Specifies if the Maya scene should be auto-saved</param> /// <param name="exportHiddenObjects">Specifies if hidden objects should be exported</param> /// <param name="copyTexturesToOutput">Specifies if textures should be copied to the output directory</param> /// <param name="optimizeVertices">Specifies if vertices should be optimized on export</param> /// <param name="exportTangents">Specifies if tangents should be exported</param> /// <param name="scaleFactor">Scales the scene by this factor</param> /// <param name="exportSkin">Specifies if skins should be exported</param> /// <param name="quality">The texture quality</param> /// <param name="dracoCompression">Specifies if draco compression should be used</param> /// <param name="exportMorphNormal">Specifies if normals should be exported for morph targets</param> /// <param name="exportMorphTangent">Specifies if tangents should be exported for morph targets</param> /// <param name="exportKHRLightsPunctual">Specifies if the KHR_lights_punctual extension should be enabled</param> /// <param name="exportKHRTextureTransform">Specifies if the KHR_texture_transform extension should be enabled</param> /// <param name="bakeAnimationFrames">Specifies if animations should be exporting keyframes directly or should manually bake out animations frame by frame</param> public void Export(ExportParameters exportParameters) { this.exportParameters = exportParameters; // Check if the animation is running MGlobal.executeCommand("play -q - state", out int isPlayed); if (isPlayed == 1) { RaiseError("Stop the animation before exporting."); return; } RaiseMessage("Export started", Color.Blue); var progression = 0.0f; ReportProgressChanged(progression); // Store export options this.isBabylonExported = exportParameters.outputFormat == "babylon" || exportParameters.outputFormat == "binary babylon"; var outputBabylonDirectory = Path.GetDirectoryName(exportParameters.outputPath); // Check directory exists if (!Directory.Exists(outputBabylonDirectory)) { RaiseError("Export stopped: Output folder does not exist"); ReportProgressChanged(100); return; } var watch = new Stopwatch(); watch.Start(); var babylonScene = new BabylonScene(outputBabylonDirectory); // Save scene if (exportParameters.autoSaveSceneFile) { RaiseMessage("Saving Maya file"); // Query expand file name string fileName = MGlobal.executeCommandStringResult($@"file -q -exn;"); // If scene has already been saved previously if (fileName.EndsWith(".ma") || fileName.EndsWith(".mb")) { // Name is already specified and this line will not fail MFileIO.save(); } else { // Open SaveAs dialog window MGlobal.executeCommand($@"fileDialog2;"); } } // Force output file extension to be babylon var outputFileName = Path.ChangeExtension(Path.GetFileName(exportParameters.outputPath), "babylon"); // Store selected nodes MSelectionList selectedNodes = new MSelectionList(); MGlobal.getActiveSelectionList(selectedNodes); selectedNodeFullPaths = new List <string>(); MItSelectionList mItSelectionList = new MItSelectionList(selectedNodes); while (!mItSelectionList.isDone) { MDagPath mDagPath = new MDagPath(); try { mItSelectionList.getDagPath(mDagPath); selectedNodeFullPaths.Add(mDagPath.fullPathName); } catch { // selected object is not a DAG object // fail silently } mItSelectionList.next(); } if (selectedNodeFullPaths.Count > 0) { RaiseMessage("Selected nodes full path"); foreach (string selectedNodeFullPath in selectedNodeFullPaths) { RaiseMessage(selectedNodeFullPath, 1); } } // Producer babylonScene.producer = new BabylonProducer { name = "Maya", version = "2018", exporter_version = exporterVersion, file = outputFileName }; // Global babylonScene.autoClear = true; // TODO - Retreive colors from Maya //babylonScene.clearColor = Loader.Core.GetBackGround(0, Tools.Forever).ToArray(); //babylonScene.ambientColor = Loader.Core.GetAmbient(0, Tools.Forever).ToArray(); babylonScene.TimelineStartFrame = Loader.GetMinTime(); babylonScene.TimelineEndFrame = Loader.GetMaxTime(); babylonScene.TimelineFramesPerSecond = Loader.GetFPS(); // TODO - Add custom properties _exportQuaternionsInsteadOfEulers = true; PrintDAG(true); PrintDAG(false); // Store the current frame. It can be change to find a proper one for the node/bone export double currentTime = Loader.GetCurrentTime(); // -------------------- // ------ Nodes ------- // -------------------- RaiseMessage("Exporting nodes"); // It makes each morph target manager export starts from id = 0. BabylonMorphTargetManager.Reset(); // Clear materials referencedMaterials.Clear(); multiMaterials.Clear(); // Get all nodes var dagIterator = new MItDag(MItDag.TraversalType.kDepthFirst, MFn.Type.kTransform); List <MDagPath> nodes = new List <MDagPath>(); while (!dagIterator.isDone) { MDagPath mDagPath = new MDagPath(); dagIterator.getPath(mDagPath); // Check if one of its descendant (direct or not) is a mesh/camera/light/locator if (isNodeRelevantToExportRec(mDagPath) // Ensure it's not one of the default cameras used as viewports in Maya && defaultCameraNames.Contains(mDagPath.partialPathName) == false) { nodes.Add(mDagPath); } else { // Skip descendants dagIterator.prune(); } dagIterator.next(); } // Export all nodes var progressionStep = 100.0f / nodes.Count; foreach (MDagPath mDagPath in nodes) { BabylonNode babylonNode = null; try { switch (getApiTypeOfDirectDescendants(mDagPath)) { case MFn.Type.kMesh: babylonNode = ExportMesh(mDagPath, babylonScene); break; case MFn.Type.kCamera: babylonNode = ExportCamera(mDagPath, babylonScene); break; case MFn.Type.kLight: // Lights api type are actually kPointLight, kSpotLight... babylonNode = ExportLight(mDagPath, babylonScene); break; case MFn.Type.kLocator: // Camera target babylonNode = ExportDummy(mDagPath, babylonScene); break; } } catch (Exception e) { this.RaiseWarning(String.Format("Exception raised during export. Node will be exported as dummy node. \r\nMessage: \r\n{0} \r\n{1}", e.Message, e.InnerException), 2); } // If node is not exported successfully if (babylonNode == null) { // Create a dummy (empty mesh) babylonNode = ExportDummy(mDagPath, babylonScene); } ; // Update progress bar progression += progressionStep; ReportProgressChanged(progression); CheckCancelled(); } RaiseMessage(string.Format("Total meshes: {0}", babylonScene.MeshesList.Count), Color.Gray, 1); // if nothing is enlightened, exclude all meshes foreach (BabylonLight light in babylonScene.LightsList) { if (light.includedOnlyMeshesIds.Length == 0) { light.excludedMeshesIds = babylonScene.MeshesList.Select(m => m.id).ToArray(); } } /* * Switch coordinate system at global level * * Add a root node with negative scaling * Pros - It's safer to use a root node * Cons - It's cleaner to switch at object level (as it is done now) * Use root node method when you want to be 100% sure of the output * Don't forget to also inverse winding order of mesh indices */ //// Switch from right to left handed coordinate system //MUuid mUuid = new MUuid(); //mUuid.generate(); //var rootNode = new BabylonMesh //{ // name = "root", // id = mUuid.asString(), // scaling = new float[] { 1, 1, -1 } //}; //foreach(var babylonMesh in babylonScene.MeshesList) //{ // // Add root meshes as child to root node // if (babylonMesh.parentId == null) // { // babylonMesh.parentId = rootNode.id; // } //} //babylonScene.MeshesList.Add(rootNode); // Main camera BabylonCamera babylonMainCamera = null; if (babylonScene.CamerasList.Count > 0) { // Set first camera as main one babylonMainCamera = babylonScene.CamerasList[0]; babylonScene.activeCameraID = babylonMainCamera.id; RaiseMessage("Active camera set to " + babylonMainCamera.name, Color.Green, 1, true); } if (babylonMainCamera == null) { RaiseWarning("No camera defined", 1); } else { RaiseMessage(string.Format("Total cameras: {0}", babylonScene.CamerasList.Count), Color.Gray, 1); } // Default light if (!exportParameters.pbrNoLight && babylonScene.LightsList.Count == 0) { RaiseWarning("No light defined", 1); RaiseWarning("A default ambient light was added for your convenience", 1); ExportDefaultLight(babylonScene); } else { RaiseMessage(string.Format("Total lights: {0}", babylonScene.LightsList.Count), Color.Gray, 1); } var sceneScaleFactor = exportParameters.scaleFactor; if (exportParameters.scaleFactor != 1.0f) { RaiseMessage(String.Format("A root node is added to globally scale the scene by {0}", sceneScaleFactor), 1); // Create root node for scaling BabylonMesh rootNode = new BabylonMesh { name = "root", id = Tools.GenerateUUID() }; rootNode.isDummy = true; float rootNodeScale = sceneScaleFactor; rootNode.scaling = new float[3] { rootNodeScale, rootNodeScale, rootNodeScale }; if (ExportQuaternionsInsteadOfEulers) { rootNode.rotationQuaternion = new float[] { 0, 0, 0, 1 }; } else { rootNode.rotation = new float[] { 0, 0, 0 }; } // Update all top nodes var babylonNodes = new List <BabylonNode>(); babylonNodes.AddRange(babylonScene.MeshesList); babylonNodes.AddRange(babylonScene.CamerasList); babylonNodes.AddRange(babylonScene.LightsList); foreach (BabylonNode babylonNode in babylonNodes) { if (babylonNode.parentId == null) { babylonNode.parentId = rootNode.id; } } // Store root node babylonScene.MeshesList.Add(rootNode); } // -------------------- // ----- Materials ---- // -------------------- RaiseMessage("Exporting materials"); GenerateMaterialDuplicationDatas(babylonScene); foreach (var mat in referencedMaterials) { ExportMaterial(mat, babylonScene, exportParameters.pbrFull); CheckCancelled(); } foreach (var mat in multiMaterials) { ExportMultiMaterial(mat.Key, mat.Value, babylonScene, exportParameters.pbrFull); CheckCancelled(); } UpdateMeshesMaterialId(babylonScene); RaiseMessage(string.Format("Total: {0}", babylonScene.MaterialsList.Count + babylonScene.MultiMaterialsList.Count), Color.Gray, 1); // Export skeletons if (exportParameters.exportSkins && skins.Count > 0) { progressSkin = 0; progressSkinStep = 100 / skins.Count; ReportProgressChanged(progressSkin); RaiseMessage("Exporting skeletons"); foreach (var skin in skins) { ExportSkin(skin, babylonScene); } } // set back the frame Loader.SetCurrentTime(currentTime); // ---------------------------- // ----- Animation groups ----- // ---------------------------- RaiseMessage("Export animation groups"); // add animation groups to the scene babylonScene.animationGroups = ExportAnimationGroups(babylonScene); if (isBabylonExported) { // if we are exporting to .Babylon then remove then remove animations from nodes if there are animation groups. if (babylonScene.animationGroups.Count > 0) { // add animations of each nodes in the animGroup List <BabylonNode> babylonNodes = new List <BabylonNode>(); babylonNodes.AddRange(babylonScene.MeshesList); babylonNodes.AddRange(babylonScene.CamerasList); babylonNodes.AddRange(babylonScene.LightsList); foreach (BabylonNode node in babylonNodes) { node.animations = null; } foreach (BabylonSkeleton skel in babylonScene.SkeletonsList) { foreach (BabylonBone bone in skel.bones) { bone.animation = null; } } } // setup a default skybox for the scene for .Babylon export. var sourcePath = exportParameters.pbrEnvironment; if (!string.IsNullOrEmpty(sourcePath)) { babylonScene.createDefaultSkybox = exportParameters.createDefaultSkybox; var fileName = Path.GetFileName(sourcePath); // Allow only dds file format if (!fileName.EndsWith(".dds")) { RaiseWarning("Failed to export defauenvironment texture: only .dds format is supported."); } else { RaiseMessage($"texture id = Max_Babylon_Default_Environment"); babylonScene.environmentTexture = fileName; if (exportParameters.writeTextures) { try { var destPath = Path.Combine(babylonScene.OutputPath, fileName); if (File.Exists(sourcePath) && sourcePath != destPath) { File.Copy(sourcePath, destPath, true); } } catch { // silently fails RaiseMessage($"Fail to export the default env texture", 3); } } } } } // Output babylonScene.Prepare(false, false); if (isBabylonExported) { Write(babylonScene, outputBabylonDirectory, outputFileName, exportParameters.outputFormat, exportParameters.generateManifest); } ReportProgressChanged(100); // Export glTF if (exportParameters.outputFormat == "gltf" || exportParameters.outputFormat == "glb") { bool generateBinary = exportParameters.outputFormat == "glb"; GLTFExporter gltfExporter = new GLTFExporter(); gltfExporter.ExportGltf(this.exportParameters, babylonScene, outputBabylonDirectory, outputFileName, generateBinary, this); } watch.Stop(); RaiseMessage(string.Format("Export done in {0:0.00}s", watch.ElapsedMilliseconds / 1000.0), Color.Blue); }
private bool ExportMorphTargetWeightAnimation(BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFNode gltfNode, List <GLTFChannel> channelList, List <GLTFAnimationSampler> samplerList, int startFrame, int endFrame, BabylonScene babylonScene, bool offsetToStartAtFrameZero = true) { if (exportedMorphTargets.Contains(babylonMorphTargetManager) || !_isBabylonMorphTargetManagerAnimationValid(babylonMorphTargetManager)) { return(false); } var influencesPerFrame = _getTargetManagerAnimationsData(babylonMorphTargetManager); var frames = new List <float>(influencesPerFrame.Keys); var framesInRange = frames.Where(frame => frame >= startFrame && frame <= endFrame).ToList(); framesInRange.Sort(); // Mandatory to sort otherwise gltf loader of babylon doesn't understand if (framesInRange.Count() <= 0) { return(false); } logger.RaiseMessage("GLTFExporter.Animation | Export animation of morph target manager with id: " + babylonMorphTargetManager.id, 2); // Target var gltfTarget = new GLTFChannelTarget { node = gltfNode.index }; gltfTarget.path = "weights"; // Buffer var buffer = GLTFBufferService.Instance.GetBuffer(gltf); // --- Input --- var accessorInput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationInput", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor accessorInput.min = new float[] { float.MaxValue }; accessorInput.max = new float[] { float.MinValue }; int numKeys = 0; foreach (var frame in framesInRange) { numKeys++; float inputValue = frame; if (offsetToStartAtFrameZero) { inputValue -= startFrame; } inputValue /= (float)babylonScene.TimelineFramesPerSecond; // Store values as bytes accessorInput.bytesList.AddRange(BitConverter.GetBytes(inputValue)); // Update min and max values GLTFBufferService.UpdateMinMaxAccessor(accessorInput, inputValue); } accessorInput.count = numKeys; if (accessorInput.count == 0) { logger.RaiseWarning(String.Format("GLTFExporter.Animation | No frames to export in morph target animation \"weight\" for mesh named \"{0}\". This will cause an error in the output gltf.", babylonMorphTargetManager.sourceMesh.name)); } // --- Output --- GLTFAccessor accessorOutput = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer), "accessorAnimationWeights", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.SCALAR ); // Populate accessor foreach (var frame in framesInRange) { var outputValues = influencesPerFrame[frame]; // Store values as bytes foreach (var outputValue in outputValues) { accessorOutput.count++; accessorOutput.bytesList.AddRange(BitConverter.GetBytes(outputValue)); } } // Animation sampler var gltfAnimationSampler = new GLTFAnimationSampler { input = accessorInput.index, output = accessorOutput.index }; gltfAnimationSampler.index = samplerList.Count; samplerList.Add(gltfAnimationSampler); // Channel var gltfChannel = new GLTFChannel { sampler = gltfAnimationSampler.index, target = gltfTarget }; channelList.Add(gltfChannel); // Mark this morph target as exported. exportedMorphTargets.Add(babylonMorphTargetManager); return(true); }
private void ExportAnimationGroups(GLTF gltf, BabylonScene babylonScene) { // Retreive and parse animation group data var animationGroupList = babylonScene.animationGroups; var animationGroupCount = animationGroupList == null ? 0 : animationGroupList.Count; gltf.AnimationsList.Clear(); gltf.AnimationsList.Capacity = Math.Max(gltf.AnimationsList.Capacity, animationGroupCount); if (animationGroupCount <= 0) { logger.RaiseMessage("GLTFExporter.Animation | No AnimationGroups: exporting all animations together.", 1); GLTFAnimation gltfAnimation = new GLTFAnimation(); gltfAnimation.name = "All Animations"; int startFrame = babylonScene.TimelineStartFrame; int endFrame = babylonScene.TimelineEndFrame; foreach (var pair in nodeToGltfNodeMap) { BabylonNode node = pair.Key; GLTFNode gltfNode = pair.Value; bool nodeHasAnimations = node.animations != null && node.animations.Length > 0 && node.animations[0] != null; bool nodeHasExtraAnimations = node.extraAnimations != null && node.extraAnimations.Count > 0 && node.extraAnimations[0] != null; BabylonMesh meshNode = node as BabylonMesh; BabylonMorphTargetManager morphTargetManager = null; bool nodeHasAnimatedMorphTargets = false; if (meshNode != null && meshNode.morphTargetManagerId != null) { morphTargetManager = GetBabylonMorphTargetManager(babylonScene, meshNode); if (morphTargetManager != null) { nodeHasAnimatedMorphTargets = morphTargetManager.targets.Any(target => target.animations != null && target.animations.Length > 0 && target.animations[0] != null); } } if (!nodeHasAnimations && !nodeHasExtraAnimations && !nodeHasAnimatedMorphTargets) { continue; } if (nodeHasAnimations && node.animations[0].property == "_matrix") { ExportBoneAnimation(gltfAnimation, startFrame, endFrame, gltf, node, pair.Value); } else { ExportNodeAnimation(gltfAnimation, startFrame, endFrame, gltf, node, gltfNode, babylonScene); } if (nodeHasAnimatedMorphTargets) { ExportMorphTargetWeightAnimation(morphTargetManager, gltf, gltfNode, gltfAnimation.ChannelList, gltfAnimation.SamplerList, startFrame, endFrame, babylonScene); } } if (gltfAnimation.ChannelList.Count > 0) { gltf.AnimationsList.Add(gltfAnimation); } else { logger.RaiseMessage("GLTFExporter.Animation | No animation data for this animation, it is ignored.", 2); } } else { foreach (BabylonAnimationGroup animGroup in animationGroupList) { logger.RaiseMessage("GLTFExporter.Animation | " + animGroup.name, 1); GLTFAnimation gltfAnimation = new GLTFAnimation(); gltfAnimation.name = animGroup.name; int startFrame = MathUtilities.RoundToInt(animGroup.from); int endFrame = MathUtilities.RoundToInt(animGroup.to); var uniqueNodeIds = animGroup.targetedAnimations.Select(targetAnim => targetAnim.targetId).Distinct(); foreach (var id in uniqueNodeIds) { BabylonNode babylonNode = babylonNodes.Find(node => node.id.Equals(id)); GLTFNode gltfNode = null; // search the babylon scene id map for the babylon node that matches this id if (babylonNode != null) { BabylonMorphTargetManager morphTargetManager = null; // search our babylon->gltf node mapping to see if this node is included in the exported gltf scene if (!nodeToGltfNodeMap.TryGetValue(babylonNode, out gltfNode)) { continue; } bool nodeHasAnimations = babylonNode.animations != null && babylonNode.animations.Length > 0 && babylonNode.animations[0] != null; bool nodeHasExtraAnimations = babylonNode.extraAnimations != null && babylonNode.extraAnimations.Count > 0 && babylonNode.extraAnimations[0] != null; if (!nodeHasAnimations && !nodeHasExtraAnimations) { continue; } if (nodeHasAnimations && babylonNode.animations[0].property == "_matrix") //TODO: Is this check accurate for deciphering between bones and nodes? { ExportBoneAnimation(gltfAnimation, startFrame, endFrame, gltf, babylonNode, gltfNode, animGroup); } else { ExportNodeAnimation(gltfAnimation, startFrame, endFrame, gltf, babylonNode, gltfNode, babylonScene, animGroup); } } else { // if the node isn't found in the scene id map, check if it is the id for a morph target BabylonMorphTargetManager morphTargetManager = babylonScene.morphTargetManagers.FirstOrDefault(mtm => mtm.targets.Any(target => target.animations != null && target.animations.Length > 0 && target.animations[0] != null)); if (morphTargetManager != null) { BabylonMesh mesh = morphTargetManager.sourceMesh; if (mesh != null && nodeToGltfNodeMap.TryGetValue(mesh, out gltfNode)) { ExportMorphTargetWeightAnimation(morphTargetManager, gltf, gltfNode, gltfAnimation.ChannelList, gltfAnimation.SamplerList, startFrame, endFrame, babylonScene); } } } } if (gltfAnimation.ChannelList.Count > 0) { gltf.AnimationsList.Add(gltfAnimation); } else { logger.RaiseMessage("No data exported for this animation, it is ignored.", 2); } // clear the exported morph target cache, since we are exporting a new animation group. //TODO: we should probably do this more elegantly. exportedMorphTargets.Clear(); } } }
private BabylonNode ExportMasterMesh(IIGameScene scene, IIGameNode meshNode, BabylonScene babylonScene) { var gameMesh = meshNode.IGameObject.AsGameMesh(); try { bool initialized = gameMesh.InitializeData; // needed, the property is in fact a method initializing the exporter that has wrongly been auto // translated into a property because it has no parameters } catch (Exception e) { RaiseWarning($"Mesh {meshNode.Name} failed to initialize. Mesh is exported as dummy.", 2); return(ExportDummy(scene, meshNode, babylonScene)); } var babylonMesh = new BabylonMesh { name = meshNode.Name, id = meshNode.MaxNode.GetGuid().ToString() }; // Position / rotation / scaling / hierarchy exportNode(babylonMesh, meshNode, scene, babylonScene); // Sounds var soundName = meshNode.MaxNode.GetStringProperty("babylonjs_sound_filename", ""); if (!string.IsNullOrEmpty(soundName)) { var filename = Path.GetFileName(soundName); var meshSound = new BabylonSound { name = filename, autoplay = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_autoplay", 1), loop = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_loop", 1), volume = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_volume", 1.0f), playbackRate = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_playbackrate", 1.0f), connectedMeshId = babylonMesh.id, isDirectional = false, spatialSound = false, distanceModel = meshNode.MaxNode.GetStringProperty("babylonjs_sound_distancemodel", "linear"), maxDistance = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_maxdistance", 100f), rolloffFactor = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_rolloff", 1.0f), refDistance = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_refdistance", 1.0f), }; var isDirectional = meshNode.MaxNode.GetBoolProperty("babylonjs_sound_directional"); if (isDirectional) { meshSound.isDirectional = true; meshSound.coneInnerAngle = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneinnerangle", 360f); meshSound.coneOuterAngle = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneouterangle", 360f); meshSound.coneOuterGain = meshNode.MaxNode.GetFloatProperty("babylonjs_sound_coneoutergain", 1.0f); } babylonScene.SoundsList.Add(meshSound); if (isBabylonExported) { try { File.Copy(soundName, Path.Combine(babylonScene.OutputPath, filename), true); } catch { } } } // Misc. #if MAX2017 || MAX2018 || MAX2019 babylonMesh.isVisible = meshNode.MaxNode.Renderable; babylonMesh.receiveShadows = meshNode.MaxNode.RcvShadows; babylonMesh.applyFog = meshNode.MaxNode.ApplyAtmospherics; #else babylonMesh.isVisible = meshNode.MaxNode.Renderable == 1; babylonMesh.receiveShadows = meshNode.MaxNode.RcvShadows == 1; babylonMesh.applyFog = meshNode.MaxNode.ApplyAtmospherics == 1; #endif babylonMesh.pickable = meshNode.MaxNode.GetBoolProperty("babylonjs_checkpickable"); babylonMesh.showBoundingBox = meshNode.MaxNode.GetBoolProperty("babylonjs_showboundingbox"); babylonMesh.showSubMeshesBoundingBox = meshNode.MaxNode.GetBoolProperty("babylonjs_showsubmeshesboundingbox"); babylonMesh.alphaIndex = (int)meshNode.MaxNode.GetFloatProperty("babylonjs_alphaindex", 1000); // Collisions babylonMesh.checkCollisions = meshNode.MaxNode.GetBoolProperty("babylonjs_checkcollisions"); // Skin var isSkinned = gameMesh.IsObjectSkinned; var skin = gameMesh.IGameSkin; var unskinnedMesh = gameMesh; IGMatrix skinInitPoseMatrix = Loader.Global.GMatrix.Create(Loader.Global.Matrix3.Create(true)); List <int> boneIds = null; int maxNbBones = 0; if (isSkinned && GetRelevantNodes(skin).Count > 0) // if the mesh has a skin with at least one bone { var skinAlreadyStored = skins.Find(_skin => IsSkinEqualTo(_skin, skin)); if (skinAlreadyStored == null) { skins.Add(skin); babylonMesh.skeletonId = skins.IndexOf(skin); } else { babylonMesh.skeletonId = skins.IndexOf(skinAlreadyStored); } skin.GetInitSkinTM(skinInitPoseMatrix); boneIds = GetNodeIndices(skin); } else { skin = null; } // Mesh if (unskinnedMesh.IGameType == Autodesk.Max.IGameObject.ObjectTypes.Mesh && unskinnedMesh.MaxMesh != null) { if (unskinnedMesh.NumberOfFaces < 1) { RaiseError($"Mesh {babylonMesh.name} has no face", 2); } if (unskinnedMesh.NumberOfVerts < 3) { RaiseError($"Mesh {babylonMesh.name} has not enough vertices", 2); } if (unskinnedMesh.NumberOfVerts >= 65536) { RaiseWarning($"Mesh {babylonMesh.name} has tmore than 65536 vertices which means that it will require specific WebGL extension to be rendered. This may impact portability of your scene on low end devices.", 2); } if (skin != null) { for (var vertexIndex = 0; vertexIndex < unskinnedMesh.NumberOfVerts; vertexIndex++) { maxNbBones = Math.Max(maxNbBones, skin.GetNumberOfBones(vertexIndex)); } } // Physics var impostorText = meshNode.MaxNode.GetStringProperty("babylonjs_impostor", "None"); if (impostorText != "None") { switch (impostorText) { case "Sphere": babylonMesh.physicsImpostor = 1; break; case "Box": babylonMesh.physicsImpostor = 2; break; case "Plane": babylonMesh.physicsImpostor = 3; break; default: babylonMesh.physicsImpostor = 0; break; } babylonMesh.physicsMass = meshNode.MaxNode.GetFloatProperty("babylonjs_mass"); babylonMesh.physicsFriction = meshNode.MaxNode.GetFloatProperty("babylonjs_friction", 0.2f); babylonMesh.physicsRestitution = meshNode.MaxNode.GetFloatProperty("babylonjs_restitution", 0.2f); } // Material var mtl = meshNode.NodeMaterial; var multiMatsCount = 1; // The DirectXShader material is a passthrough to its render material. // The shell material is a passthrough to its baked material. while (mtl != null && (isShellMaterial(mtl) || isDirectXShaderMaterial(mtl))) { if (isShellMaterial(mtl)) { // Retrieve the baked material from the shell material. mtl = GetBakedMaterialFromShellMaterial(mtl); } else // isDirectXShaderMaterial(mtl) { // Retrieve the render material from the directX shader mtl = GetRenderMaterialFromDirectXShader(mtl); } } if (mtl != null) { IIGameMaterial unsupportedMaterial = isMaterialSupported(mtl); if (unsupportedMaterial == null) { babylonMesh.materialId = mtl.MaxMaterial.GetGuid().ToString(); if (!referencedMaterials.Contains(mtl)) { referencedMaterials.Add(mtl); } multiMatsCount = Math.Max(mtl.SubMaterialCount, 1); } else { if (mtl.SubMaterialCount == 0 || mtl == unsupportedMaterial) { RaiseWarning("Unsupported material type '" + unsupportedMaterial.MaterialClass + "'. Material is ignored.", 2); } else { RaiseWarning("Unsupported sub-material type '" + unsupportedMaterial.MaterialClass + "'. Material is ignored.", 2); } } } babylonMesh.visibility = meshNode.MaxNode.GetVisibility(0, Tools.Forever); var vertices = new List <GlobalVertex>(); var indices = new List <int>(); var mappingChannels = unskinnedMesh.ActiveMapChannelNum; bool hasUV = false; bool hasUV2 = false; for (int i = 0; i < mappingChannels.Count; ++i) { #if MAX2017 || MAX2018 || MAX2019 var channelNum = mappingChannels[i]; #else var channelNum = mappingChannels[new IntPtr(i)]; #endif if (channelNum == 1) { hasUV = true; } else if (channelNum == 2) { hasUV2 = true; } } var hasColor = unskinnedMesh.NumberOfColorVerts > 0; var hasAlpha = unskinnedMesh.GetNumberOfMapVerts(-2) > 0; var optimizeVertices = meshNode.MaxNode.GetBoolProperty("babylonjs_optimizevertices"); var invertedWorldMatrix = GetInvertWorldTM(meshNode, 0); var offsetTM = GetOffsetTM(meshNode, 0); // Compute normals var subMeshes = new List <BabylonSubMesh>(); List <int> faceIndexes = null; ExtractGeometry(babylonMesh, vertices, indices, subMeshes, boneIds, skin, unskinnedMesh, invertedWorldMatrix, offsetTM, hasUV, hasUV2, hasColor, hasAlpha, optimizeVertices, multiMatsCount, meshNode, ref faceIndexes); if (vertices.Count >= 65536) { RaiseWarning($"Mesh {babylonMesh.name} has {vertices.Count} vertices. This may prevent your scene to work on low end devices where 32 bits indice are not supported", 2); if (!optimizeVertices) { RaiseError("You can try to optimize your object using [Try to optimize vertices] option", 2); } } RaiseMessage($"{vertices.Count} vertices, {indices.Count / 3} faces", 2); // Buffers babylonMesh.positions = vertices.SelectMany(v => new[] { v.Position.X, v.Position.Y, v.Position.Z }).ToArray(); babylonMesh.normals = vertices.SelectMany(v => new[] { v.Normal.X, v.Normal.Y, v.Normal.Z }).ToArray(); // Export tangents if option is checked and mesh has tangents if (exportParameters.exportTangents) { babylonMesh.tangents = vertices.SelectMany(v => v.Tangent).ToArray(); } if (hasUV) { babylonMesh.uvs = vertices.SelectMany(v => new[] { v.UV.X, 1 - v.UV.Y }).ToArray(); } if (hasUV2) { babylonMesh.uvs2 = vertices.SelectMany(v => new[] { v.UV2.X, 1 - v.UV2.Y }).ToArray(); } if (skin != null) { babylonMesh.matricesWeights = vertices.SelectMany(v => v.Weights.ToArray()).ToArray(); babylonMesh.matricesIndices = vertices.Select(v => v.BonesIndices).ToArray(); babylonMesh.numBoneInfluencers = maxNbBones; if (maxNbBones > 4) { babylonMesh.matricesWeightsExtra = vertices.SelectMany(v => v.WeightsExtra != null ? v.WeightsExtra.ToArray() : new[] { 0.0f, 0.0f, 0.0f, 0.0f }).ToArray(); babylonMesh.matricesIndicesExtra = vertices.Select(v => v.BonesIndicesExtra).ToArray(); } } if (hasColor) { babylonMesh.colors = vertices.SelectMany(v => v.Color.ToArray()).ToArray(); babylonMesh.hasVertexAlpha = hasAlpha; } babylonMesh.subMeshes = subMeshes.ToArray(); // Buffers - Indices babylonMesh.indices = indices.ToArray(); // ------------------------ // ---- Morph targets ----- // ------------------------ // Retreive modifiers with morpher flag List <IIGameModifier> modifiers = new List <IIGameModifier>(); for (int i = 0; i < meshNode.IGameObject.NumModifiers; i++) { var modifier = meshNode.IGameObject.GetIGameModifier(i); if (modifier.ModifierType == Autodesk.Max.IGameModifier.ModType.Morpher) { modifiers.Add(modifier); } } // Cast modifiers to morphers List <IIGameMorpher> morphers = modifiers.ConvertAll(new Converter <IIGameModifier, IIGameMorpher>(modifier => modifier.AsGameMorpher())); var hasMorphTarget = false; morphers.ForEach(morpher => { if (morpher.NumberOfMorphTargets > 0) { hasMorphTarget = true; } }); if (hasMorphTarget) { RaiseMessage("Export morph targets", 2); var rawScene = Loader.Core.RootNode; // Morph Target Manager var babylonMorphTargetManager = new BabylonMorphTargetManager(); babylonScene.MorphTargetManagersList.Add(babylonMorphTargetManager); babylonMesh.morphTargetManagerId = babylonMorphTargetManager.id; // Morph Targets var babylonMorphTargets = new List <BabylonMorphTarget>(); // All morphers are considered identical // Their targets are concatenated morphers.ForEach(morpher => { for (int i = 0; i < morpher.NumberOfMorphTargets; i++) { // Morph target var maxMorphTarget = morpher.GetMorphTarget(i); // Ensure target still exists (green color legend) if (maxMorphTarget != null) { var babylonMorphTarget = new BabylonMorphTarget { name = maxMorphTarget.Name }; babylonMorphTargets.Add(babylonMorphTarget); // TODO - Influence babylonMorphTarget.influence = 0f; // Target geometry var targetVertices = ExtractVertices(babylonMesh, maxMorphTarget, optimizeVertices, faceIndexes); babylonMorphTarget.positions = targetVertices.SelectMany(v => new[] { v.Position.X, v.Position.Y, v.Position.Z }).ToArray(); if (rawScene.GetBoolProperty("babylonjs_export_Morph_Normals", 1)) { babylonMorphTarget.normals = targetVertices.SelectMany(v => new[] { v.Normal.X, v.Normal.Y, v.Normal.Z }).ToArray(); } // Tangent if (exportParameters.exportTangents && rawScene.GetBoolProperty("babylonjs_export_Morph_Tangents")) { babylonMorphTarget.tangents = targetVertices.SelectMany(v => v.Tangent).ToArray(); } // Animations var animations = new List <BabylonAnimation>(); var morphWeight = morpher.GetMorphWeight(i); ExportFloatGameController(morphWeight, "influence", animations); if (animations.Count > 0) { babylonMorphTarget.animations = animations.ToArray(); } } } }); babylonMorphTargetManager.targets = babylonMorphTargets.ToArray(); } } // World Modifiers ExportWorldModifiers(meshNode, babylonScene, babylonMesh); // Animations // Done last to avoid '0 vertex found' error (unkown cause) exportAnimation(babylonMesh, meshNode); babylonScene.MeshesList.Add(babylonMesh); return(babylonMesh); }
private void _exportMorphTargets(BabylonMesh babylonMesh, BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFBuffer buffer, GLTFMeshPrimitive meshPrimitive, List <float> weights) { var gltfMorphTargets = new List <GLTFMorphTarget>(); foreach (var babylonMorphTarget in babylonMorphTargetManager.targets) { var gltfMorphTarget = new GLTFMorphTarget(); // Positions if (babylonMorphTarget.positions != null) { var accessorTargetPositions = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer), "accessorTargetPositions", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.VEC3 ); gltfMorphTarget.Add(GLTFMorphTarget.Attribute.POSITION.ToString(), accessorTargetPositions.index); // Populate accessor accessorTargetPositions.min = new float[] { float.MaxValue, float.MaxValue, float.MaxValue }; accessorTargetPositions.max = new float[] { float.MinValue, float.MinValue, float.MinValue }; for (int indexPosition = 0; indexPosition < babylonMorphTarget.positions.Length; indexPosition += 3) { var positionTarget = Tools.SubArray(babylonMorphTarget.positions, indexPosition, 3); // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive var positionMesh = Tools.SubArray(babylonMesh.positions, indexPosition, 3); for (int indexCoordinate = 0; indexCoordinate < positionTarget.Length; indexCoordinate++) { positionTarget[indexCoordinate] = positionTarget[indexCoordinate] - positionMesh[indexCoordinate]; } // Store values as bytes foreach (var coordinate in positionTarget) { accessorTargetPositions.bytesList.AddRange(BitConverter.GetBytes(coordinate)); } // Update min and max values GLTFBufferService.UpdateMinMaxAccessor(accessorTargetPositions, positionTarget); } accessorTargetPositions.count = babylonMorphTarget.positions.Length / 3; } // Normals if (babylonMorphTarget.normals != null) { var accessorTargetNormals = GLTFBufferService.Instance.CreateAccessor( gltf, GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer), "accessorTargetNormals", GLTFAccessor.ComponentType.FLOAT, GLTFAccessor.TypeEnum.VEC3 ); gltfMorphTarget.Add(GLTFMorphTarget.Attribute.NORMAL.ToString(), accessorTargetNormals.index); // Populate accessor for (int indexNormal = 0; indexNormal < babylonMorphTarget.positions.Length; indexNormal += 3) { var normalTarget = Tools.SubArray(babylonMorphTarget.normals, indexNormal, 3); // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive var normalMesh = Tools.SubArray(babylonMesh.normals, indexNormal, 3); for (int indexCoordinate = 0; indexCoordinate < normalTarget.Length; indexCoordinate++) { normalTarget[indexCoordinate] = normalTarget[indexCoordinate] - normalMesh[indexCoordinate]; } // Store values as bytes foreach (var coordinate in normalTarget) { accessorTargetNormals.bytesList.AddRange(BitConverter.GetBytes(coordinate)); } } accessorTargetNormals.count = babylonMorphTarget.normals.Length / 3; } if (gltfMorphTarget.Count > 0) { gltfMorphTargets.Add(gltfMorphTarget); weights.Add(babylonMorphTarget.influence); } } if (gltfMorphTargets.Count > 0) { meshPrimitive.targets = gltfMorphTargets.ToArray(); } }