private void OnEnable() { _target = (MaterialDef)target; // Try to refresh the database! SharedEditorDataManager.TryRefreshDatabaseAtStart(); }
public MaterialDef addMaterial(string name) { MaterialDef mat = new MaterialDef(name); materials.Add(name, mat); return(mat); }
public IEnumerable <Tri> TrianglesFor(MaterialDef material) { if (datas.ContainsKey(material.name)) { foreach (Tri t in datas[material.name].tris) { yield return(t); } } }
private Color TraceLight(Vector3 pos, Vector3 normal, Color diffuseColor, MaterialDef materialDef) { Color color = RGBAZero; foreach (Light light in lights) { if (light.enabled) { Color colorToAdd = LightTrace(light, pos, normal, diffuseColor, materialDef); colorToAdd.a = 1; color += colorToAdd; } } return(color); }
private Imgd FileLoader(string filePath, MaterialDef matDef, MapGenConfig config) { logger.Debug($"Load image from \"{filePath}\""); if (FileExtUtil.IsExtension(filePath, ".imd")) { return(ImageResizer.NormalizeImageSize(File.OpenRead(filePath).Using(s => Imgd.Read(s)))); } if (FileExtUtil.IsExtension(filePath, ".png")) { var imdFile = Path.ChangeExtension(filePath, ".imd"); if (config.skipConversionIfExists && File.Exists(imdFile)) { logger.Debug($"Skipping png to imd conversion, due to imd file existence and skipConversionIfExists option."); } else { try { logger.Debug($"Using ImgTool for png to imd conversion."); var imgtoolOptions = matDef.imgtoolOptions ?? config.imgtoolOptions ?? "-b 8"; var result = new RunCmd( "OpenKh.Command.ImgTool.exe", $"imd \"{filePath}\" -o \"{imdFile}\" {imgtoolOptions}" ); if (result.ExitCode != 0) { throw new Exception($"ImgTool failed ({result.ExitCode})"); } } catch (Win32Exception ex) { throw new Exception("ImgTool failed.", ex); } } return(FileLoader(imdFile, matDef, config)); } throw new NotSupportedException(Path.GetExtension(filePath)); }
private void cbMaterialsFromMatFile_SelectedIndexChanged(object sender, EventArgs e) { string path = getCurrentMatFileNamePath(); MtrFile mf = ms.findMtrFile(path); if (mf == null) { return; } string matName = cbMaterialsFromMatFile.Text; MaterialDef md = mf.findMaterialDef(matName); if (md == null) { return; } string txt = mf.getMaterialDefText(matName); tbMaterialText.Text = txt; }
private void ConvertModelIntoMapModel(string modelFile, MapGenConfig config) { logger.Debug($"Loading 3D model file \"{modelFile}\" using Assimp."); var assimp = new Assimp.AssimpContext(); var scene = assimp.ImportFile(modelFile, Assimp.PostProcessSteps.PreTransformVertices); bigMeshContainer = new BigMeshContainer(); var scale = config.scale; Matrix4x4 matrix = Matrix4x4.Identity; if (config.applyMatrix != null) { var m = config.applyMatrix; if (m.Length == 16) { matrix = new Matrix4x4( m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15] ); logger.Debug($"Apply matrix: {matrix}"); } } else { matrix *= scale; } logger.Debug($"Starting triangle strip conversion for {scene.Meshes.Count} meshes."); foreach (var inputMesh in scene.Meshes) { logger.Debug($"Mesh: {inputMesh.Name} ({inputMesh.FaceCount:#,##0} faces, {inputMesh.VertexCount:#,##0} vertices)"); var modelMat = scene.Materials[inputMesh.MaterialIndex]; var matDef = config.FindMaterial(modelMat.Name ?? "default") ?? MaterialDef.CreateFallbackFor(modelMat.Name); if (matDef.ignore) { logger.Info($"This mesh \"{inputMesh.Name}\" is not rendered due to ignore flag of material \"{modelMat.Name}\"."); continue; } var kh2Mesh = bigMeshContainer.AllocateBigMeshForMaterial(matDef); var diffuseTextureFile = modelMat.TextureDiffuse.FilePath; if (!string.IsNullOrEmpty(diffuseTextureFile)) { if (config.reuseImd) { logger.Debug($"The mesh \"{inputMesh.Name}\" material \"{matDef.name}\" has filepath \"{diffuseTextureFile}\" for diffuse texture. It will be associated with material's fromFile3. Setting preferable imd file to fromFile2 due to reuseImd flag."); matDef.fromFile2 = Path.ChangeExtension(diffuseTextureFile, ".imd"); matDef.fromFile3 = diffuseTextureFile; } else { logger.Debug($"The mesh \"{inputMesh.Name}\" material \"{matDef.name}\" has filepath \"{diffuseTextureFile}\" for diffuse texture. It will be associated with material's fromFile2."); matDef.fromFile3 = diffuseTextureFile; } } var kh2BaseVert = kh2Mesh.vertexList.Count; List <int> vertexToLocal = new List <int>(); foreach (var inputVertex in inputMesh.Vertices) { var vertex = Vector3.Transform( new Vector3(inputVertex.X, inputVertex.Y, inputVertex.Z), matrix ); var index = kh2Mesh.vertexList.IndexOf(vertex); if (index < 0) { index = kh2Mesh.vertexList.Count; kh2Mesh.vertexList.Add(vertex); } vertexToLocal.Add(index); } var localFaces = inputMesh.Faces .Select( set => set.Indices .Select(index => new VertPair { uvColorIndex = index, vertexIndex = vertexToLocal[index] }) .ToArray() ) .ToArray(); var inputTexCoords = inputMesh.TextureCoordinateChannels.First(); var inputVertexColorList = inputMesh.VertexColorChannels.First(); var hasVertexColor = inputMesh.VertexColorChannelCount >= 1; var maxIntensity = matDef.maxColorIntensity ?? config.maxColorIntensity ?? 128; var maxAlpha = matDef.maxAlpha ?? config.maxAlpha ?? 128; var triConverter = config.disableTriangleStripsOptimization ? (TriangleFansToTriangleStripsConverter)TriangleFansToTriangleStripsNoOpts : (TriangleFansToTriangleStripsConverter)TriangleFansToTriangleStripsOptimized; foreach (var triStripInput in triConverter(localFaces)) { var triStripOut = new BigMesh.TriangleStrip(); foreach (var vertPair in triStripInput) { triStripOut.vertexIndices.Add(kh2BaseVert + vertPair.vertexIndex); triStripOut.uvList.Add(Get2DCoord(inputTexCoords[vertPair.uvColorIndex])); if (hasVertexColor) { triStripOut.vertexColorList.Add(ConvertVertexColor(inputVertexColorList[vertPair.uvColorIndex], maxIntensity, maxAlpha)); } else { triStripOut.vertexColorList.Add(new Color(maxIntensity, maxIntensity, maxIntensity, maxAlpha)); } } kh2Mesh.triangleStripList.Add(triStripOut); } logger.Debug($"Output: {kh2Mesh.vertexList.Count:#,##0} vertices, {kh2Mesh.triangleStripList.Count:#,##0} triangle strips."); } logger.Debug($"The conversion has done."); logger.Debug($"Starting mesh splitter and vif packets builder."); mapModel = new Mdlx.M4 { VifPackets = new List <Mdlx.VifPacketDescriptor>(), }; foreach (var bigMesh in bigMeshContainer.MeshList .Where(it => it.textureIndex != -1) ) { foreach (var smallMesh in BigMeshSplitter.Split(bigMesh)) { var dmaPack = new MapVifPacketBuilder(smallMesh); smallMeshList.Add(smallMesh); bigMesh.vifPacketIndices.Add(Convert.ToUInt16(mapModel.VifPackets.Count)); smallMesh.vifPacketIndices.Add(Convert.ToUInt16(mapModel.VifPackets.Count)); mapModel.VifPackets.Add( new Mdlx.VifPacketDescriptor { VifPacket = dmaPack.vifPacket.ToArray(), TextureId = smallMesh.textureIndex, DmaPerVif = new ushort[] { dmaPack.firstVifPacketQwc, 0, }, IsTransparentFlag = smallMesh.matDef.transparentFlag ?? 0, } ); } } logger.Debug($"Output: {mapModel.VifPackets.Count:#,##0} vif packets."); logger.Debug($"The builder has done."); logger.Debug($"Starting vifPacketRenderingGroup builder."); // first group: render all mapModel.vifPacketRenderingGroup = new List <ushort[]>( new ushort[][] { Enumerable.Range(0, mapModel.VifPackets.Count) .Select(it => Convert.ToUInt16(it)) .ToArray() } ); logger.Debug($"Output: {mapModel.vifPacketRenderingGroup.Count:#,##0} groups."); mapModel.DmaChainIndexRemapTable = new List <ushort>( Enumerable.Range(0, mapModel.VifPackets.Count) .Select(it => Convert.ToUInt16(it)) .ToArray() ); }
internal void ReadXML(string xmlFile) { XmlDocument xml = new XmlDocument(); xml.Load(xmlFile); DefaultFallbackMTDName = xml.SelectSingleNode("material_info_bank/default_fallback_mtd_name").InnerText.ToLower(); var materialDefNodes = xml.SelectNodes("material_info_bank/material_def_list/material_def"); MaterialDefs.Clear(); foreach (XmlNode mdn in materialDefNodes) { var mat = new MaterialDef(); mat.ReadXML(mdn); if (!MaterialDefs.ContainsKey(mat.MTD)) { MaterialDefs.Add(mat.MTD, mat); } else { //throw new Exception($"MTD '{mat.MTD}' defined twice in material info bank."); MaterialDefs[mat.MTD] = mat; } } GXItemStructs.Clear(); var gxItemNodes = xml.SelectNodes("material_info_bank/gx_item_struct_list/gx_item_struct"); foreach (XmlNode gin in gxItemNodes) { string gxid = gin.SafeGetAttribute("gxid"); var structDef = new XmlStructDef(gin); GXItemStructs.Add(gxid, structDef); } DefaultGXItemDataExamples.Clear(); var matExamples = xml.SelectNodes("material_info_bank/MATERIAL_INSTANCE_EXAMPLE_LIST/MATERIAL_INSTANCE_EXAMPLE"); foreach (XmlNode matExNode in matExamples) { var mtd = matExNode.SafeGetAttribute("mtd").ToLower(); var exNodes = matExNode.SelectNodes("material_data_example"); List <byte[]> gxExamplesForThisMtd = new List <byte[]>(); foreach (XmlNode mx in exNodes) { var gxExamples = mx.SelectNodes("gx_item_list/gx_item"); foreach (XmlNode gn in gxExamples) { byte[] gxData = gn.InnerText.Split(' ').Select(x => byte.Parse(x, System.Globalization.NumberStyles.HexNumber)).ToArray(); gxExamplesForThisMtd.Add(gxData); } break; // Read only one for now } if (!DefaultGXItemDataExamples.ContainsKey(mtd)) { DefaultGXItemDataExamples.Add(mtd, gxExamplesForThisMtd); } } }
// TODO: Clean up this method, split in smaller parts, update it as more info is available private bool Read(BinaryReader br) { try { Header header = new Header(); header.Read(br); NodesTree nodesTree = new NodesTree(); nodesTree.Read(br); Name = nodesTree.name; br.BaseStream.Seek(header.ContentsTableOffset, SeekOrigin.Begin); ContentsTable contentsTable = new ContentsTable(); contentsTable.Read(br); MaterialsHeader materialsHeader = new MaterialsHeader(); materialsHeader.Read(br); ObjectsTable objectsTable = new ObjectsTable(); objectsTable.SetCount(contentsTable.objectsCount); objectsTable.Read(br); SubObjectsTable subObjectsTable = new SubObjectsTable(); subObjectsTable.SetCount(contentsTable.subObjectsCount); subObjectsTable.Read(br); // In theory, it could be read all the data from current stream position // but it would be safer to read by using the already read offsets // Read the materials table MaterialsTable materialsTable = new MaterialsTable(); materialsTable.SetCount(materialsHeader.materialsCount); br.BaseStream.Seek(materialsHeader.materialsTableOffset, SeekOrigin.Begin); materialsTable.Read(br); // Build the materials list for (int i = 0; i < materialsTable.GetCount(); i++) { Material mat = materialsTable[i].GetMaterial(br); Materials.Add(mat); } // TODO: Read data for all the subobjects // Read data for all the objects for (int i = 0; i < objectsTable.GetCount(); i++) { Model model = new Model(); br.BaseStream.Seek(objectsTable[i].nameAddress, SeekOrigin.Begin); model.name = Text.ReadText(br); if (objectsTable[i].materialDefsAddress != null && objectsTable[i].materialDefsCount > 0) { br.BaseStream.Seek(objectsTable[i].materialDefsAddress.Value, SeekOrigin.Begin); MaterialDef matDef = new MaterialDef(); matDef.Read(br); model.materialIndices = matDef.materialIndices; } if (objectsTable[i].childDefsAddress != null && objectsTable[i].childsDefsCount > 0) { br.BaseStream.Seek(objectsTable[i].childDefsAddress.Value, SeekOrigin.Begin); ChildsDef childsDef = new ChildsDef(); childsDef.SetCount(objectsTable[i].childsDefsCount); childsDef.Read(br); // TODO: Decide how to add it to the model } if (objectsTable[i].data1Address != null && objectsTable[i].data1Count > 0) { List <int> meshesAddress = new List <int>(); br.BaseStream.Seek(objectsTable[i].data1Address.Value, SeekOrigin.Begin); for (int j = 0; j < objectsTable[i].data1Count; j++) { int address = (int)ReadRelativeOffset(br); meshesAddress.Add(address); } for (int j = 0; j < objectsTable[i].data1Count; j++) { br.BaseStream.Seek(meshesAddress[j], SeekOrigin.Begin); Def def = new Def(); if (!def.Read(br)) { return(false); } if (def.nextChunk != null && def.nextChunk is MeshDef) { MeshDef md = (MeshDef)def.nextChunk; for (int k = 0; k < md.meshesData.Count; k++) { MeshData data = md.meshesData[k]; Mesh mesh = new Mesh(data.boundingBox, data.vertices, data.triangles, data.textureVertices); model.meshes.Add(mesh); } } } } Models.Add(model); } } catch { // TODO: Handle here any error reading return(false); } return(true); }
private Color LightTrace(Light light, Vector3 pos, Vector3 normal, Color diffuseColor, MaterialDef materialDef) { float diffuse; if (light.type == LightType.Directional) { diffuse = Vector3.Dot(-light.transform.forward, normal); if (diffuse > 0) { if (Physics.Raycast(pos, -light.transform.forward, Mathf.Infinity, collisionMask)) { return(RGBAZero); } Vector3 viewVector = (cameraToTrace.transform.position - pos).normalized; Vector3 halfVector = (-light.transform.forward + viewVector).normalized; float dot = Vector3.Dot(halfVector, normal); float spec = 0; if (dot > 0) { spec = Mathf.Pow(dot, 128); } return(light.color * light.intensity * spec * materialDef.Ks + diffuseColor * diffuse * materialDef.Kd); } } else { float distance = Vector3.Distance(pos, light.transform.position); Vector3 direction = (light.transform.position - pos).normalized; diffuse = Vector3.Dot(normal, direction); if (distance < light.range && diffuse > 0) { if (light.type == LightType.Point) { if (Physics.Raycast(pos, direction, distance, collisionMask)) { return(RGBAZero); } Vector3 viewVector = (cameraToTrace.transform.position - pos).normalized; Vector3 halfVector = (direction + viewVector).normalized; float dot = Vector3.Dot(halfVector, normal); float spec = 0; if (dot > 0) { spec = Mathf.Pow(dot, 128); } float r = distance / light.range; float Attenuation = 1.0f + 25.0f * r * r; return((light.color * spec * materialDef.Ks + diffuseColor * diffuse * materialDef.Kd) * light.intensity / Attenuation); } else if (light.type == LightType.Spot) { float spotFactor = Vector3.Dot(-light.transform.forward, direction); float spotCulloff = Mathf.Cos((light.spotAngle / 2.0f) * Mathf.Deg2Rad); if (spotFactor > spotCulloff) { if (Physics.Raycast(pos, direction, distance, collisionMask)) { return(RGBAZero); } Vector3 viewVector = (cameraToTrace.transform.position - pos).normalized; Vector3 halfVector = (direction + viewVector).normalized; float dot = Vector3.Dot(halfVector, normal); float spec = 0; if (dot > 0) { spec = Mathf.Pow(dot, 128); } return((light.color * spec * materialDef.Ks + diffuseColor * diffuse * materialDef.Kd) * light.intensity * (1.0f - distance / light.range) * (1.0f - (1.0f - spotFactor) / (1.0f - spotCulloff))); } } else if (light.type == LightType.Area) { Color color = RGBAZero; float sampleNumber = 2; Vector2 lightArea = light.areaSize; float sampleRengeX = lightArea.x / sampleNumber; float sampleRengeY = lightArea.y / sampleNumber; int sampleX = (int)(lightArea.x / sampleRengeX); int sampleY = (int)(lightArea.y / sampleRengeY); float r = distance / light.range; float Attenuation = 1.0f + 25.0f * r * r; for (int i = 0; i < sampleY; i++) { for (int j = 0; j < sampleX; j++) { float offsetRight = lightArea.x * (((sampleX - j) / sampleX) - 0.5f); float offsetUp = lightArea.y * (((sampleY - i) / sampleY) - 0.5f); float u = Random.Range(0.0f, 1f); float v = Random.Range(0.0f, 1f); float sida = 2 * Mathf.PI * u; float radius = Mathf.Sqrt(v); //offsetRight = lightArea.x * Mathf.Cos(sida) * radius; //offsetUp = lightArea.y * Mathf.Sin(sida) * radius; Vector3 lightPos = light.transform.position + light.transform.right * offsetRight + light.transform.up * offsetUp; if (Physics.Raycast(pos, (lightPos - pos).normalized, distance, collisionMask)) { break; } Vector3 Ldirection = (lightPos - pos).normalized; Vector3 viewVector = (cameraToTrace.transform.position - pos).normalized; Vector3 halfVector = (Ldirection + viewVector).normalized; float dot = Vector3.Dot(halfVector, normal); float spec = 0; if (dot > 0) { spec = Mathf.Pow(dot, 128); } color += (light.color * spec * materialDef.Ks + diffuseColor * diffuse * materialDef.Kd) / (sampleX * sampleY); //integrate += (spec) * sampleRengeX * sampleRengeY; } } color *= light.intensity / Attenuation; //return (light.color * spec * materialDef.Ks + diffuseColor * diffuse * materialDef.Kd) * light.intensity / Attenuation; return(color); } } } return(RGBAZero); }
private Color TraceRay(Ray ray, int depth, MeshCollider lastHitCollider, bool innerRay, out Vector3 hitPosition) { Color color = RGBAZero; RaycastHit hit = new RaycastHit(); hitPosition = Vector3.zero; if (depth < 5) { if (innerRay) { Ray inverseRay = new Ray(); inverseRay.origin = ray.GetPoint(999.0f); inverseRay.direction = -ray.direction; RaycastHit[] hits; hits = Physics.RaycastAll(inverseRay, 999, collisionMask); if (hits.Length <= 0) { return(color); } else { Vector3 offsetOrigin = ray.GetPoint(0.0001f); float maxDist = -1; for (int hitId = 0; hitId < hits.Length; ++hitId) { Vector3 hitDir = (hits[hitId].point - offsetOrigin).normalized; if (Vector3.Dot(hitDir, ray.direction) > 0) { float distToOrigin = Vector3.Distance(hits[hitId].point, inverseRay.origin); if (distToOrigin > maxDist) { maxDist = distToOrigin; hit = hits[hitId]; } } } if (maxDist < 0) { return(color); } } } else { if (!Physics.Raycast(ray, out hit, Mathf.Infinity, collisionMask)) { return(color); } } MaterialDef materialDef = hit.transform.GetComponent <MaterialDef>(); MeshCollider collider = hit.collider as MeshCollider; Mesh mesh = collider.sharedMesh; int[] triangles = mesh.triangles; Vector3[] normals = mesh.normals; Vector3 barycentry = hit.barycentricCoordinate; Vector3 interpolateNormal = normals[triangles[hit.triangleIndex * 3 + 0]] * barycentry.x + normals[triangles[hit.triangleIndex * 3 + 1]] * barycentry.y + normals[triangles[hit.triangleIndex * 3 + 2]] * barycentry.z; interpolateNormal.Normalize(); interpolateNormal = hit.transform.TransformDirection(interpolateNormal); if (innerRay) { interpolateNormal = -interpolateNormal; } hitPosition = hit.point; switch (materialDef.materialType) { case MaterialType.REFLECTION_AND_REFRACTION: { Vector3 inDir = ray.direction; Color newDiffuseColor = RGBAZero; float kr; fresnel(inDir, interpolateNormal, materialDef.Ior, out kr); //reflect Vector3 reflectionDir = Vector3.Reflect(inDir, interpolateNormal); Vector3 reflectionOrigin = (Vector3.Dot(reflectionDir, interpolateNormal) < 0) ? (hit.point - interpolateNormal * 0.00001f) : (hit.point + interpolateNormal * 0.00001f); Vector3 reflectionHitPosition; Color reflectionColor = TraceRay(new Ray(reflectionOrigin, reflectionDir), depth + 1, collider, innerRay, out reflectionHitPosition); newDiffuseColor += reflectionColor * kr; Vector3 refractionDir = Vector3.zero; Vector3 refractionOrigin = Vector3.zero; Vector3 refractionHitPosition = Vector3.zero; //refract if (kr < 1.0f) { refractionDir = refract(inDir, interpolateNormal, materialDef.Ior); refractionOrigin = (Vector3.Dot(refractionDir, interpolateNormal) < 0) ? (hit.point - interpolateNormal * 0.00001f) : (hit.point + interpolateNormal * 0.00001f); Color refractionColor = TraceRay(new Ray(refractionOrigin, refractionDir), depth + 1, collider, !innerRay, out refractionHitPosition); float absorbDist = 0; float absorbR = 1; float absorbG = 1; float absorbB = 1; if ((depth + 1 < 5) && !innerRay) { absorbDist = Vector3.Distance(hit.point, refractionHitPosition); absorbR = Mathf.Exp(-materialDef.ColorAbsorb.x * absorbDist); absorbG = Mathf.Exp(-materialDef.ColorAbsorb.y * absorbDist); absorbB = Mathf.Exp(-materialDef.ColorAbsorb.z * absorbDist); } newDiffuseColor += refractionColor * (1 - kr) * new Color(absorbR, absorbG, absorbB); } if (drawDebugRay) { if (kr < 1.0f) { Debug.DrawLine(refractionOrigin, refractionHitPosition, Color.blue, 10.0f); } Debug.DrawLine(reflectionOrigin, reflectionHitPosition, Color.black, 10.0f); Debug.DrawLine(hit.point, hit.point + interpolateNormal * 5, Color.green, 10.0f); } color = TraceLight(hit.point + interpolateNormal * 0.00001f, interpolateNormal, RGBAZero, materialDef) + newDiffuseColor; } break; case MaterialType.REFLECTION: { Vector3 inDir = ray.direction; float kr; fresnel(inDir, interpolateNormal, materialDef.Ior, out kr); Vector3 reflectionDir = Vector3.Reflect(inDir, interpolateNormal); Vector3 reflectionOrigin = (Vector3.Dot(reflectionDir, interpolateNormal) < 0) ? (hit.point + interpolateNormal * 0.00001f) : (hit.point - interpolateNormal * 0.00001f); Ray reflectionRay = new Ray(reflectionOrigin, reflectionDir); Vector3 reflectionHitPosition; color = TraceRay(reflectionRay, depth + 1, collider, innerRay, out reflectionHitPosition) * kr + TraceLight(hit.point + interpolateNormal * 0.00001f, interpolateNormal, RGBAZero, materialDef); if (drawDebugRay) { Debug.DrawLine(reflectionOrigin, reflectionHitPosition, Color.red, 10.0f); Debug.DrawLine(hit.point, hit.point + interpolateNormal * 5, Color.green, 10.0f); } } break; case MaterialType.DIFFUSE_AND_GLOSSY: { Color diffuseColor = RGBAZero; Material mat = hit.transform.GetComponent <Renderer>().material; if (mat.mainTexture != null) { Vector2 texCoord = hit.textureCoord; diffuseColor += (mat.mainTexture as Texture2D).GetPixelBilinear(texCoord.x, texCoord.y); } else { diffuseColor += mat.color; } color = TraceLight(hit.point + interpolateNormal * 0.00001f, interpolateNormal, diffuseColor, materialDef); if (drawDebugRay) { Debug.DrawLine(hit.point, hit.point + interpolateNormal * 5, Color.green, 10.0f); } } break; default: break; } } return(color); }
public bool hasTrianglesFor(MaterialDef m) { List <Tri> t = new List <Tri>(TrianglesFor(m)); return(t.Count != 0); }