private void ProcessModel(GltfFile modelFile, GltfFile[] animFiles) { var modelPath = modelFile.Path; var modelData = modelFile.Data; var modelName = Path.GetFileNameWithoutExtension(modelPath); var outputModelFolder = Path.Combine(OutputFolder, modelName); Directory.CreateDirectory(outputModelFolder); Log.WriteLine($"Copying images..."); if (modelData.Images != null) { using (Log.Indented()) { CopyImages(modelData, modelPath, outputModelFolder); } } Log.WriteLine($"Fetching model buffers..."); using (Log.Indented()) { FetchBuffers(modelData, modelPath); } var modelNodeIndexByName = modelData.Nodes.Select((n, i) => (n, i)) .ToDictionary(pair => pair.Item1.Name, pair => pair.Item2); foreach (GltfFile animFile in animFiles) { Log.WriteLine($"Processing animations in {animFile.Path}..."); using (Log.Indented()) { ProcessAnimations(modelFile, animFile, modelNodeIndexByName, outputModelFolder); } // The anim-file-data was modified, reload next time. animFile.Invalidate(); } var outputBufferPath = Path.Combine(outputModelFolder, OutputBufferFilename); using (var newBufferStream = File.Create(outputBufferPath)) { Log.WriteLine($"Packing all accessors in a single buffer..."); PackAccessors(modelFile, newBufferStream, OutputBufferFilename); Log.WriteLine($"Writing model glTF file..."); var outputModelPath = Path.Combine(outputModelFolder, OutputModelFilename); modelData.SaveModel(outputModelPath); } }
private void ProcessAnimations(GltfFile modelFile, GltfFile animFile, Dictionary <string, int> modelNodeIndexByName, string outputModelFolder) { foreach (Animation animation in animFile.Data.Animations) { using (Log.Indented()) { Log.WriteLine($"Processing animation clip {animation.Name} in {animFile.Path}..."); using (Log.Indented()) { ProcessAnimation(modelFile, animFile, animation, modelNodeIndexByName, outputModelFolder); } } } }
private void ProcessAnimation(GltfFile modelFile, GltfFile animFile, Animation animation, Dictionary <string, int> modelNodeIndexByName, string outputModelFolder) { var animData = animFile.Data; var modelData = modelFile.Data; // Load all animation buffers into memory // TODO: This could be done lazily! FetchBuffers(animData, animFile.Path); foreach (var channel in animation.Channels) { var nodeIndex = channel.Target?.Node; if (nodeIndex.HasValue) { var nodeName = animData.Nodes[nodeIndex.Value].Name; if (!_allModelNodeNames.Contains(nodeName)) { Log.WriteLine($"WARNING: Clip node '{nodeName}' not found in any model. Similar node names are:"); using (Log.Indented()) { Fastenshtein.Levenshtein lev = new Fastenshtein.Levenshtein(nodeName); Log.WriteLine(string.Join(", ", _allModelNodeNames.OrderBy(lev.DistanceFrom).Take(3))); } } } } int tc = 0; int rc = 0; int sc = 0; int wc = 0; // Keep only those animation channels that animate the current model, // and are different from the initial scene AnimationChannel[] clipChannels = animation .Channels .Where(channel => { var nodeIndex = channel?.Target?.Node; if (!nodeIndex.HasValue) { return(false); } var node = animData.Nodes[nodeIndex.Value]; if (!modelNodeIndexByName.ContainsKey(node.Name)) { return(false); } var sampler = animation.Samplers[channel.Sampler]; var outputAccessor = animData.Accessors[sampler.Output]; // We assume the exporter already reduced constant curves to a single key. if (outputAccessor.Count != 1) { return(true); } // We don't handle sparse data yet, keep it if (!outputAccessor.BufferView.HasValue) { return(true); } var mesh = node.Mesh.HasValue ? animData.Meshes[node.Mesh.Value] : null; var outputValues = GetComponentAtIndex(animData, outputAccessor, 0); switch (channel.Target.Path) { case AnimationChannelTarget.PathEnum.translation: return(AreDifferent(ref tc, outputValues, node.Translation ?? DefaultTranslation, 1e-3f)); case AnimationChannelTarget.PathEnum.rotation: return(AreDifferent(ref rc, outputValues, node.Rotation ?? DefaultRotation, 1e-4f)); case AnimationChannelTarget.PathEnum.scale: return(AreDifferent(ref sc, outputValues, node.Scale ?? DefaultScale, 1e-4f)); case AnimationChannelTarget.PathEnum.weights: return(AreDifferent(ref wc, outputValues, node.Weights ?? mesh?.Weights, 1e-3f)); default: return(true); } }) .ToArray(); if (clipChannels.Length == 0) { Log.WriteLine($"NOTE: Animation '{animation.Name}' is not used by model '{modelFile.Path}'"); return; } if (tc + rc + sc + wc > 0) { Log.WriteLine($"Skipped {tc} translation, {rc} rotation, {sc} scaling and {wc} weight redundant animation channels"); } // Keep only those samplers used by any channel AnimationSampler[] clipSamplers = clipChannels .Select(channel => animation.Samplers[channel.Sampler]) .Distinct() .ToArray(); AnimationChannelTarget[] clipTargets = clipChannels .Select(channel => channel.Target) .ToArray(); Accessor[] clipSamplerInputs = clipSamplers.Select(sampler => animData.Accessors[sampler.Input]).ToArray(); Accessor[] clipSamplerOutputs = clipSamplers.Select(sampler => animData.Accessors[sampler.Output]).ToArray(); Accessor[] clipAccessors = clipSamplerInputs.Concat(clipSamplerOutputs).Distinct().ToArray(); Remapper <Accessor> newAccessors = modelData.Accessors.Concat(clipAccessors).RemapFrom(animData.Accessors); Remapper <AnimationSampler> newSamplers = clipSamplers.RemapFrom(animation.Samplers); Remapper <AnimationChannel> newChannels = clipChannels.RemapFrom(animation.Channels); // Remap sampler indices foreach (var sampler in clipSamplers) { sampler.Input = newAccessors.Remap(sampler.Input); sampler.Output = newAccessors.Remap(sampler.Output); } // Remap channel indices foreach (var channel in clipChannels) { channel.Sampler = newSamplers.Remap(channel.Sampler); } // Remap animation channel targets foreach (var target in clipTargets) { // ReSharper disable once PossibleInvalidOperationException var clipNode = animData.Nodes[(int)target.Node]; var nodeIndex = modelNodeIndexByName[clipNode.Name]; target.Node = nodeIndex; } animation.Channels = newChannels.OutputItems; animation.Samplers = newSamplers.OutputItems; var clipBufferViews = clipAccessors .Select(a => a.BufferView) .Where(bv => bv.HasValue) .Select(bv => bv.Value) .Distinct() .Select(i => animData.BufferViews[i]) .ToArray(); var newBufferViews = modelData.BufferViews.Concat(clipBufferViews) .RemapFrom(animData.BufferViews); // Remap clip accessor indices foreach (var accessor in clipAccessors) { if (accessor.BufferView.HasValue) { accessor.BufferView = newBufferViews.Remap(accessor.BufferView.Value); } } var clipBuffers = clipBufferViews .Select(bv => bv.Buffer) .Distinct() .Select(i => animData.Buffers[i]) .ToArray(); var newBuffers = modelData.Buffers.Concat(clipBuffers).RemapFrom(animData.Buffers); // Remap buffer view indices foreach (var bufferView in clipBufferViews) { bufferView.Buffer = newBuffers.Remap(bufferView.Buffer); } // TODO: Animation indices don't need to be remapped? modelData.Animations = (modelData.Animations ?? Array.Empty <Animation>()).Append(animation).ToArray(); modelData.Accessors = newAccessors.OutputItems; modelData.BufferViews = newBufferViews.OutputItems; modelData.Buffers = newBuffers.OutputItems; }
/// <summary> /// Creates a single buffer and a minimum number of buffer-views to pack all the accessors. /// </summary> private void PackAccessors(GltfFile modelFile, Stream newBufferStream, string outputBufferUri) { var fileData = modelFile.Data; var buffers = fileData.Buffers; var bufferViews = fileData.BufferViews; // TODO: Check sparse accessors // Group accessor by optional target, component-stride and component byte-length // For each group, we need to create a single buffer-view. var accessorGroups = fileData .Accessors .Where(a => a.BufferView.HasValue) .GroupBy(a => (target: bufferViews[a.BufferView.Value].Target, stride: a.GetByteStride(bufferViews) /*, componentByteLength: a.GetComponentByteLength()*/)) .ToDictionary(g => g.Key, g => g.ToArray()); // NOTE: The following select sequence has a side-effect var newBufferViews = accessorGroups .Select((pair, newBufferViewIndex) => { //var ((target, stride, componentByteLength), accessors) = pair; var((target, stride), accessors) = pair; var newBufferViewOffset = (int)newBufferStream.Position; //Debug.Assert(newBufferStream.GetComponentPadding(componentByteLength) == 0); foreach (var accessor in accessors) { var componentByteLength = accessor.GetComponentByteLength(); var accessorPadding = newBufferStream.GetComponentPadding(componentByteLength); newBufferStream.WriteByte(0, accessorPadding); var newAccessorByteOffset = (int)(newBufferStream.Position - newBufferViewOffset); Debug.Assert(accessor.BufferView.HasValue); var bufferView = bufferViews[accessor.BufferView.Value]; var buffer = buffers[bufferView.Buffer]; var data = _bufferFileData[buffer]; var bufferOffset = bufferView.ByteOffset + accessor.ByteOffset; var rowLength = accessor.GetComponentDimension() * componentByteLength; for (int i = 0; i < accessor.Count; ++i) { Debug.Assert(newBufferStream.GetComponentPadding(componentByteLength) == 0); newBufferStream.Write(data, bufferOffset + i * stride, rowLength); Debug.Assert(newBufferStream.GetComponentPadding(componentByteLength) == 0); } // Patch the accessor. accessor.ByteOffset = newAccessorByteOffset; accessor.BufferView = newBufferViewIndex; } var newBufferView = new BufferView { Target = target, ByteOffset = newBufferViewOffset, ByteStride = target == BufferView.TargetEnum.ARRAY_BUFFER ? stride : (int?)null, ByteLength = (int)(newBufferStream.Position - newBufferViewOffset) }; return(newBufferView); }) .ToArray(); var newBuffer = new Buffer { ByteLength = (int)newBufferStream.Length, Uri = outputBufferUri }; fileData.BufferViews = newBufferViews; fileData.Buffers = new[] { newBuffer }; }