using System; using System.IO; using System.Collections.Generic; using System.Text; using NW4RTools.Models; using Collada141; namespace NW4RTools { public class ColladaWriter { public static void WriteModel(Stream outputStream, ResFile file, string modelName) { new ColladaWriter(file).SaveModel(outputStream, modelName); } ResFile CurrentFile; Models.Model CurrentModel; COLLADA Collada; library_geometries LibGeometries; library_visual_scenes LibVisualScenes; library_images LibImages; library_materials LibMaterials; library_effects LibEffects; private ColladaWriter(ResFile file) { CurrentFile = file; } public void SaveModel(Stream outputStream, string modelName) { CurrentModel = CurrentFile.GetGroup("3DModels(NW4R)")[modelName]; Collada = new COLLADA(); Collada.asset = new asset(); Collada.asset.contributor = new assetContributor[1]; Collada.asset.contributor[0] = new assetContributor(); Collada.asset.contributor[0].authoring_tool = "NW4RTools Collada exporter by Treeki"; Collada.asset.contributor[0].source_data = "NW4R model: " + modelName; Collada.asset.created = DateTime.Now; Collada.asset.modified = DateTime.Now; Collada.asset.unit = new assetUnit(); Collada.asset.unit.meter = 1.0; Collada.asset.up_axis = UpAxisType.Y_UP; var ColladaItems = new List(); // First up, before anything else: images and materials // Compile a list of every texture we use in the model var usedTextures = new List(); foreach (var kv in CurrentModel.Materials) { foreach (var texInfo in kv.Value.TextureInfos) { if (!usedTextures.Contains(texInfo.TextureName)) usedTextures.Add(texInfo.TextureName); } } // Create a Collada image for these LibImages = new library_images(); ColladaItems.Add(LibImages); var ImageList = new List(); var TexGroup = CurrentFile.GetGroup("Textures(NW4R)"); foreach (var texName in usedTextures) { var img = new image(); var tex = TexGroup[texName]; img.name = "Texture-" + texName; img.id = img.name; img.Item = "./images/" + texName + ".png"; ImageList.Add(img); } LibImages.image = ImageList.ToArray(); // Make a quick reference material for each one LibMaterials = new library_materials(); ColladaItems.Add(LibMaterials); var MaterialList = new List(); foreach (var kv in CurrentModel.Materials) { var mat = new material(); mat.name = "Material-" + kv.Key; mat.id = mat.name; mat.instance_effect = new instance_effect(); mat.instance_effect.url = "#Material-Effect-" + kv.Key; MaterialList.Add(mat); } LibMaterials.material = MaterialList.ToArray(); // Now create an effect for each material (this is where all the real work is done!) LibEffects = new library_effects(); ColladaItems.Add(LibEffects); var EffectList = new List(); foreach (var kv in CurrentModel.Materials) { EffectList.Add(CreateEffectFromMaterial(kv.Key, kv.Value)); } LibEffects.effect = EffectList.ToArray(); // Now shapes LibGeometries = new library_geometries(); ColladaItems.Add(LibGeometries); var GeometryList = new List(); foreach (var kv in CurrentModel.Shapes) { GeometryList.Add(CreateGeometryFromShape(kv.Key, kv.Value)); } LibGeometries.geometry = GeometryList.ToArray(); // SHAPES ARE DONE. // Next up: Visual Scenes (I will just create one atm) LibVisualScenes = new library_visual_scenes(); ColladaItems.Add(LibVisualScenes); LibVisualScenes.visual_scene = new visual_scene[1]; var mainScene = LibVisualScenes.visual_scene[0] = new visual_scene(); // TODO: Change this so it doesn't have the possibility of name collisions with shapes mainScene.id = "RootNode"; mainScene.name = "RootNode"; var mainSceneNodeList = new List(); // OK, so here's what's up: first off, we must create a definition for every node var NodeDefs = new Dictionary(); foreach (var kv in CurrentModel.Nodes) { string nodeName = kv.Key; Node origNode = kv.Value; var cNode = new node(); cNode.id = nodeName; cNode.name = nodeName; //cNode.type = NodeType.JOINT; cNode.node1 = new node[0]; double cosX = Math.Cos(origNode.Rotation.x / 180 * Math.PI); double cosY = Math.Cos(origNode.Rotation.y / 180 * Math.PI); double cosZ = Math.Cos(origNode.Rotation.z / 180 * Math.PI); double sinX = Math.Sin(origNode.Rotation.x / 180 * Math.PI); double sinY = Math.Sin(origNode.Rotation.y / 180 * Math.PI); double sinZ = Math.Sin(origNode.Rotation.z / 180 * Math.PI); var nodeMatrix = new matrix(); nodeMatrix.Values = new double[] { origNode.Scale.x * cosY * cosZ, origNode.Scale.y * (sinX * cosZ * sinY - cosX * sinZ), origNode.Scale.z * (sinX * sinZ + cosX * cosZ * sinY), origNode.Translation.x, origNode.Scale.x * sinZ * cosY, origNode.Scale.y * (sinX * sinZ * sinY + cosZ * cosX), origNode.Scale.z * (cosX * sinZ * sinY - sinX * cosZ), origNode.Translation.y, -origNode.Scale.x * sinY, origNode.Scale.y * sinX * cosY, origNode.Scale.z * cosX * cosY, origNode.Translation.z, 0, 0, 0, 1 }; cNode.Items = new object[] { nodeMatrix }; cNode.ItemsElementName = new ItemsChoiceType2[] { ItemsChoiceType2.matrix }; NodeDefs[origNode] = cNode; } // Now add them to the hierarchy foreach (var kv in NodeDefs) { Node origNode = kv.Key; node cNode = kv.Value; if (origNode.Parent == null) { mainSceneNodeList.Add(cNode); } else { var parentNode = NodeDefs[origNode.Parent]; // this is stupid, thanks C# node[] nodeArrayCopy = parentNode.node1; Array.Resize(ref nodeArrayCopy, nodeArrayCopy.Length + 1); nodeArrayCopy[nodeArrayCopy.Length - 1] = cNode; parentNode.node1 = nodeArrayCopy; } } // Apply shapes to nodes /*foreach (var kv in CurrentModel.Shapes) { Shape shape = kv.Value; Node origNode = CurrentModel.Nodes[CurrentModel.MatrixIDtoNodeID[shape.MatrixID]]; node cNode = NodeDefs[origNode]; var newGeoEntry = new instance_geometry(); newGeoEntry.name = kv.Key; newGeoEntry.url = String.Format("#{0}-lib", kv.Key); instance_geometry[] geoArrayCopy = cNode.instance_geometry; if (geoArrayCopy == null) geoArrayCopy = new instance_geometry[1]; else Array.Resize(ref geoArrayCopy, geoArrayCopy.Length + 1); geoArrayCopy[geoArrayCopy.Length - 1] = newGeoEntry; cNode.instance_geometry = geoArrayCopy; // TODO: Add material handling, I'll probably have to parse DrawOpa/DrawXlu for this... }*/ // WARNING: THIS NEEDS REFACTORING int drawID = 0; foreach (var insn in CurrentModel.Bytecode["DrawOpa"].Instructions) { if (insn is ByteCode.DrawShapeInstruction) { var dsInsn = insn as ByteCode.DrawShapeInstruction; Shape shape = CurrentModel.Shapes[dsInsn.ShapeID]; string shapeName = CurrentModel.Shapes.GetKeyForIndex(dsInsn.ShapeID); Node origNode = CurrentModel.Nodes[dsInsn.NodeID]; node cNode = NodeDefs[origNode]; Material mat = CurrentModel.Materials[dsInsn.MaterialID]; string matName = CurrentModel.Materials.GetKeyForIndex(dsInsn.MaterialID); var newGeoEntry = new instance_geometry(); newGeoEntry.name = String.Format("DrawOpa{0}-{1}", drawID, shapeName); newGeoEntry.url = String.Format("#{0}-lib", shapeName); // now add the material var bindMaterial = newGeoEntry.bind_material = new bind_material(); bindMaterial.technique_common = new instance_material[1]; var matTechnique = bindMaterial.technique_common[0] = new instance_material(); // constant marker so that I don't have to set a unique material name in each primitive // it doesn't matter, since each geometry instance only uses one material anyway matTechnique.symbol = "NW4R_MATERIAL"; matTechnique.target = "#Material-" + matName; matTechnique.bind_vertex_input = new instance_materialBind_vertex_input[1]; matTechnique.bind_vertex_input[0] = new instance_materialBind_vertex_input(); matTechnique.bind_vertex_input[0].semantic = "CHANNEL1"; matTechnique.bind_vertex_input[0].input_semantic = "TEXCOORD"; matTechnique.bind_vertex_input[0].input_set = 0; // ok, now add the instance_geometry into the node instance_geometry[] geoArrayCopy = cNode.instance_geometry; if (geoArrayCopy == null) geoArrayCopy = new instance_geometry[1]; else Array.Resize(ref geoArrayCopy, geoArrayCopy.Length + 1); geoArrayCopy[geoArrayCopy.Length - 1] = newGeoEntry; cNode.instance_geometry = geoArrayCopy; drawID++; } } drawID = 0; foreach (var insn in CurrentModel.Bytecode["DrawXlu"].Instructions) { if (insn is ByteCode.DrawShapeInstruction) { var dsInsn = insn as ByteCode.DrawShapeInstruction; Shape shape = CurrentModel.Shapes[dsInsn.ShapeID]; string shapeName = CurrentModel.Shapes.GetKeyForIndex(dsInsn.ShapeID); Node origNode = CurrentModel.Nodes[dsInsn.NodeID]; node cNode = NodeDefs[origNode]; Material mat = CurrentModel.Materials[dsInsn.MaterialID]; string matName = CurrentModel.Materials.GetKeyForIndex(dsInsn.MaterialID); var newGeoEntry = new instance_geometry(); newGeoEntry.name = String.Format("DrawXlu{0}-{1}", drawID, shapeName); newGeoEntry.url = String.Format("#{0}-lib", shapeName); // now add the material var bindMaterial = newGeoEntry.bind_material = new bind_material(); bindMaterial.technique_common = new instance_material[1]; var matTechnique = bindMaterial.technique_common[0] = new instance_material(); // constant marker so that I don't have to set a unique material name in each primitive // it doesn't matter, since each geometry instance only uses one material anyway matTechnique.symbol = "NW4R_MATERIAL"; matTechnique.target = "#Material-" + matName; matTechnique.bind_vertex_input = new instance_materialBind_vertex_input[1]; matTechnique.bind_vertex_input[0] = new instance_materialBind_vertex_input(); matTechnique.bind_vertex_input[0].semantic = "CHANNEL1"; matTechnique.bind_vertex_input[0].input_semantic = "TEXCOORD"; matTechnique.bind_vertex_input[0].input_set = 0; // ok, now add the instance_geometry into the node instance_geometry[] geoArrayCopy = cNode.instance_geometry; if (geoArrayCopy == null) geoArrayCopy = new instance_geometry[1]; else Array.Resize(ref geoArrayCopy, geoArrayCopy.Length + 1); geoArrayCopy[geoArrayCopy.Length - 1] = newGeoEntry; cNode.instance_geometry = geoArrayCopy; drawID++; } } /*foreach (var kv in CurrentModel.Shapes) { var thisNode = new node(); thisNode.id = kv.Key; thisNode.name = kv.Key; thisNode.instance_geometry = new instance_geometry[1]; thisNode.instance_geometry[0] = new instance_geometry(); thisNode.instance_geometry[0].url = String.Format("#{0}-lib", kv.Key); mainSceneNodeList.Add(thisNode); }*/ mainScene.node = mainSceneNodeList.ToArray(); // Finally, create a scene Collada.scene = new COLLADAScene(); Collada.scene.instance_visual_scene = new InstanceWithExtra(); Collada.scene.instance_visual_scene.url = "#RootNode"; Collada.Items = ColladaItems.ToArray(); Collada.Save(outputStream); } private effect CreateEffectFromMaterial(string name, Material mat) { var eff = new effect(); eff.id = "Material-Effect-" + name; eff.name = eff.id; // this is based off what colladamax outputs eff.Items = new effectFx_profile_abstractProfile_COMMON[1]; var profile = eff.Items[0] = new effectFx_profile_abstractProfile_COMMON(); // TODO: handle this correctly for multiple textures ETC and check how bb does it // HACK!! if (mat.TextureInfos.Count == 0) return eff; profile.Items = new object[2]; // create a surface newparam var surfaceParam = new common_newparam_type(); var surface = new fx_surface_common(); surface.type = fx_surface_type_enum.Item2D; surface.init_from = new fx_surface_init_from_common[1]; surface.init_from[0] = new fx_surface_init_from_common(); surface.init_from[0].Value = "Texture-" + mat.TextureInfos[0].TextureName; surfaceParam.sid = "Surface-" + mat.TextureInfos[0].TextureName; surfaceParam.ItemElementName = ItemChoiceType.surface; surfaceParam.Item = surface; profile.Items[0] = surfaceParam; // now create a sampler newparam var samplerParam = new common_newparam_type(); var sampler2d = new fx_sampler2D_common(); sampler2d.source = "Surface-" + mat.TextureInfos[0].TextureName; samplerParam.sid = "Sampler-" + mat.TextureInfos[0].TextureName; samplerParam.ItemElementName = ItemChoiceType.sampler2D; samplerParam.Item = sampler2d; profile.Items[1] = samplerParam; // now make a technique // should I really use blinn...? profile.technique = new effectFx_profile_abstractProfile_COMMONTechnique(); profile.technique.sid = "common"; var pShader = new effectFx_profile_abstractProfile_COMMONTechniquePhong(); pShader.diffuse = new common_color_or_texture_type(); var diffuseTex = new common_color_or_texture_typeTexture(); diffuseTex.texture = "Sampler-" + mat.TextureInfos[0].TextureName; diffuseTex.texcoord = "CHANNEL1"; pShader.diffuse.Item = diffuseTex; profile.technique.Item = pShader; return eff; } private geometry CreateGeometryFromShape(string name, Shape shape) { var geo = new geometry(); geo.id = name + "-lib"; geo.name = name + "Mesh"; var m = new mesh(); geo.Item = m; // Vertex settings var firstDL = new InputStream(shape.DisplayList1); firstDL.Seek(0x0C); UInt32 vtxDesc1 = firstDL.ReadUInt32(); firstDL.Seek(0x12); UInt32 vtxDesc2 = firstDL.ReadUInt32(); firstDL.Seek(0x22); UInt32 vtxAttr1 = firstDL.ReadUInt32(); firstDL.Seek(0x28); UInt32 vtxAttr2 = firstDL.ReadUInt32(); firstDL.Seek(0x2E); UInt32 vtxAttr3 = firstDL.ReadUInt32(); var vs = new VertexSettings(); vs.SetDesc(vtxDesc1, vtxDesc2); vs.SetAttrFmt(vtxAttr1, vtxAttr2, vtxAttr3); // Figure out how many elements we need in the Source array // Position data ALWAYS exists int sourceCount = 1; sourceCount += (shape.NrmData != null) ? 1 : 0; for (int i = 0; i < 8; i++) sourceCount += (shape.TexCoordData[i] != null) ? 1 : 0; m.source = new source[sourceCount]; int currentSource = 0; // TODO: Refactor this messy code! int dest; // Write position data var posData = shape.PosData; var posSource = new source(); posSource.id = name + "-lib-Position"; m.source[currentSource++] = posSource; var posArray = new float_array(); posArray.id = name + "-lib-Position-array"; posArray.count = (ulong)(posData.GetRealCount() * posData.EntryCount); posArray.Values = new double[posArray.count]; dest = 0; for (int i = 0; i < posData.EntryCount; i++) { float[] data = posData.GetEntry(i); for (int j = 0; j < data.Length; j++) { posArray.Values[dest++] = data[j]; } } posSource.Item = posArray; // Write position technique posSource.technique_common = new sourceTechnique_common(); var posAcc = posSource.technique_common.accessor = new accessor(); posAcc.source = String.Format("#{0}-lib-Position-array", name); posAcc.count = posData.EntryCount; posAcc.stride = (ulong)posData.GetRealCount(); posAcc.param = new param[posData.GetRealCount()]; string[] posParamNames = new string[] { "X", "Y", "Z" }; for (int i = 0; i < posAcc.param.Length; i++) { posAcc.param[i] = new param(); posAcc.param[i].name = posParamNames[i]; posAcc.param[i].type = "float"; } // Write normal data if (shape.NrmData != null) { var nrmData = shape.NrmData; var nrmSource = new source(); nrmSource.id = name + "-lib-Normal"; m.source[currentSource++] = nrmSource; var nrmArray = new float_array(); nrmArray.id = name + "-lib-Normal-array"; nrmArray.count = (ulong)(nrmData.GetRealCount() * nrmData.EntryCount); nrmArray.Values = new double[nrmArray.count]; dest = 0; for (int i = 0; i < nrmData.EntryCount; i++) { float[] data = nrmData.GetEntry(i); for (int j = 0; j < data.Length; j++) { nrmArray.Values[dest++] = data[j]; } } nrmSource.Item = nrmArray; // Write normal technique nrmSource.technique_common = new sourceTechnique_common(); var nrmAcc = nrmSource.technique_common.accessor = new accessor(); nrmAcc.source = String.Format("#{0}-lib-Normal-array", name); nrmAcc.count = nrmData.EntryCount; nrmAcc.stride = (ulong)nrmData.GetRealCount(); nrmAcc.param = new param[nrmData.GetRealCount()]; string[] nrmParamNames = new string[] { "X", "Y", "Z" }; for (int i = 0; i < nrmAcc.param.Length; i++) { nrmAcc.param[i] = new param(); nrmAcc.param[i].name = nrmParamNames[i]; nrmAcc.param[i].type = "float"; } } // Write TexCoord data for (int tcIndex = 0; tcIndex < 8; tcIndex++) { if (shape.TexCoordData[tcIndex] != null) { var tcData = shape.TexCoordData[tcIndex]; var tcSource = new source(); tcSource.id = String.Format("{0}-lib-TexCoord{1}", name, tcIndex); m.source[currentSource++] = tcSource; var tcArray = new float_array(); tcArray.id = String.Format("{0}-lib-TexCoord{1}-array", name, tcIndex); tcArray.count = (ulong)(tcData.GetRealCount() * tcData.EntryCount); tcArray.Values = new double[tcArray.count]; dest = 0; for (int i = 0; i < tcData.EntryCount; i++) { float[] data = tcData.GetEntry(i); for (int j = 0; j < data.Length; j++) { tcArray.Values[dest++] = data[j]; } } tcSource.Item = tcArray; // Write texcoord technique tcSource.technique_common = new sourceTechnique_common(); var tcAcc = tcSource.technique_common.accessor = new accessor(); tcAcc.source = String.Format("#{0}-lib-TexCoord{1}-array", name, tcIndex); tcAcc.count = tcData.EntryCount; tcAcc.stride = (ulong)tcData.GetRealCount(); tcAcc.param = new param[tcData.GetRealCount()]; string[] tcParamNames = new string[] { "S", "T" }; for (int i = 0; i < tcAcc.param.Length; i++) { tcAcc.param[i] = new param(); tcAcc.param[i].name = tcParamNames[i]; tcAcc.param[i].type = "float"; } } } // Ok, we've written all the raw float data, now set up vertices // TODO: Vertex colours m.vertices = new vertices(); m.vertices.id = String.Format("{0}-lib-Vertex", name); m.vertices.input = new InputLocal[1]; m.vertices.input[0] = new InputLocal(); m.vertices.input[0].semantic = "POSITION"; m.vertices.input[0].source = String.Format("#{0}-lib-Position", name); // And before we finish, write the polygon data of course var dl = new InputStream(shape.DisplayList2); List meshItems = new List(); // create the Input array -- we can reuse sourceCount! var inputArray = new InputLocalOffset[sourceCount]; currentSource = 0; var posInput = inputArray[currentSource] = new InputLocalOffset(); posInput.semantic = "VERTEX"; posInput.offset = (ulong)currentSource; posInput.source = String.Format("#{0}-lib-Vertex", name); currentSource++; if (shape.NrmData != null) { var nrmInput = inputArray[currentSource] = new InputLocalOffset(); nrmInput.semantic = "NORMAL"; nrmInput.offset = (ulong)currentSource; nrmInput.source = String.Format("#{0}-lib-Normal", name); currentSource++; } for (int i = 0; i < 8; i++) { if (shape.TexCoordData[i] != null) { var tcInput = inputArray[currentSource] = new InputLocalOffset(); tcInput.semantic = "TEXCOORD"; tcInput.offset = (ulong)currentSource; tcInput.@set = (ulong)i; tcInput.source = String.Format("#{0}-lib-TexCoord{1}", name, i); currentSource++; } } // Create a list for tristrips beforehand, because they're THE most common List triStrips = new List(); // Now go through the display list while (true) { if (dl.AtEnd) break; byte cmd = dl.ReadByte(); if (cmd == 0) break; PrimitiveType prim = (PrimitiveType)((cmd >> 3) & 7); int vtxCount = dl.ReadUInt16(); // first, parse it into a list of vertices GXIndexedVertex[] vtxs = new GXIndexedVertex[vtxCount]; string[] pVtxs = new string[vtxCount]; for (int i = 0; i < vtxCount; i++) { vtxs[i].LoadFrom(dl, vs); pVtxs[i] = vtxs[i].Position.ToString(); if (vs.NormalDesc != VertexSettings.DescType.None) pVtxs[i] += " " + vtxs[i].Normal.ToString(); for (int j = 0; j < 8; j++) { if (vs.TexCoordDesc[j] != VertexSettings.DescType.None) { pVtxs[i] += " " + vtxs[i].TexCoords[j].ToString(); } } } switch (prim) { case PrimitiveType.Triangles: var pTri = new triangles(); pTri.material = "NW4R_MATERIAL"; pTri.count = (ulong)(vtxCount / 3); // should be 1? dunno pTri.input = inputArray; StringBuilder pTriData = new StringBuilder(); for (int i = 0; i < vtxCount; i++) { pTriData.AppendFormat("{0} ", pVtxs[i]); } pTri.p = pTriData.ToString(); meshItems.Add(pTri); break; case PrimitiveType.TriangleStrip: StringBuilder pTriStripData = new StringBuilder(); for (int i = 0; i < vtxCount; i++) { pTriStripData.AppendFormat("{0} ", pVtxs[i]); } triStrips.Add(pTriStripData.ToString()); break; default: Console.WriteLine("UNIMPLEMENTED PRIMITIVE TYPE"); return geo; } } // If any tristrips were found, add them! if (triStrips.Count > 0) { var pTriStrips = new tristrips(); pTriStrips.material = "NW4R_MATERIAL"; pTriStrips.input = inputArray; pTriStrips.count = (ulong)triStrips.Count; pTriStrips.p = triStrips.ToArray(); meshItems.Add(pTriStrips); } m.Items = meshItems.ToArray(); // FINALLY DONE! return geo; } } }