public static Assimp.Scene CreateAssimpScene(this IGeometryModel model, Assimp.AssimpContext context, string formatId) { var scale = ModelViewerPlugin.Settings.GeometryScale; //either Assimp or collada has issues when there is a name conflict const string bonePrefix = "~"; const string geomPrefix = "-"; const string scenPrefix = "$"; var scene = new Assimp.Scene(); scene.RootNode = new Assimp.Node($"{scenPrefix}{model.Name}"); //Assimp is Y-up in inches by default - this forces it to export as Z-up in meters scene.RootNode.Transform = (CoordinateSystem.HaloCEX * ModelViewerPlugin.Settings.AssimpScale).ToAssimp4x4(); #region Nodes var allNodes = new List <Assimp.Node>(); foreach (var node in model.Nodes) { var result = new Assimp.Node($"{bonePrefix}{node.Name}"); var q = new System.Numerics.Quaternion(node.Rotation.X, node.Rotation.Y, node.Rotation.Z, node.Rotation.W); var mat = System.Numerics.Matrix4x4.CreateFromQuaternion(q); mat.Translation = new System.Numerics.Vector3(node.Position.X * scale, node.Position.Y * scale, node.Position.Z * scale); result.Transform = mat.ToAssimp4x4(); allNodes.Add(result); } for (int i = 0; i < model.Nodes.Count; i++) { var node = model.Nodes[i]; if (node.ParentIndex >= 0) { allNodes[node.ParentIndex].Children.Add(allNodes[i]); } else { scene.RootNode.Children.Add(allNodes[i]); } } #endregion var meshLookup = new List <int>(); #region Meshes for (int i = 0; i < model.Meshes.Count; i++) { var geom = model.Meshes[i]; if (geom.Submeshes.Count == 0) { meshLookup.Add(-1); continue; } meshLookup.Add(scene.MeshCount); foreach (var sub in geom.Submeshes) { var m = new Assimp.Mesh($"mesh{i:D3}"); var indices = geom.Indicies.Skip(sub.IndexStart).Take(sub.IndexLength); var minIndex = indices.Min(); var maxIndex = indices.Max(); var vertCount = maxIndex - minIndex + 1; if (geom.IndexFormat == IndexFormat.TriangleStrip) { indices = indices.Unstrip(); } indices = indices.Select(x => x - minIndex); var vertices = geom.Vertices.Skip(minIndex).Take(vertCount); if (geom.BoundsIndex >= 0) { vertices = vertices.Select(v => (IVertex) new CompressedVertex(v, model.Bounds[geom.BoundsIndex.Value])); } int vIndex = -1; var boneLookup = new Dictionary <int, Assimp.Bone>(); foreach (var v in vertices) { vIndex++; if (v.Position.Count > 0) { m.Vertices.Add(v.Position[0].ToAssimp3D(scale)); //some Halo shaders use position W as the colour alpha - add it to a colour channel to preserve it //also assimp appears to have issues exporting obj when a colour channel exists so only do this for collada if (formatId == "collada" && v.Color.Count == 0 && !float.IsNaN(v.Position[0].W)) { m.VertexColorChannels[0].Add(new Assimp.Color4D { R = v.Position[0].W }); } } if (v.Normal.Count > 0) { m.Normals.Add(v.Normal[0].ToAssimp3D()); } if (v.TexCoords.Count > 0) { m.TextureCoordinateChannels[0].Add(v.TexCoords[0].ToAssimpUV()); } if (geom.VertexWeights == VertexWeights.None && !geom.NodeIndex.HasValue) { continue; } #region Vertex Weights var weights = new List <Tuple <int, float> >(4); if (geom.NodeIndex.HasValue) { weights.Add(Tuple.Create <int, float>(geom.NodeIndex.Value, 1)); } else if (geom.VertexWeights == VertexWeights.Skinned) { var ind = v.BlendIndices[0]; var wt = v.BlendWeight[0]; if (wt.X > 0) { weights.Add(Tuple.Create((int)ind.X, wt.X)); } if (wt.Y > 0) { weights.Add(Tuple.Create((int)ind.Y, wt.Y)); } if (wt.Z > 0) { weights.Add(Tuple.Create((int)ind.Z, wt.Z)); } if (wt.W > 0) { weights.Add(Tuple.Create((int)ind.W, wt.W)); } } foreach (var val in weights) { Assimp.Bone b; if (boneLookup.ContainsKey(val.Item1)) { b = boneLookup[val.Item1]; } else { var t = model.Nodes[val.Item1].OffsetTransform; t.M41 *= scale; t.M42 *= scale; t.M43 *= scale; b = new Assimp.Bone { Name = bonePrefix + model.Nodes[val.Item1].Name, OffsetMatrix = t.ToAssimp4x4() }; m.Bones.Add(b); boneLookup.Add(val.Item1, b); } b.VertexWeights.Add(new Assimp.VertexWeight(vIndex, val.Item2)); } #endregion } m.SetIndices(indices.ToArray(), 3); m.MaterialIndex = sub.MaterialIndex; scene.Meshes.Add(m); } } #endregion #region Regions foreach (var reg in model.Regions) { var regNode = new Assimp.Node($"{geomPrefix}{reg.Name}"); foreach (var perm in reg.Permutations) { var meshStart = meshLookup[perm.MeshIndex]; if (meshStart < 0) { continue; } var permNode = new Assimp.Node($"{geomPrefix}{perm.Name}"); if (perm.TransformScale != 1 || !perm.Transform.IsIdentity) { permNode.Transform = Assimp.Matrix4x4.FromScaling(new Assimp.Vector3D(perm.TransformScale)) * perm.Transform.ToAssimp4x4(scale); } var meshCount = Enumerable.Range(perm.MeshIndex, perm.MeshCount).Sum(i => model.Meshes[i].Submeshes.Count); permNode.MeshIndices.AddRange(Enumerable.Range(meshStart, meshCount)); regNode.Children.Add(permNode); } if (regNode.ChildCount > 0) { scene.RootNode.Children.Add(regNode); } } #endregion #region Materials foreach (var mat in model.Materials) { var m = new Assimp.Material { Name = mat?.Name ?? "unused" }; //prevent max from making every material super shiny m.ColorEmissive = m.ColorReflective = m.ColorSpecular = new Assimp.Color4D(0, 0, 0, 1); m.ColorDiffuse = m.ColorTransparent = new Assimp.Color4D(1); //max only seems to care about diffuse var dif = mat?.Submaterials.FirstOrDefault(s => s.Usage == MaterialUsage.Diffuse); if (dif != null) { var suffix = dif.Bitmap.SubmapCount > 1 ? "[0]" : string.Empty; var filePath = $"{dif.Bitmap.Name}{suffix}.{ModelViewerPlugin.Settings.MaterialExtension}"; //collada spec says it requires URI formatting, and Assimp doesn't do it for us //for some reason "new Uri(filePath, UriKind.Relative)" doesnt change the slashes, have to use absolute uri if (formatId == FormatId.Collada) { filePath = new Uri("X:\\", UriKind.Absolute).MakeRelativeUri(new Uri(System.IO.Path.Combine("X:\\", filePath))).ToString(); } m.TextureDiffuse = new Assimp.TextureSlot { BlendFactor = 1, FilePath = filePath, TextureType = Assimp.TextureType.Diffuse }; } scene.Materials.Add(m); } #endregion return(scene); }
public static Assimp.Scene ToAssimpScene(RWScene scene) { Assimp.Scene aiScene = new Assimp.Scene(); int drawCallIdx = 0; int materialIdx = 0; int totalSplitIdx = 0; List<int> meshStartIndices = new List<int>(); foreach (RWDrawCall drawCall in scene.DrawCalls) { meshStartIndices.Add(totalSplitIdx); var mesh = scene.Meshes[drawCall.MeshIndex]; var node = scene.Nodes[drawCall.NodeIndex]; int splitIdx = 0; foreach (RWMeshMaterialSplit split in mesh.MaterialSplitData.MaterialSplits) { Assimp.Mesh aiMesh = new Assimp.Mesh(Assimp.PrimitiveType.Triangle); aiMesh.Name = string.Format("DrawCall{0}_Split{1}", drawCallIdx.ToString("00"), splitIdx.ToString("00")); aiMesh.MaterialIndex = split.MaterialIndex + materialIdx; // get split indices int[] indices = split.Indices; if (mesh.MaterialSplitData.PrimitiveType == RWPrimitiveType.TriangleStrip) indices = MeshUtilities.ToTriangleList(indices, true); // pos & nrm for (int i = 0; i < indices.Length; i++) { if (mesh.HasVertices) { var vert = Vector3.Transform(mesh.Vertices[indices[i]], node.WorldTransform); aiMesh.Vertices.Add(vert.ToAssimpVector3D()); } if (mesh.HasNormals) { var nrm = Vector3.TransformNormal(mesh.Normals[indices[i]], node.WorldTransform); aiMesh.Normals.Add(nrm.ToAssimpVector3D()); } } // tex coords if (mesh.HasTexCoords) { for (int i = 0; i < mesh.TextureCoordinateChannelCount; i++) { List<Assimp.Vector3D> texCoordChannel = new List<Assimp.Vector3D>(); for (int j = 0; j < indices.Length; j++) { texCoordChannel.Add(mesh.TextureCoordinateChannels[i][indices[j]].ToAssimpVector3D(0)); } aiMesh.TextureCoordinateChannels[i] = texCoordChannel; } } // colors if (mesh.HasColors) { List<Assimp.Color4D> vertColorChannel = new List<Assimp.Color4D>(); for (int i = 0; i < indices.Length; i++) { var color = mesh.Colors[indices[i]]; vertColorChannel.Add(new Assimp.Color4D(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f)); } aiMesh.VertexColorChannels[0] = vertColorChannel; } // generate temporary face indices int[] tempIndices = new int[aiMesh.VertexCount]; for (int i = 0; i < aiMesh.VertexCount; i++) tempIndices[i] = i; aiMesh.SetIndices(tempIndices, 3); // add the mesh to the list aiScene.Meshes.Add(aiMesh); splitIdx++; } totalSplitIdx += splitIdx; foreach (RWMaterial mat in mesh.Materials) { Assimp.Material aiMaterial = new Assimp.Material(); aiMaterial.AddProperty(new Assimp.MaterialProperty(Assimp.Unmanaged.AiMatKeys.NAME, "Material" + (materialIdx++).ToString("00"))); if (mat.IsTextured) { aiMaterial.AddProperty(new Assimp.MaterialProperty(Assimp.Unmanaged.AiMatKeys.TEXTURE_BASE, mat.TextureReference.ReferencedTextureName + ".png", Assimp.TextureType.Diffuse, 0)); } aiScene.Materials.Add(aiMaterial); } drawCallIdx++; } // store node lookup Dictionary<RWSceneNode, Assimp.Node> nodeLookup = new Dictionary<RWSceneNode, Assimp.Node>(); // first create the root node var rootNode = new Assimp.Node("SceneRoot"); rootNode.Transform = scene.Nodes[0].Transform.ToAssimpMatrix4x4(); nodeLookup.Add(scene.Nodes[0], rootNode); for (int i = 1; i < scene.Nodes.Count - 1; i++) { var node = scene.Nodes[i]; string name = node.BoneMetadata.BoneNameID.ToString(); var aiNode = new Assimp.Node(name); aiNode.Transform = node.Transform.ToAssimpMatrix4x4(); // get the associated meshes for this node var drawCalls = scene.DrawCalls.FindAll(dc => dc.NodeIndex == i); foreach (var drawCall in drawCalls) { for (int j = 0; j < scene.Meshes[drawCall.MeshIndex].MaterialCount; j++) { aiNode.MeshIndices.Add(meshStartIndices[scene.DrawCalls.IndexOf(drawCall)] + j); } } nodeLookup[node.Parent].Children.Add(aiNode); nodeLookup.Add(node, aiNode); } aiScene.RootNode = rootNode; return aiScene; }