using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using NUnit.Framework; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Geometry.Parametric; using SharpGLTF.Schema2; using SharpGLTF.Scenes; namespace SharpGLTF.Geometry { using VERTEX1 = VertexBuilder; using VERTEX2 = VertexBuilder; [Category("Toolkit.Geometry")] public class MeshBuilderTests { [Test] public void CreateInvalidTriangles() { var m = new Materials.MaterialBuilder(); var mb = VERTEX2.CreateCompatibleMesh(); // replaces default preprocessor with a debug preprocessor that throws exceptions at the slightest issue. mb.VertexPreprocessor.SetValidationPreprocessors(); int TriangleCounter() { return mb.Primitives.Sum(item => item.Triangles.Count); } var prim = mb.UsePrimitive(m); var a = VERTEX2 .Create(Vector3.Zero,Vector3.UnitX) .WithMaterial(Vector4.One,Vector2.Zero) .WithSkinning((0,1)); var b = VERTEX2 .Create(Vector3.UnitX, Vector3.UnitX) .WithMaterial(Vector4.One, Vector2.Zero) .WithSkinning((0, 1)); var c = VERTEX2 .Create(Vector3.UnitY, Vector3.UnitX) .WithMaterial(Vector4.One, Vector2.Zero) .WithSkinning((0, 1)); prim.AddTriangle(a, b, c); Assert.That(TriangleCounter(), Is.EqualTo(1)); var v2nan = new Vector2(float.NaN, float.NaN); var v3nan = new Vector3(float.NaN, float.NaN, float.NaN); var v4nan = new Vector4(float.NaN, float.NaN, float.NaN, float.NaN); Assert.Throws(typeof(ArgumentException), () => prim.AddTriangle(a.WithGeometry(v3nan), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentException), () => prim.AddTriangle(a.WithGeometry(Vector3.Zero, v3nan), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentOutOfRangeException), () => prim.AddTriangle(a.WithGeometry(Vector3.Zero, Vector3.Zero), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentOutOfRangeException), () => prim.AddTriangle(a.WithGeometry(Vector3.Zero, Vector3.UnitX * 0.8f), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentException), () => prim.AddTriangle(a.WithMaterial(v2nan), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentException), () => prim.AddTriangle(a.WithMaterial(v4nan), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentOutOfRangeException), () => prim.AddTriangle(a.WithMaterial(Vector4.One*2), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentOutOfRangeException), () => prim.AddTriangle(a.WithMaterial(-Vector4.One), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); Assert.Throws(typeof(ArgumentOutOfRangeException), () => prim.AddTriangle(a.WithSkinning((0,0)), b, c)); Assert.That(TriangleCounter(), Is.EqualTo(1)); } [Test] public void CreateMeshInSanitizedMode() { var mesh = VERTEX2.CreateCompatibleMesh(); mesh.VertexPreprocessor.SetSanitizerPreprocessors(); var prim = mesh.UsePrimitive(Materials.MaterialBuilder.CreateDefault(), 1); var p = new VertexPositionNormal(Vector3.UnitX, new Vector3(float.NaN)); var m = new VertexColor1Texture1(Vector4.One * 7, new Vector2(float.NaN)); var s = new VertexJoints4((0, 2), (1, 7), (2, 6), (3, 5)); var v1Idx = prim.AddPoint(new VERTEX2(p, m, s)); var v1Bis = prim.Vertices[v1Idx]; NumericsAssert.AreEqual(v1Bis.Geometry.Position, Vector3.UnitX); NumericsAssert.AreEqual(v1Bis.Geometry.Normal, Vector3.UnitX); NumericsAssert.AreEqual(v1Bis.Material.Color, Vector4.One); NumericsAssert.AreEqual(v1Bis.Material.TexCoord, Vector2.Zero); NumericsAssert.AreEqual(v1Bis.Skinning.Joints, new Vector4(1, 2, 3, 0)); NumericsAssert.AreEqual(v1Bis.Skinning.Weights, new Vector4(7, 6, 5, 2) / (7f + 6f + 5f + 2f)); } [Test] public static void CreateWithDegeneratedTriangle() { // create materials var material1 = new Materials.MaterialBuilder() .WithMetallicRoughnessShader() .WithChannelParam(Materials.KnownChannel.BaseColor, Materials.KnownProperty.RGBA, Vector4.One * 0.5f); var material2 = new Materials.MaterialBuilder() .WithMetallicRoughnessShader() .WithChannelParam(Materials.KnownChannel.BaseColor, Materials.KnownProperty.RGBA, Vector4.One * 0.7f); // create a mesh with degenerated triangles var validTriangle = ( new Vector3(4373.192624189425f, 5522.678275192156f, -359.8238015332605f), new Vector3(4370.978060142137f, 5522.723320999183f, -359.89184701762827f), new Vector3(4364.615741107147f, 5511.510615546256f, -359.08922455413233f) ); var degeneratedTriangle = ( new Vector3(4374.713581837248f, 5519.741978117265f, -360.87014389818034f), new Vector3(4373.187151107471f, 5521.493282925338f, -355.70835120644153f), new Vector3(4373.187151107471f, 5521.493282925338f, -355.70835120644153f) ); var mesh = new MeshBuilder("mesh"); mesh.VertexPreprocessor.SetValidationPreprocessors(); var validIndices = mesh.UsePrimitive(material1) .AddTriangle ( new VertexPosition(validTriangle.Item1), new VertexPosition(validTriangle.Item2), new VertexPosition(validTriangle.Item3) ); Assert.That(validIndices.A, Is.GreaterThanOrEqualTo(0)); Assert.That(validIndices.B, Is.GreaterThanOrEqualTo(0)); Assert.That(validIndices.C, Is.GreaterThanOrEqualTo(0)); var degenIndices = mesh.UsePrimitive(material2) .AddTriangle ( new VertexPosition(degeneratedTriangle.Item1), new VertexPosition(degeneratedTriangle.Item2), new VertexPosition(degeneratedTriangle.Item3) ); Assert.That(degenIndices.A, Is.LessThan(0)); Assert.That(degenIndices.B, Is.LessThan(0)); Assert.That(degenIndices.C, Is.LessThan(0)); // create scene: var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); // check gltf2 var model = scene.ToGltf2(); Assert.That(model.LogicalMeshes[0].Primitives, Has.Count.EqualTo(1)); } [Test] public static void CreateWithMutableSharedMaterial() { // create materials var material1 = Materials.MaterialBuilder.CreateDefault(); var material2 = Materials.MaterialBuilder.CreateDefault(); var material3 = Materials.MaterialBuilder.CreateDefault(); Assert.That(Materials.MaterialBuilder.AreEqualByContent(material1, material2), Is.True); Assert.That(Materials.MaterialBuilder.AreEqualByContent(material1, material3), Is.True); Assert.That(material2 != material1); Assert.That(material3 != material1); // MeshBuilder should split primitives by material reference, // because in general, materials will not be immutable. var mesh = new MeshBuilder(); mesh.UsePrimitive(material1, 1).AddPoint(default); mesh.UsePrimitive(material2, 1).AddPoint(default); mesh.UsePrimitive(material3, 1).AddPoint(default); Assert.That(mesh.Primitives, Has.Count.EqualTo(3)); // create scene var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); // check gltf // The build process should identify that, at this point, material1, material2 and material3 // represent the same material, and coalesce to a single material. var gltfModel = scene.ToGltf2(); Assert.That(gltfModel.LogicalMaterials, Has.Count.EqualTo(1)); // since Materials.MaterialBuilder is not immutable we can change the contents, // so now, material1, material2 and material3 no longer represent the same material material1.WithMetallicRoughnessShader().WithChannelParam(Materials.KnownChannel.BaseColor, Materials.KnownProperty.RGBA, Vector4.One * 0.2f); material2.WithMetallicRoughnessShader().WithChannelParam(Materials.KnownChannel.BaseColor, Materials.KnownProperty.RGBA, Vector4.One * 0.4f); material3.WithMetallicRoughnessShader().WithChannelParam(Materials.KnownChannel.BaseColor, Materials.KnownProperty.RGBA, Vector4.One * 0.6f); gltfModel = scene.ToGltf2(); Assert.That(gltfModel.LogicalMaterials, Has.Count.EqualTo(3)); } [Test] public void CreateMeshWithTriangleAndQuad() { var dmat = Materials.MaterialBuilder.CreateDefault(); var mesh = VERTEX1.CreateCompatibleMesh(); var prim = mesh.UsePrimitive(dmat); var triIdx = prim.AddTriangle(new VERTEX1(Vector3.Zero), new VERTEX1(Vector3.UnitX * 2) , new VERTEX1(Vector3.UnitY * 2)); var qadIdx = prim.AddQuadrangle(new VERTEX1(-Vector3.UnitX), new VERTEX1(Vector3.UnitY), new VERTEX1(Vector3.UnitX), new VERTEX1(-Vector3.UnitY)); Assert.That(prim.Vertices, Has.Count.EqualTo(7)); Assert.That(prim.Triangles, Has.Count.EqualTo(3)); Assert.That(prim.Surfaces, Has.Count.EqualTo(2)); Assert.That(triIdx, Is.EqualTo((0, 1, 2))); Assert.That(qadIdx, Is.EqualTo((3, 4, 5, 6))); Assert.That(prim.GetIndices(), Is.EqualTo(new[] { 0, 1, 2, 3, 4, 5, 3, 5, 6 })); } [Test] public void CreateMeshWithCustomVertexAttribute() { var dmat = Materials.MaterialBuilder.CreateDefault(); var mesh = new MeshBuilder(); var prim = mesh.UsePrimitive(dmat); prim.AddTriangle ( (Vector3.UnitX, (Vector4.One, Vector2.Zero, 0.1f)), (Vector3.UnitY, (Vector4.One, Vector2.Zero, 0.2f)), (Vector3.UnitZ, (Vector4.One, Vector2.Zero, 0.3f)) ); var dstScene = new Schema2.ModelRoot(); var dstMesh = dstScene.CreateMesh(mesh); var batchId = dstMesh.Primitives[0].GetVertexAccessor(VertexColor1Texture1Custom1.CUSTOMATTRIBUTENAME).AsScalarArray(); Assert.That(batchId, Is.EqualTo(new float[] { 0.1f, 0.2f, 0.3f })); } [Test] public void GenerateTangents() { var vertices = new VertexBufferColumns(); vertices.Positions = new[] { Vector3.Zero, Vector3.UnitX, Vector3.UnitY }; vertices.Normals = new[] { Vector3.UnitZ, Vector3.UnitZ, Vector3.UnitZ }; vertices.TexCoords0 = new[] { Vector2.Zero, Vector2.UnitX, Vector2.UnitY }; var indices = new[] { (0,1,2) } as IEnumerable<(int,int,int)>; VertexBufferColumns.CalculateTangents(new[] { (vertices, indices) }); } [Test] public void CloneMeshBuilder() { var material1 = Materials.MaterialBuilder.CreateDefault(); var material2 = Materials.MaterialBuilder.CreateDefault(); var mesh = new MeshBuilder(); mesh.AddCube(material1, Matrix4x4.Identity); mesh.AddSphere(material2, 5, Matrix4x4.CreateTranslation(0, 10, 0)); var cloned1 = mesh.Clone( m => m.Clone() ); var primitivePairs = mesh.Primitives.Zip(cloned1.Primitives, (src, dst) => (src, dst)); foreach(var (src, dst) in primitivePairs) { Assert.That(dst.Material, Is.Not.SameAs(src.Material)); Assert.That(dst.Triangles, Has.Count.EqualTo(src.Triangles.Count)); Assert.That(dst.Triangles, Is.EqualTo(src.Triangles)); } var material3 = Materials.MaterialBuilder.CreateDefault(); // force all geometries to use a single material, // which should result in a mesh with with all the primitives // of the source mesh merged into a single primitive. var cloned2 = cloned1.Clone(m => material3); Assert.That(cloned2.Primitives.Count, Is.EqualTo(1)); Assert.That(cloned2.Primitives.First().Material, Is.SameAs(material3)); Assert.That(cloned2.Primitives.Sum(item => item.Triangles.Count), Is.EqualTo(cloned1.Primitives.Sum(item => item.Triangles.Count))); } } }