Axiom3D:Ogre公告板集与合并批次
在上文中,我们把Ogre里的网格分解成点线面后,我们要完成一个新的功能,在点上突出显示.
得到顶点位置后,这个功能也就是一个很简单的事,最开始是每个顶点添加一个子节点,节点上添加一个圆点.
foreach (var vect in this.Points) { var cEntity = EntityBuilder.Instance.SphereX1.Clone(Guid.NewGuid().ToString()); cEntity.UserData = vect; var cNode = this.PointNode.CreateChildSceneNode(vect.VertexInfo.Position); cNode.Scale = Vector3.UnitScale * scale; cNode.AttachObject(cEntity); }
这个是最简单的,但是添加后,FPS直接下降到100,虽然100现在是够了,但是后面肯定还有新的内容,所以这种方案是放弃的.
前面这个Mesh有约300个顶点,这种方法添加三百个Entity,只有一个SubEntity的这种,但是也会造成三百个批次,FPS肯定下降的厉害,我们要做的就是减少批次,于是我们采用StaticGeometry,如下:
StaticGeometry sg = Root.Instance.SceneManager.CreateStaticGeometry("point_sg", 0); foreach (var vect in this.Points) { var scale = mesh.BoundingBox.Size.Length / 5; sg.AddEntity(cEntity, vect.VertexInfo.Position, Quaternion.Identity, Vector3.UnitScale * scale); } sg.Build();
确实,我们能看到效果了,FPS几乎不变,只有少数下降,但是staticGeometry有个缺点,他是独立的,与原Node上实体没有关系了,就是如果我移动了Node的位置,点的位置没有变化.
本来在就我想直接设置point_size就好,不过有个问题,这个参数和硬件有关,不同的硬件可能大小不一样,还有就是这个点上只能是球形,颜色也要求一样,这几样都达不到要求,我考虑把所有球放入一个Mesh里,一个Mesh对应一个SubMesh,在我原来认为,Ogre会自动把同材质的SubMesh做一个批次渲染,但是FPS告诉我们,一个SubMesh对应一个批次,同材质也是.
ManualObject mo = new ManualObject(this.MeshName + "point/Show"); foreach (var vect in this.Points) { var scale = mesh.BoundingBox.Size.Length / 5; var sphereX1 = new Sphere3D(); sphereX1.Center = vect.VertexInfo.Position; sphereX1.ThetaDiv = 24; sphereX1.Radius = 0.1 * scale; sphereX1.ToManualObjectSection(mo, "MeshViewer/AxisX"); }
我们知道ManualObject一个begin与end对就Mesh里的一个SubMesh,Ogre没给我们合并,所以还是三百多个批次,FPS又下降下二位数了.我查了下,公告板集合这个东东,下面是公告板集合的代码与效果:
BillboardSet set = Root.Instance.SceneManager.CreateBillboardSet(); set.BillboardType = BillboardType.Point; var scale = mesh.BoundingBox.Size.Length * 0.1; set.SetDefaultDimensions(scale, scale); foreach (var vect in this.Points) { set.CreateBillboard(vect.VertexInfo.Position, ColorEx.DarkRed); } this.PointNode.AttachObject(set);
公告板给我们生成始终朝向我们的四边形,这里没用上面的模型,用公告板也能达到FPS不下降的效果,但是他顶点固定显示成四边形,也不能显示成别的模型现在看来还是达不到我们的需求,需要注意的是,默认的射线相交查询不会得到公告板集,需要我们设置query.QueryTypeMask |= (uint)SceneQueryTypeMask.Fx;因为默认的场景创建的CreateRayQuery并不包含公告板集的查询mask,我们需要加上公告板集的mask(SceneQueryTypeMask.Fx).
其实现在这里,差不多就清楚了,减少渲染批次,StaticGeometry与InstancedGeometry就有这个效果,我们查看相应源码GeometryBucket.cs里的类GeometryBucket与他的方法Assign,Build就很清楚了,思路有点像前文用EdgeListBuilder分析模型重组得到所有顶点,三角形这个过程.Build就是把多个顶点缓冲与索引缓冲组合成一个顶点缓冲与一个索引缓冲的情况,当然根据MaterialBucket这个类来看,肯定是只能组合同材质的,并且我们不需要考虑staticGeometry里模型的LOD,材质等情况,我们完全能对应我们这里的需求,写个简单方便易用的类.
public class MergerBatch { private List<Vector3> positions; private List<uint> colors; private List<int> indexs; private int startIndex = 0; private AxisAlignedBox box = null; private bool HaveColor { get { return this.colors != null && this.colors.Count > 0; } } public MergerBatch() { positions = new List<Vector3>(); colors = new List<uint>(); indexs = new List<int>(); startIndex = 0; box = new AxisAlignedBox(); } public void AddBatch(IList<Vector3> pos, IList<int> index, ColorEx color) { foreach (var p in pos) { this.positions.Add(p); this.colors.Add((uint)color.ToRGBA()); box.Merge(p); } foreach (var i in index) { this.indexs.Add(i + startIndex); } startIndex = this.positions.Count; } public Mesh ToMesh(string meshName, bool bRecreate = false) { var mesh = MeshManager.Instance.GetByName(meshName) as Mesh; if (mesh != null) { if (bRecreate) { MeshManager.Instance.Remove(meshName); } else { return mesh; } } mesh = MeshManager.Instance.CreateManual(meshName, "General", null); mesh.SharedVertexData = new VertexData(); mesh.SharedVertexData.vertexCount = this.positions.Count; //顶点位置数据写入顶点缓冲 VertexDeclaration decl = mesh.SharedVertexData.vertexDeclaration; decl.AddElement(0, 0, VertexElementType.Float3, VertexElementSemantic.Position); HardwareVertexBuffer vbuf = HardwareBufferManager.Instance.CreateVertexBuffer( decl.Clone(0), mesh.SharedVertexData.vertexCount, BufferUsage.StaticWriteOnly); vbuf.WriteData(0, vbuf.Size, this.positions.ToArray(), true); //写入颜色 decl.AddElement(1, 0, VertexElementType.Color, VertexElementSemantic.Diffuse); HardwareVertexBuffer cbuf = HardwareBufferManager.Instance.CreateVertexBuffer( decl.Clone(1), mesh.SharedVertexData.vertexCount, BufferUsage.StaticWriteOnly); cbuf.WriteData(0, cbuf.Size, this.colors.ToArray(), true); VertexBufferBinding bind = mesh.SharedVertexData.vertexBufferBinding; bind.SetBinding(0, vbuf); bind.SetBinding(1, cbuf); //建立一个subMesh HardwareIndexBuffer ibuf = HardwareBufferManager.Instance.CreateIndexBuffer(IndexType.Size32, this.indexs.Count, BufferUsage.StaticWriteOnly); ibuf.WriteData(0, ibuf.Size, this.indexs.ToArray(), true); SubMesh sub = mesh.CreateSubMesh(); sub.useSharedVertices = true; sub.indexData = new IndexData(); sub.indexData.indexBuffer = ibuf; sub.indexData.indexCount = this.indexs.Count; sub.indexData.indexStart = 0; //sub.MaterialName = "Voxel/Default"; mesh.BoundingBox = box; mesh.Load(); return mesh; } public void Clear() { this.positions.Clear(); this.colors.Clear(); this.indexs.Clear(); startIndex = 0; } }
这个类把多次顶点与索引组合成一个Mesh,我们需要一个方便生成一些简单几何模型的类,下面是从开源项目HeilxToolkit里相关代码移植过来的.
public class MeshBuilder { /// <summary> /// 'All curves should have the same number of Vector2s' exception message. /// </summary> private const string AllCurvesShouldHaveTheSameNumberOfVector2s = "All curves should have the same number of Vector2s"; /// <summary> /// 'Source mesh normal vectors should not be null' exception message. /// </summary> private const string SourceMeshNormalsShouldNotBeNull = "Source mesh normal vectors should not be null."; /// <summary> /// 'Source mesh texture coordinates should not be null' exception message. /// </summary> private const string SourceMeshTextureCoordinatesShouldNotBeNull = "Source mesh texture coordinates should not be null."; /// <summary> /// 'Wrong number of diameters' exception message. /// </summary> private const string WrongNumberOfDiameters = "Wrong number of diameters."; /// <summary> /// 'Wrong number of angles' exception message. /// </summary> private const string WrongNumberOfAngles = "Wrong number of angles."; /// <summary> /// 'Wrong number of positions' exception message. /// </summary> private const string WrongNumberOfPositions = "Wrong number of positions."; /// <summary> /// 'Wrong number of normal vectors' exception message. /// </summary> private const string WrongNumberOfNormals = "Wrong number of normal vectors."; /// <summary> /// 'Wrong number of texture coordinates' exception message. /// </summary> private const string WrongNumberOfTextureCoordinates = "Wrong number of texture coordinates."; /// <summary> /// The circle cache. /// </summary> private static readonly ThreadLocal<Dictionary<int, IList<Vector2>>> CircleCache = new ThreadLocal<Dictionary<int, IList<Vector2>>>(() => new Dictionary<int, IList<Vector2>>()); /// <summary> /// The unit sphere cache. /// </summary> private static readonly ThreadLocal<Dictionary<int, MeshGeometry3D>> UnitSphereCache = new ThreadLocal<Dictionary<int, MeshGeometry3D>>(() => new Dictionary<int, MeshGeometry3D>()); /// <summary> /// The normal vectors. /// </summary> private IList<Vector3> normals; /// <summary> /// The positions. /// </summary> private IList<Vector3> positions; /// <summary> /// The texture coordinates. /// </summary> private IList<Vector2> textureCoordinates; /// <summary> /// The triangle indices. /// </summary> private IList<int> triangleIndices; /// <summary> /// Initializes a new instance of the <see cref="MeshBuilder"/> class. /// </summary> /// <remarks> /// Normal and texture coordinate generation are included. /// </remarks> public MeshBuilder() : this(true, true) { } /// <summary> /// Initializes a new instance of the <see cref="MeshBuilder"/> class. /// </summary> /// <param name="generateNormals"> /// Generate normal vectors. /// </param> /// <param name="generateTextureCoordinates"> /// Generate texture coordinates. /// </param> public MeshBuilder(bool generateNormals, bool generateTextureCoordinates) { this.positions = new List<Vector3>(); this.triangleIndices = new List<int>(); if (generateNormals) { this.normals = new List<Vector3>(); } if (generateTextureCoordinates) { this.textureCoordinates = new List<Vector2>(); } } /// <summary> /// Box face enumeration. /// </summary> [Flags] public enum BoxFaces { /// <summary> /// The top. /// </summary> Top = 0x1, /// <summary> /// The bottom. /// </summary> Bottom = 0x2, /// <summary> /// The left side. /// </summary> Left = 0x4, /// <summary> /// The right side. /// </summary> Right = 0x8, /// <summary> /// The front side. /// </summary> Front = 0x10, /// <summary> /// The back side. /// </summary> Back = 0x20, /// <summary> /// All sides. /// </summary> All = Top | Bottom | Left | Right | Front | Back } /// <summary> /// Gets the normal vectors of the mesh. /// </summary> /// <value>The normal vectors.</value> public IList<Vector3> Normals { get { return this.normals; } } /// <summary> /// Gets the positions collection of the mesh. /// </summary> /// <value> The positions. </value> public IList<Vector3> Positions { get { return this.positions; } } /// <summary> /// Gets the texture coordinates of the mesh. /// </summary> /// <value>The texture coordinates.</value> public IList<Vector2> TextureCoordinates { get { return this.textureCoordinates; } } /// <summary> /// Gets the triangle indices. /// </summary> /// <value>The triangle indices.</value> public IList<int> TriangleIndices { get { return this.triangleIndices; } } /// <summary> /// Gets or sets a value indicating whether to create normal vectors. /// </summary> /// <value> /// <c>true</c> if normal vectors should be created; otherwise, <c>false</c>. /// </value> public bool CreateNormals { get { return this.normals != null; } set { if (value && this.normals == null) { this.normals = new List<Vector3>(); } if (!value) { this.normals = null; } } } /// <summary> /// Gets or sets a value indicating whether to create texture coordinates. /// </summary> /// <value> /// <c>true</c> if texture coordinates should be created; otherwise, <c>false</c>. /// </value> public bool CreateTextureCoordinates { get { return this.textureCoordinates != null; } set { if (value && this.textureCoordinates == null) { this.textureCoordinates = new List<Vector2>(); } if (!value) { this.textureCoordinates = null; } } } /// <summary> /// Gets a circle section (cached). /// </summary> /// <param name="thetaDiv"> /// The number of division. /// </param> /// <returns> /// A circle. /// </returns> public static IList<Vector2> GetCircle(int thetaDiv) { IList<Vector2> circle; if (!CircleCache.Value.TryGetValue(thetaDiv, out circle)) { circle = new List<Vector2>(); CircleCache.Value.Add(thetaDiv, circle); for (int i = 0; i < thetaDiv; i++) { double theta = Math.PI * 2 * ((double)i / (thetaDiv - 1)); circle.Add(new Vector2(Math.Cos(theta), -Math.Sin(theta))); } } return circle; } /// <summary> /// Adds an arrow to the mesh. /// </summary> /// <param name="Vector21"> /// The start Vector2. /// </param> /// <param name="Vector22"> /// The end Vector2. /// </param> /// <param name="diameter"> /// The diameter of the arrow cylinder. /// </param> /// <param name="headLength"> /// Length of the head (relative to diameter). /// </param> /// <param name="thetaDiv"> /// The number of divisions around the arrow. /// </param> public void AddArrow(Vector3 Vector21, Vector3 Vector22, double diameter, double headLength = 3, int thetaDiv = 18) { var dir = Vector22 - Vector21; double length = dir.Length; double r = diameter / 2; var pc = new List<Vector2> { new Vector2(0, 0), new Vector2(0, r), new Vector2(length - (diameter * headLength), r), new Vector2(length - (diameter * headLength), r * 2), new Vector2(length, 0) }; this.AddRevolvedGeometry(pc, null, Vector21, dir, thetaDiv); } /// <summary> /// Adds the edges of a bounding box as cylinders. /// </summary> /// <param name="boundingBox"> /// The bounding box. /// </param> /// <param name="diameter"> /// The diameter of the cylinders. /// </param> public void AddBoundingBox(Rect boundingBox, double diameter) { var p0 = new Vector3(boundingBox.X, boundingBox.Y, boundingBox.Z); var p1 = new Vector3(boundingBox.X, boundingBox.Y + boundingBox.SizeY, boundingBox.Z); var p2 = new Vector3(boundingBox.X + boundingBox.SizeX, boundingBox.Y + boundingBox.SizeY, boundingBox.Z); var p3 = new Vector3(boundingBox.X + boundingBox.SizeX, boundingBox.Y, boundingBox.Z); var p4 = new Vector3(boundingBox.X, boundingBox.Y, boundingBox.Z + boundingBox.SizeZ); var p5 = new Vector3(boundingBox.X, boundingBox.Y + boundingBox.SizeY, boundingBox.Z + boundingBox.SizeZ); var p6 = new Vector3(boundingBox.X + boundingBox.SizeX, boundingBox.Y + boundingBox.SizeY, boundingBox.Z + boundingBox.SizeZ); var p7 = new Vector3(boundingBox.X + boundingBox.SizeX, boundingBox.Y, boundingBox.Z + boundingBox.SizeZ); Action<Vector3, Vector3> addEdge = (c1, c2) => this.AddCylinder(c1, c2, diameter, 10); addEdge(p0, p1); addEdge(p1, p2); addEdge(p2, p3); addEdge(p3, p0); addEdge(p4, p5); addEdge(p5, p6); addEdge(p6, p7); addEdge(p7, p4); addEdge(p0, p4); addEdge(p1, p5); addEdge(p2, p6); addEdge(p3, p7); } /// <summary> /// Adds a box aligned with the X, Y and Z axes. /// </summary> /// <param name="center"> /// The center Vector2 of the box. /// </param> /// <param name="xlength"> /// The length of the box along the X axis. /// </param> /// <param name="ylength"> /// The length of the box along the Y axis. /// </param> /// <param name="zlength"> /// The length of the box along the Z axis. /// </param> public void AddBox(Vector3 center, double xlength, double ylength, double zlength) { this.AddBox(center, xlength, ylength, zlength, BoxFaces.All); } /// <summary> /// Adds a box aligned with the X, Y and Z axes. /// </summary> /// <param name="rectangle"> /// The 3-D "rectangle". /// </param> public void AddBox(Rect rectangle) { this.AddBox( new Vector3(rectangle.X + (rectangle.SizeX * 0.5), rectangle.Y + (rectangle.SizeY * 0.5), rectangle.Z + (rectangle.SizeZ * 0.5)), rectangle.SizeX, rectangle.SizeY, rectangle.SizeZ, BoxFaces.All); } /// <summary> /// Adds a box with the specified faces, aligned with the X, Y and Z axes. /// </summary> /// <param name="center"> /// The center Vector2 of the box. /// </param> /// <param name="xlength"> /// The length of the box along the X axis. /// </param> /// <param name="ylength"> /// The length of the box along the Y axis. /// </param> /// <param name="zlength"> /// The length of the box along the Z axis. /// </param> /// <param name="faces"> /// The faces to include. /// </param> public void AddBox(Vector3 center, double xlength, double ylength, double zlength, BoxFaces faces) { this.AddBox(center, new Vector3(1, 0, 0), new Vector3(0, 1, 0), xlength, ylength, zlength, faces); } /// <summary> /// Adds a box with the specified faces, aligned with the specified axes. /// </summary> /// <param name="center">The center Vector2 of the box.</param> /// <param name="x">The x axis.</param> /// <param name="y">The y axis.</param> /// <param name="xlength">The length of the box along the X axis.</param> /// <param name="ylength">The length of the box along the Y axis.</param> /// <param name="zlength">The length of the box along the Z axis.</param> /// <param name="faces">The faces to include.</param> public void AddBox(Vector3 center, Vector3 x, Vector3 y, double xlength, double ylength, double zlength, BoxFaces faces = BoxFaces.All) { var z = x.Cross(y); if ((faces & BoxFaces.Front) == BoxFaces.Front) { this.AddCubeFace(center, x, z, xlength, ylength, zlength); } if ((faces & BoxFaces.Back) == BoxFaces.Back) { this.AddCubeFace(center, -x, z, xlength, ylength, zlength); } if ((faces & BoxFaces.Left) == BoxFaces.Left) { this.AddCubeFace(center, -y, z, ylength, xlength, zlength); } if ((faces & BoxFaces.Right) == BoxFaces.Right) { this.AddCubeFace(center, y, z, ylength, xlength, zlength); } if ((faces & BoxFaces.Top) == BoxFaces.Top) { this.AddCubeFace(center, z, y, zlength, xlength, ylength); } if ((faces & BoxFaces.Bottom) == BoxFaces.Bottom) { this.AddCubeFace(center, -z, y, zlength, xlength, ylength); } } /// <summary> /// Adds a (possibly truncated) cone. /// </summary> /// <param name="origin"> /// The origin. /// </param> /// <param name="direction"> /// The direction (normalization not required). /// </param> /// <param name="baseRadius"> /// The base radius. /// </param> /// <param name="topRadius"> /// The top radius. /// </param> /// <param name="height"> /// The height. /// </param> /// <param name="baseCap"> /// Include a base cap if set to <c>true</c> . /// </param> /// <param name="topCap"> /// Include the top cap if set to <c>true</c> . /// </param> /// <param name="thetaDiv"> /// The number of divisions around the cone. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Cone_(geometry). /// </remarks> public void AddCone( Vector3 origin, Vector3 direction, double baseRadius, double topRadius, double height, bool baseCap, bool topCap, int thetaDiv) { var pc = new List<Vector2>(); var tc = new List<double>(); if (baseCap) { pc.Add(new Vector2(0, 0)); tc.Add(0); } pc.Add(new Vector2(0, baseRadius)); tc.Add(1); pc.Add(new Vector2(height, topRadius)); tc.Add(0); if (topCap) { pc.Add(new Vector2(height, 0)); tc.Add(1); } this.AddRevolvedGeometry(pc, tc, origin, direction, thetaDiv); } /// <summary> /// Adds a cone. /// </summary> /// <param name="origin">The origin Vector2.</param> /// <param name="apex">The apex Vector2.</param> /// <param name="baseRadius">The base radius.</param> /// <param name="baseCap"> /// Include a base cap if set to <c>true</c> . /// </param> /// <param name="thetaDiv">The theta div.</param> public void AddCone(Vector3 origin, Vector3 apex, double baseRadius, bool baseCap, int thetaDiv) { var dir = apex - origin; this.AddCone(origin, dir, baseRadius, 0, dir.Length, baseCap, false, thetaDiv); } /// <summary> /// Adds a cube face. /// </summary> /// <param name="center"> /// The center of the cube. /// </param> /// <param name="normal"> /// The normal vector for the face. /// </param> /// <param name="up"> /// The up vector for the face. /// </param> /// <param name="dist"> /// The distance from the center of the cube to the face. /// </param> /// <param name="width"> /// The width of the face. /// </param> /// <param name="height"> /// The height of the face. /// </param> public void AddCubeFace(Vector3 center, Vector3 normal, Vector3 up, double dist, double width, double height) { var right = normal.Cross(up); var n = normal * dist / 2; up *= height / 2; right *= width / 2; var p1 = center + n - up - right; var p2 = center + n - up + right; var p3 = center + n + up + right; var p4 = center + n + up - right; int i0 = this.positions.Count; this.positions.Add(p1); this.positions.Add(p2); this.positions.Add(p3); this.positions.Add(p4); if (this.normals != null) { this.normals.Add(normal); this.normals.Add(normal); this.normals.Add(normal); this.normals.Add(normal); } if (this.textureCoordinates != null) { this.textureCoordinates.Add(new Vector2(1, 1)); this.textureCoordinates.Add(new Vector2(0, 1)); this.textureCoordinates.Add(new Vector2(0, 0)); this.textureCoordinates.Add(new Vector2(1, 0)); } this.triangleIndices.Add(i0 + 2); this.triangleIndices.Add(i0 + 1); this.triangleIndices.Add(i0 + 0); this.triangleIndices.Add(i0 + 0); this.triangleIndices.Add(i0 + 3); this.triangleIndices.Add(i0 + 2); } /// <summary> /// Adds a cylinder to the mesh. /// </summary> /// <param name="p1"> /// The first Vector2. /// </param> /// <param name="p2"> /// The second Vector2. /// </param> /// <param name="diameter"> /// The diameters. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the cylinder. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Cylinder_(geometry). /// </remarks> public void AddCylinder(Vector3 p1, Vector3 p2, double diameter, int thetaDiv) { Vector3 n = p2 - p1; double l = n.Length; n.Normalize(); this.AddCone(p1, n, diameter / 2, diameter / 2, l, false, false, thetaDiv); } /// <summary> /// Adds a collection of edges as cylinders. /// </summary> /// <param name="Vector2s"> /// The Vector2s. /// </param> /// <param name="edges"> /// The edge indices. /// </param> /// <param name="diameter"> /// The diameter of the cylinders. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the cylinders. /// </param> public void AddEdges(IList<Vector3> Vector2s, IList<int> edges, double diameter, int thetaDiv) { for (int i = 0; i < edges.Count - 1; i += 2) { this.AddCylinder(Vector2s[edges[i]], Vector2s[edges[i + 1]], diameter, thetaDiv); } } /// <summary> /// Adds an extruded surface of the specified curve. /// </summary> /// <param name="Vector2s"> /// The 2D Vector2s describing the curve to extrude. /// </param> /// <param name="xaxis"> /// The x-axis. /// </param> /// <param name="p0"> /// The start origin of the extruded surface. /// </param> /// <param name="p1"> /// The end origin of the extruded surface. /// </param> /// <remarks> /// The y-axis is determined by the cross product between the specified x-axis and the p1-origin vector. /// </remarks> public void AddExtrudedGeometry(IList<Vector2> Vector2s, Vector3 xaxis, Vector3 p0, Vector3 p1) { var ydirection = xaxis.Cross(p1 - p0); ydirection.Normalize(); xaxis.Normalize(); int index0 = this.positions.Count; int np = 2 * Vector2s.Count; foreach (var p in Vector2s) { var v = (xaxis * p.x) + (ydirection * p.y); this.positions.Add(p0 + v); this.positions.Add(p1 + v); v.Normalize(); if (this.normals != null) { this.normals.Add(v); this.normals.Add(v); } if (this.textureCoordinates != null) { this.textureCoordinates.Add(new Vector2(0, 0)); this.textureCoordinates.Add(new Vector2(1, 0)); } int i1 = index0 + 1; int i2 = (index0 + 2) % np; int i3 = ((index0 + 2) % np) + 1; this.triangleIndices.Add(i1); this.triangleIndices.Add(i2); this.triangleIndices.Add(index0); this.triangleIndices.Add(i1); this.triangleIndices.Add(i3); this.triangleIndices.Add(i2); } } /// <summary> /// Adds a polygon. /// </summary> /// <param name="Vector2s">The 2D Vector2s defining the polygon.</param> /// <param name="axisX">The x axis.</param> /// <param name="axisY">The y axis.</param> /// <param name="origin">The origin.</param> public void AddPolygon(IList<Vector2> Vector2s, Vector3 axisX, Vector3 axisY, Vector3 origin) { var indices = CuttingEarsTriangulator.Triangulate(Vector2s); var index0 = this.positions.Count; foreach (var p in Vector2s) { this.positions.Add(origin + (axisX * p.x) + (axisY * p.y)); } foreach (var i in indices) { this.triangleIndices.Add(index0 + i); } } /// <summary> /// Adds an extruded surface of the specified line segments. /// </summary> /// <param name="Vector2s">The 2D Vector2s describing the line segments to extrude. The number of Vector2s must be even.</param> /// <param name="axisX">The x-axis.</param> /// <param name="p0">The start origin of the extruded surface.</param> /// <param name="p1">The end origin of the extruded surface.</param> /// <remarks>The y-axis is determined by the cross product between the specified x-axis and the p1-origin vector.</remarks> public void AddExtrudedSegments(IList<Vector2> Vector2s, Vector3 axisX, Vector3 p0, Vector3 p1) { if (Vector2s.Count % 2 != 0) { throw new InvalidOperationException("The number of Vector2s should be even."); } var axisY = axisX.Cross(p1 - p0); axisY.Normalize(); axisX.Normalize(); int index0 = this.positions.Count; for (int i = 0; i < Vector2s.Count; i++) { var p = Vector2s[i]; var d = (axisX * p.x) + (axisY * p.y); this.positions.Add(p0 + d); this.positions.Add(p1 + d); if (this.normals != null) { d.Normalize(); this.normals.Add(d); this.normals.Add(d); } if (this.textureCoordinates != null) { var v = i / (Vector2s.Count - 1d); this.textureCoordinates.Add(new Vector2(0, v)); this.textureCoordinates.Add(new Vector2(1, v)); } } int n = Vector2s.Count - 1; for (int i = 0; i < n; i++) { int i0 = index0 + (i * 2); int i1 = i0 + 1; int i2 = i0 + 3; int i3 = i0 + 2; this.triangleIndices.Add(i0); this.triangleIndices.Add(i1); this.triangleIndices.Add(i2); this.triangleIndices.Add(i2); this.triangleIndices.Add(i3); this.triangleIndices.Add(i0); } } /// <summary> /// Adds a lofted surface. /// </summary> /// <param name="positionsList"> /// List of lofting sections. /// </param> /// <param name="normalList"> /// The normal list. /// </param> /// <param name="textureCoordinateList"> /// The texture coordinate list. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Loft_(3D). /// </remarks> public void AddLoftedGeometry( IList<IList<Vector3>> positionsList, IList<IList<Vector3>> normalList, IList<IList<Vector2>> textureCoordinateList) { int index0 = this.positions.Count; int n = -1; for (int i = 0; i < positionsList.Count; i++) { var pc = positionsList[i]; // check that all curves have same number of Vector2s if (n == -1) { n = pc.Count; } if (pc.Count != n) { throw new InvalidOperationException(AllCurvesShouldHaveTheSameNumberOfVector2s); } // add the Vector2s foreach (var p in pc) { this.positions.Add(p); } // add normals if (this.normals != null && normalList != null) { var nc = normalList[i]; foreach (var normal in nc) { this.normals.Add(normal); } } // add texcoords if (this.textureCoordinates != null && textureCoordinateList != null) { var tc = textureCoordinateList[i]; foreach (var t in tc) { this.textureCoordinates.Add(t); } } } for (int i = 0; i + 1 < positionsList.Count; i++) { for (int j = 0; j + 1 < n; j++) { int i0 = index0 + (i * n) + j; int i1 = i0 + n; int i2 = i1 + 1; int i3 = i0 + 1; this.triangleIndices.Add(i0); this.triangleIndices.Add(i1); this.triangleIndices.Add(i2); this.triangleIndices.Add(i2); this.triangleIndices.Add(i3); this.triangleIndices.Add(i0); } } } /// <summary> /// Adds a single node. /// </summary> /// <param name="position"> /// The position. /// </param> /// <param name="normal"> /// The normal. /// </param> /// <param name="textureCoordinate"> /// The texture coordinate. /// </param> public void AddNode(Vector3 position, Vector3 normal, Vector2 textureCoordinate) { this.positions.Add(position); if (this.normals != null) { this.normals.Add(normal); } if (this.textureCoordinates != null) { this.textureCoordinates.Add(textureCoordinate); } } /// <summary> /// Adds a (possibly hollow) pipe. /// </summary> /// <param name="Vector21"> /// The start Vector2. /// </param> /// <param name="Vector22"> /// The end Vector2. /// </param> /// <param name="innerDiameter"> /// The inner diameter. /// </param> /// <param name="diameter"> /// The outer diameter. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the pipe. /// </param> public void AddPipe(Vector3 Vector21, Vector3 Vector22, double innerDiameter, double diameter, int thetaDiv) { var dir = Vector22 - Vector21; double height = dir.Length; dir.Normalize(); var pc = new List<Vector2> { new Vector2(0, innerDiameter / 2), new Vector2(0, diameter / 2), new Vector2(height, diameter / 2), new Vector2(height, innerDiameter / 2) }; var tc = new List<double> { 1, 0, 1, 0 }; if (innerDiameter > 0) { // Add the inner surface pc.Add(new Vector2(0, innerDiameter / 2)); tc.Add(1); } this.AddRevolvedGeometry(pc, tc, Vector21, dir, thetaDiv); } /// <summary> /// Adds a polygon. /// </summary> /// <param name="Vector2s"> /// The Vector2s of the polygon. /// </param> /// <remarks> /// If the number of Vector2s is greater than 4, a triangle fan is used. /// </remarks> public void AddPolygon(IList<Vector3> Vector2s) { switch (Vector2s.Count) { case 3: this.AddTriangle(Vector2s[0], Vector2s[1], Vector2s[2]); break; case 4: this.AddQuad(Vector2s[0], Vector2s[1], Vector2s[2], Vector2s[3]); break; default: this.AddTriangleFan(Vector2s); break; } } /// <summary> /// Adds a pyramid. /// </summary> /// <param name="center"> /// The center. /// </param> /// <param name="sideLength"> /// Length of the sides of the pyramid. /// </param> /// <param name="height"> /// The height of the pyramid. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Pyramid_(geometry). /// </remarks> public void AddPyramid(Vector3 center, double sideLength, double height) { this.AddPyramid(center, new Vector3(1, 0, 0), new Vector3(0, 0, 1), sideLength, height); } /// <summary> /// Adds a pyramid. /// </summary> /// <param name="center">The center.</param> /// <param name="normal">The normal vector (normalized).</param> /// <param name="up">The 'up' vector (normalized).</param> /// <param name="sideLength">Length of the sides of the pyramid.</param> /// <param name="height">The height of the pyramid.</param> public void AddPyramid(Vector3 center, Vector3 normal, Vector3 up, double sideLength, double height) { var right = normal.Cross(up); var n = normal * sideLength / 2; up *= height; right *= sideLength / 2; var p1 = center - n - right; var p2 = center - n + right; var p3 = center + n + right; var p4 = center + n - right; var p5 = center + up; this.AddTriangle(p1, p2, p5); this.AddTriangle(p2, p3, p5); this.AddTriangle(p3, p4, p5); this.AddTriangle(p4, p1, p5); } /// <summary> /// Adds an octahedron. /// </summary> /// <param name="center">The center.</param> /// <param name="normal">The normal vector.</param> /// <param name="up">The up vector.</param> /// <param name="sideLength">Length of the side.</param> /// <param name="height">The half height of the octahedron.</param> /// <remarks>See <a href="http://en.wikipedia.org/wiki/Octahedron">Octahedron</a>.</remarks> public void AddOctahedron(Vector3 center, Vector3 normal, Vector3 up, double sideLength, double height) { var right = normal.Cross(up); var n = normal * sideLength / 2; up *= height / 2; right *= sideLength / 2; var p1 = center - n - up - right; var p2 = center - n - up + right; var p3 = center + n - up + right; var p4 = center + n - up - right; var p5 = center + up; var p6 = center - up; this.AddTriangle(p1, p2, p5); this.AddTriangle(p2, p3, p5); this.AddTriangle(p3, p4, p5); this.AddTriangle(p4, p1, p5); this.AddTriangle(p2, p1, p6); this.AddTriangle(p3, p2, p6); this.AddTriangle(p4, p3, p6); this.AddTriangle(p1, p4, p6); } /// <summary> /// Adds a quadrilateral polygon. /// </summary> /// <param name="p0"> /// The first Vector2. /// </param> /// <param name="p1"> /// The second Vector2. /// </param> /// <param name="p2"> /// The third Vector2. /// </param> /// <param name="p3"> /// The fourth Vector2. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Quadrilateral. /// </remarks> public void AddQuad(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) { //// The nodes are arranged in counter-clockwise order //// p3 p2 //// +---------------+ //// | | //// | | //// +---------------+ //// origin p1 var uv0 = new Vector2(0, 0); var uv1 = new Vector2(1, 0); var uv2 = new Vector2(1, 1); var uv3 = new Vector2(0, 1); this.AddQuad(p0, p1, p2, p3, uv0, uv1, uv2, uv3); } /// <summary> /// Adds a quadrilateral polygon. /// </summary> /// <param name="p0"> /// The first Vector2. /// </param> /// <param name="p1"> /// The second Vector2. /// </param> /// <param name="p2"> /// The third Vector2. /// </param> /// <param name="p3"> /// The fourth Vector2. /// </param> /// <param name="uv0"> /// The first texture coordinate. /// </param> /// <param name="uv1"> /// The second texture coordinate. /// </param> /// <param name="uv2"> /// The third texture coordinate. /// </param> /// <param name="uv3"> /// The fourth texture coordinate. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Quadrilateral. /// </remarks> public void AddQuad(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector2 uv0, Vector2 uv1, Vector2 uv2, Vector2 uv3) { //// The nodes are arranged in counter-clockwise order //// p3 p2 //// +---------------+ //// | | //// | | //// +---------------+ //// origin p1 int i0 = this.positions.Count; this.positions.Add(p0); this.positions.Add(p1); this.positions.Add(p2); this.positions.Add(p3); if (this.textureCoordinates != null) { this.textureCoordinates.Add(uv0); this.textureCoordinates.Add(uv1); this.textureCoordinates.Add(uv2); this.textureCoordinates.Add(uv3); } if (this.normals != null) { var w = (p3 - p0).Cross(p1 - p0); w.Normalize(); this.normals.Add(w); this.normals.Add(w); this.normals.Add(w); this.normals.Add(w); } this.triangleIndices.Add(i0 + 0); this.triangleIndices.Add(i0 + 1); this.triangleIndices.Add(i0 + 2); this.triangleIndices.Add(i0 + 2); this.triangleIndices.Add(i0 + 3); this.triangleIndices.Add(i0 + 0); } /// <summary> /// Adds a list of quadrilateral polygons. /// </summary> /// <param name="quadPositions"> /// The Vector2s. /// </param> /// <param name="quadNormals"> /// The normal vectors. /// </param> /// <param name="quadTextureCoordinates"> /// The texture coordinates. /// </param> public void AddQuads( IList<Vector3> quadPositions, IList<Vector3> quadNormals, IList<Vector2> quadTextureCoordinates) { if (quadPositions == null) { throw new ArgumentNullException("quadPositions"); } if (this.normals != null && quadNormals == null) { throw new ArgumentNullException("quadNormals"); } if (this.textureCoordinates != null && quadTextureCoordinates == null) { throw new ArgumentNullException("quadTextureCoordinates"); } if (quadNormals != null && quadNormals.Count != quadPositions.Count) { throw new InvalidOperationException(WrongNumberOfNormals); } if (quadTextureCoordinates != null && quadTextureCoordinates.Count != quadPositions.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } Debug.Assert(quadPositions.Count > 0 && quadPositions.Count % 4 == 0, "Wrong number of positions."); int index0 = this.positions.Count; foreach (var p in quadPositions) { this.positions.Add(p); } if (this.textureCoordinates != null && quadTextureCoordinates != null) { foreach (var tc in quadTextureCoordinates) { this.textureCoordinates.Add(tc); } } if (this.normals != null && quadNormals != null) { foreach (var n in quadNormals) { this.normals.Add(n); } } int indexEnd = this.positions.Count; for (int i = index0; i + 3 < indexEnd; i++) { this.triangleIndices.Add(i); this.triangleIndices.Add(i + 1); this.triangleIndices.Add(i + 2); this.triangleIndices.Add(i + 2); this.triangleIndices.Add(i + 3); this.triangleIndices.Add(i); } } /// <summary> /// Adds a rectangular mesh (m x n Vector2s). /// </summary> /// <param name="Vector2s"> /// The one-dimensional array of Vector2s. The Vector2s are stored row-by-row. /// </param> /// <param name="columns"> /// The number of columns in the rectangular mesh. /// </param> public void AddRectangularMesh(IList<Vector3> Vector2s, int columns) { if (Vector2s == null) { throw new ArgumentNullException("Vector2s"); } int index0 = this.Positions.Count; foreach (var pt in Vector2s) { this.positions.Add(pt); } int rows = Vector2s.Count / columns; this.AddRectangularMeshTriangleIndices(index0, rows, columns); if (this.normals != null) { this.AddRectangularMeshNormals(index0, rows, columns); } if (this.textureCoordinates != null) { this.AddRectangularMeshTextureCoordinates(rows, columns); } } /// <summary> /// Adds a rectangular mesh defined by a two-dimensional array of Vector2s. /// </summary> /// <param name="Vector2s"> /// The Vector2s. /// </param> /// <param name="texCoords"> /// The texture coordinates (optional). /// </param> /// <param name="closed0"> /// set to <c>true</c> if the mesh is closed in the first dimension. /// </param> /// <param name="closed1"> /// set to <c>true</c> if the mesh is closed in the second dimension. /// </param> public void AddRectangularMesh( Vector3[,] Vector2s, Vector2[,] texCoords = null, bool closed0 = false, bool closed1 = false) { if (Vector2s == null) { throw new ArgumentNullException("Vector2s"); } int rows = Vector2s.GetUpperBound(0) + 1; int columns = Vector2s.GetUpperBound(1) + 1; int index0 = this.positions.Count; for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { this.positions.Add(Vector2s[i, j]); } } this.AddRectangularMeshTriangleIndices(index0, rows, columns, closed0, closed1); if (this.normals != null) { this.AddRectangularMeshNormals(index0, rows, columns); } if (this.textureCoordinates != null) { if (texCoords != null) { for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { this.textureCoordinates.Add(texCoords[i, j]); } } } else { this.AddRectangularMeshTextureCoordinates(rows, columns); } } } /// <summary> /// Adds a regular icosahedron. /// </summary> /// <param name="center"> /// The center. /// </param> /// <param name="radius"> /// The radius. /// </param> /// <param name="shareVertices"> /// Share vertices if set to <c>true</c> . /// </param> /// <remarks> /// See <a href="http://en.wikipedia.org/wiki/Icosahedron">Wikipedia</a> and <a href="http://www.gamedev.net/community/forums/topic.asp?topic_id=283350">link</a>. /// </remarks> public void AddRegularIcosahedron(Vector3 center, double radius, bool shareVertices) { double a = Math.Sqrt(2.0 / (5.0 + Math.Sqrt(5.0))); double b = Math.Sqrt(2.0 / (5.0 - Math.Sqrt(5.0))); var icosahedronIndices = new[] { 1, 4, 0, 4, 9, 0, 4, 5, 9, 8, 5, 4, 1, 8, 4, 1, 10, 8, 10, 3, 8, 8, 3, 5, 3, 2, 5, 3, 7, 2, 3, 10, 7, 10, 6, 7, 6, 11, 7, 6, 0, 11, 6, 1, 0, 10, 1, 6, 11, 0, 9, 2, 11, 9, 5, 2, 9, 11, 2, 7 }; var icosahedronVertices = new[] { new Vector3(-a, 0, b), new Vector3(a, 0, b), new Vector3(-a, 0, -b), new Vector3(a, 0, -b), new Vector3(0, b, a), new Vector3(0, b, -a), new Vector3(0, -b, a), new Vector3(0, -b, -a), new Vector3(b, a, 0), new Vector3(-b, a, 0), new Vector3(b, -a, 0), new Vector3(-b, -a, 0) }; if (shareVertices) { int index0 = this.positions.Count; foreach (var v in icosahedronVertices) { this.positions.Add(center + (v * radius)); } foreach (int i in icosahedronIndices) { this.triangleIndices.Add(index0 + i); } } else { for (int i = 0; i + 2 < icosahedronIndices.Length; i += 3) { this.AddTriangle( center + (icosahedronVertices[icosahedronIndices[i]] * radius), center + (icosahedronVertices[icosahedronIndices[i + 1]] * radius), center + (icosahedronVertices[icosahedronIndices[i + 2]] * radius)); } } } /// <summary> /// Adds a surface of revolution. /// </summary> /// <param name="origin">The origin.</param> /// <param name="axis">The axis.</param> /// <param name="section">The Vector2s defining the curve to revolve.</param> /// <param name="sectionIndices">The indices of the line segments of the section.</param> /// <param name="thetaDiv">The number of divisions.</param> /// <param name="textureValues">The texture values.</param> public void AddSurfaceOfRevolution( Vector3 origin, Vector3 axis, IList<Vector2> section, IList<int> sectionIndices, int thetaDiv = 37, IList<double> textureValues = null) { if (this.textureCoordinates != null && textureValues == null) { throw new ArgumentNullException("textureValues"); } if (textureValues != null && textureValues.Count != section.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } axis.Normalize(); // Find two unit vectors orthogonal to the specified direction var u = axis.FindAnyPerpendicular(); var v = axis.Cross(u); var circle = GetCircle(thetaDiv); int n = section.Count; int index0 = this.positions.Count; for (int i = 0; i < thetaDiv; i++) { var w = (v * circle[i].x) + (u * circle[i].y); for (int j = 0; j < n; j++) { var q1 = origin + (axis * section[j].y) + (w * section[j].x); this.positions.Add(q1); if (this.normals != null) { double tx = section[j + 1].x - section[j].x; double ty = section[j + 1].y - section[j].y; var normal = (-axis * ty) + (w * tx); normal.Normalize(); this.normals.Add(normal); } if (this.textureCoordinates != null) { this.textureCoordinates.Add(new Vector2((double)i / (thetaDiv - 1), textureValues == null ? (double)j / (n - 1) : textureValues[j])); } } } for (int i = 0; i < thetaDiv; i++) { var ii = (i + 1) % thetaDiv; for (int j = 0; j + 1 < sectionIndices.Count; j += 2) { var j0 = sectionIndices[j]; var j1 = sectionIndices[j + 1]; int i0 = index0 + (i * n) + j0; int i1 = index0 + (ii * n) + j0; int i2 = index0 + (i * n) + j1; int i3 = index0 + (ii * n) + j1; this.triangleIndices.Add(i0); this.triangleIndices.Add(i1); this.triangleIndices.Add(i3); this.triangleIndices.Add(i3); this.triangleIndices.Add(i2); this.triangleIndices.Add(i0); } } } /// <summary> /// Adds a surface of revolution. /// </summary> /// <param name="Vector2s">The Vector2s (x coordinates are distance from the origin along the axis of revolution, y coordinates are radius, )</param> /// <param name="textureValues">The v texture coordinates, one for each Vector2 in the <paramref name="Vector2s" /> list.</param> /// <param name="origin">The origin of the revolution axis.</param> /// <param name="direction">The direction of the revolution axis.</param> /// <param name="thetaDiv">The number of divisions around the mesh.</param> /// <remarks> /// See http://en.wikipedia.org/wiki/Surface_of_revolution. /// </remarks> public void AddRevolvedGeometry(IList<Vector2> Vector2s, IList<double> textureValues, Vector3 origin, Vector3 direction, int thetaDiv) { direction.Normalize(); // Find two unit vectors orthogonal to the specified direction var u = direction.FindAnyPerpendicular(); var v = direction.Cross(u); u.Normalize(); v.Normalize(); var circle = GetCircle(thetaDiv); int index0 = this.positions.Count; int n = Vector2s.Count; int totalNodes = (Vector2s.Count - 1) * 2 * thetaDiv; int rowNodes = (Vector2s.Count - 1) * 2; for (int i = 0; i < thetaDiv; i++) { var w = (v * circle[i].x) + (u * circle[i].y); for (int j = 0; j + 1 < n; j++) { // Add segment var q1 = origin + (direction * Vector2s[j].x) + (w * Vector2s[j].y); var q2 = origin + (direction * Vector2s[j + 1].x) + (w * Vector2s[j + 1].y); // TODO: should not add segment if q1==q2 (corner Vector2) // const double eps = 1e-6; // if (Vector3.Subtract(q1, q2).LengthSquared < eps) // continue; this.positions.Add(q1); this.positions.Add(q2); if (this.normals != null) { double tx = Vector2s[j + 1].x - Vector2s[j].x; double ty = Vector2s[j + 1].y - Vector2s[j].y; var normal = (-direction * ty) + (w * tx); normal.Normalize(); this.normals.Add(normal); this.normals.Add(normal); } if (this.textureCoordinates != null) { this.textureCoordinates.Add(new Vector2((double)i / (thetaDiv - 1), textureValues == null ? (double)j / (n - 1) : textureValues[j])); this.textureCoordinates.Add(new Vector2((double)i / (thetaDiv - 1), textureValues == null ? (double)(j + 1) / (n - 1) : textureValues[j + 1])); } int i0 = index0 + (i * rowNodes) + (j * 2); int i1 = i0 + 1; int i2 = index0 + ((((i + 1) * rowNodes) + (j * 2)) % totalNodes); int i3 = i2 + 1; this.triangleIndices.Add(i1); this.triangleIndices.Add(i0); this.triangleIndices.Add(i2); this.triangleIndices.Add(i1); this.triangleIndices.Add(i2); this.triangleIndices.Add(i3); } } } /// <summary> /// Adds a sphere (by subdividing a regular icosahedron). /// </summary> /// <param name="center"> /// The center of the sphere. /// </param> /// <param name="radius"> /// The radius of the sphere. /// </param> /// <param name="subdivisions"> /// The number of triangular subdivisions of the original icosahedron. /// </param> /// <remarks> /// See <a href="http://www.fho-emden.de/~hoffmann/ikos27042002.pdf">link</a>. /// </remarks> public void AddSubdivisionSphere(Vector3 center, double radius, int subdivisions) { int p0 = this.positions.Count; this.Append(GetUnitSphere(subdivisions)); int p1 = this.positions.Count; for (int i = p0; i < p1; i++) { this.positions[i] = center + (radius * this.positions[i]); } } /// <summary> /// Adds a sphere. /// </summary> /// <param name="center"> /// The center of the sphere. /// </param> /// <param name="radius"> /// The radius of the sphere. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the sphere. /// </param> /// <param name="phiDiv"> /// The number of divisions from top to bottom of the sphere. /// </param> public void AddSphere(Vector3 center, double radius, int thetaDiv = 20, int phiDiv = 10) { this.AddEllipsoid(center, radius, radius, radius, thetaDiv, phiDiv); } /// <summary> /// Adds an ellipsoid. /// </summary> /// <param name="center"> /// The center of the ellipsoid. /// </param> /// <param name="radiusx"> /// The x radius of the ellipsoid. /// </param> /// <param name="radiusy"> /// The y radius of the ellipsoid. /// </param> /// <param name="radiusz"> /// The z radius of the ellipsoid. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the ellipsoid. /// </param> /// <param name="phiDiv"> /// The number of divisions from top to bottom of the ellipsoid. /// </param> public void AddEllipsoid(Vector3 center, double radiusx, double radiusy, double radiusz, int thetaDiv = 20, int phiDiv = 10) { int index0 = this.Positions.Count; double dt = 2 * Math.PI / thetaDiv; double dp = Math.PI / phiDiv; for (int pi = 0; pi <= phiDiv; pi++) { double phi = pi * dp; for (int ti = 0; ti <= thetaDiv; ti++) { // we want to start the mesh on the x axis double theta = ti * dt; // Spherical coordinates // http://mathworld.wolfram.com/SphericalCoordinates.html double x = Math.Cos(theta) * Math.Sin(phi); double y = Math.Sin(theta) * Math.Sin(phi); double z = Math.Cos(phi); var p = new Vector3(center.x + (radiusx * x), center.y + (radiusy * y), center.z + (radiusz * z)); this.positions.Add(p); if (this.normals != null) { var n = new Vector3(x, y, z); this.normals.Add(n); } if (this.textureCoordinates != null) { var uv = new Vector2(theta / (2 * Math.PI), phi / Math.PI); this.textureCoordinates.Add(uv); } } } this.AddRectangularMeshTriangleIndices(index0, phiDiv + 1, thetaDiv + 1, true); } /// <summary> /// Adds a triangle. /// </summary> /// <param name="p0"> /// The first Vector2. /// </param> /// <param name="p1"> /// The second Vector2. /// </param> /// <param name="p2"> /// The third Vector2. /// </param> public void AddTriangle(Vector3 p0, Vector3 p1, Vector3 p2) { var uv0 = new Vector2(0, 0); var uv1 = new Vector2(1, 0); var uv2 = new Vector2(0, 1); this.AddTriangle(p0, p1, p2, uv0, uv1, uv2); } /// <summary> /// Adds a triangle. /// </summary> /// <param name="p0"> /// The first Vector2. /// </param> /// <param name="p1"> /// The second Vector2. /// </param> /// <param name="p2"> /// The third Vector2. /// </param> /// <param name="uv0"> /// The first texture coordinate. /// </param> /// <param name="uv1"> /// The second texture coordinate. /// </param> /// <param name="uv2"> /// The third texture coordinate. /// </param> public void AddTriangle(Vector3 p0, Vector3 p1, Vector3 p2, Vector2 uv0, Vector2 uv1, Vector2 uv2) { int i0 = this.positions.Count; this.positions.Add(p0); this.positions.Add(p1); this.positions.Add(p2); if (this.textureCoordinates != null) { this.textureCoordinates.Add(uv0); this.textureCoordinates.Add(uv1); this.textureCoordinates.Add(uv2); } if (this.normals != null) { var w = (p1 - p0).Cross(p2 - p0); w.Normalize(); this.normals.Add(w); this.normals.Add(w); this.normals.Add(w); } this.triangleIndices.Add(i0 + 0); this.triangleIndices.Add(i0 + 1); this.triangleIndices.Add(i0 + 2); } /// <summary> /// Adds a triangle fan. /// </summary> /// <param name="vertices"> /// The vertex indices of the triangle fan. /// </param> public void AddTriangleFan(IList<int> vertices) { for (int i = 0; i + 2 < vertices.Count; i++) { this.triangleIndices.Add(vertices[0]); this.triangleIndices.Add(vertices[i + 1]); this.triangleIndices.Add(vertices[i + 2]); } } /// <summary> /// Adds a triangle fan to the mesh /// </summary> /// <param name="fanPositions"> /// The Vector2s of the triangle fan. /// </param> /// <param name="fanNormals"> /// The normal vectors of the triangle fan. /// </param> /// <param name="fanTextureCoordinates"> /// The texture coordinates of the triangle fan. /// </param> public void AddTriangleFan( IList<Vector3> fanPositions, IList<Vector3> fanNormals = null, IList<Vector2> fanTextureCoordinates = null) { if (this.positions == null) { throw new ArgumentNullException("fanPositions"); } if (this.normals != null && this.normals == null) { throw new ArgumentNullException("fanNormals"); } if (this.textureCoordinates != null && this.textureCoordinates == null) { throw new ArgumentNullException("fanTextureCoordinates"); } int index0 = this.positions.Count; foreach (var p in fanPositions) { this.positions.Add(p); } if (this.textureCoordinates != null && fanTextureCoordinates != null) { foreach (var tc in fanTextureCoordinates) { this.textureCoordinates.Add(tc); } } if (this.normals != null && fanNormals != null) { foreach (var n in fanNormals) { this.normals.Add(n); } } int indexEnd = this.positions.Count; for (int i = index0; i + 2 < indexEnd; i++) { this.triangleIndices.Add(index0); this.triangleIndices.Add(i + 1); this.triangleIndices.Add(i + 2); } } /// <summary> /// Adds a triangle strip to the mesh. /// </summary> /// <param name="stripPositions"> /// The Vector2s of the triangle strip. /// </param> /// <param name="stripNormals"> /// The normal vectors of the triangle strip. /// </param> /// <param name="stripTextureCoordinates"> /// The texture coordinates of the triangle strip. /// </param> /// <remarks> /// See http://en.wikipedia.org/wiki/Triangle_strip. /// </remarks> public void AddTriangleStrip( IList<Vector3> stripPositions, IList<Vector3> stripNormals = null, IList<Vector2> stripTextureCoordinates = null) { if (stripPositions == null) { throw new ArgumentNullException("stripPositions"); } if (this.normals != null && stripNormals == null) { throw new ArgumentNullException("stripNormals"); } if (this.textureCoordinates != null && stripTextureCoordinates == null) { throw new ArgumentNullException("stripTextureCoordinates"); } if (stripNormals != null && stripNormals.Count != stripPositions.Count) { throw new InvalidOperationException(WrongNumberOfNormals); } if (stripTextureCoordinates != null && stripTextureCoordinates.Count != stripPositions.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } int index0 = this.positions.Count; for (int i = 0; i < stripPositions.Count; i++) { this.positions.Add(stripPositions[i]); if (this.normals != null && stripNormals != null) { this.normals.Add(stripNormals[i]); } if (this.textureCoordinates != null && stripTextureCoordinates != null) { this.textureCoordinates.Add(stripTextureCoordinates[i]); } } int indexEnd = this.positions.Count; for (int i = index0; i + 2 < indexEnd; i += 2) { this.triangleIndices.Add(i); this.triangleIndices.Add(i + 1); this.triangleIndices.Add(i + 2); if (i + 3 < indexEnd) { this.triangleIndices.Add(i + 1); this.triangleIndices.Add(i + 3); this.triangleIndices.Add(i + 2); } } } /// <summary> /// Adds a polygon specified by vertex index (uses a triangle fan). /// </summary> /// <param name="vertexIndices">The vertex indices.</param> public void AddPolygon(IList<int> vertexIndices) { int n = vertexIndices.Count; for (int i = 0; i + 2 < n; i++) { this.triangleIndices.Add(vertexIndices[0]); this.triangleIndices.Add(vertexIndices[i + 1]); this.triangleIndices.Add(vertexIndices[i + 2]); } } /// <summary> /// Adds a polygon defined by vertex indices (uses the cutting ears algorithm). /// </summary> /// <param name="vertexIndices">The vertex indices.</param> public void AddPolygonByCuttingEars(IList<int> vertexIndices) { var Vector2s = vertexIndices.Select(vi => this.positions[vi]).ToList(); var poly3D = new Polygon3D(Vector2s); // Transform the polygon to 2D var poly2D = poly3D.Flatten(); // Triangulate var triangulatedIndices = poly2D.Triangulate(); if (triangulatedIndices != null) { foreach (var i in triangulatedIndices) { this.triangleIndices.Add(vertexIndices[i]); } } } /// <summary> /// Adds a list of triangles. /// </summary> /// <param name="trianglePositions"> /// The Vector2s (the number of Vector2s must be a multiple of 3). /// </param> /// <param name="triangleNormals"> /// The normal vectors (corresponding to the Vector2s). /// </param> /// <param name="triangleTextureCoordinates"> /// The texture coordinates (corresponding to the Vector2s). /// </param> public void AddTriangles( IList<Vector3> trianglePositions, IList<Vector3> triangleNormals = null, IList<Vector2> triangleTextureCoordinates = null) { if (trianglePositions == null) { throw new ArgumentNullException("trianglePositions"); } if (this.normals != null && triangleNormals == null) { throw new ArgumentNullException("triangleNormals"); } if (this.textureCoordinates != null && triangleTextureCoordinates == null) { throw new ArgumentNullException("triangleTextureCoordinates"); } if (trianglePositions.Count % 3 != 0) { throw new InvalidOperationException(WrongNumberOfPositions); } if (triangleNormals != null && triangleNormals.Count != trianglePositions.Count) { throw new InvalidOperationException(WrongNumberOfNormals); } if (triangleTextureCoordinates != null && triangleTextureCoordinates.Count != trianglePositions.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } int index0 = this.positions.Count; foreach (var p in trianglePositions) { this.positions.Add(p); } if (this.textureCoordinates != null && triangleTextureCoordinates != null) { foreach (var tc in triangleTextureCoordinates) { this.textureCoordinates.Add(tc); } } if (this.normals != null && triangleNormals != null) { foreach (var n in triangleNormals) { this.normals.Add(n); } } int indexEnd = this.positions.Count; for (int i = index0; i < indexEnd; i++) { this.triangleIndices.Add(i); } } /// <summary> /// Adds a tube. /// </summary> /// <param name="path"> /// A list of Vector2s defining the centers of the tube. /// </param> /// <param name="values"> /// The texture coordinate X-values (optional). /// </param> /// <param name="diameters"> /// The diameters (optional). /// </param> /// <param name="thetaDiv"> /// The number of divisions around the tube. /// </param> /// <param name="isTubeClosed"> /// Set to true if the tube path is closed. /// </param> public void AddTube(IList<Vector3> path, double[] values, double[] diameters, int thetaDiv, bool isTubeClosed) { var circle = GetCircle(thetaDiv); this.AddTube(path, values, diameters, circle, isTubeClosed, true); } /// <summary> /// Adds a tube. /// </summary> /// <param name="path"> /// A list of Vector2s defining the centers of the tube. /// </param> /// <param name="diameter"> /// The diameter of the tube. /// </param> /// <param name="thetaDiv"> /// The number of divisions around the tube. /// </param> /// <param name="isTubeClosed"> /// Set to true if the tube path is closed. /// </param> public void AddTube(IList<Vector3> path, double diameter, int thetaDiv, bool isTubeClosed) { this.AddTube(path, null, new[] { diameter }, thetaDiv, isTubeClosed); } /// <summary> /// Adds a tube with a custom section. /// </summary> /// <param name="path"> /// A list of Vector2s defining the centers of the tube. /// </param> /// <param name="values"> /// The texture coordinate X values (optional). /// </param> /// <param name="diameters"> /// The diameters (optional). /// </param> /// <param name="section"> /// The section to extrude along the tube path. /// </param> /// <param name="isTubeClosed"> /// If the tube is closed set to <c>true</c> . /// </param> /// <param name="isSectionClosed"> /// if set to <c>true</c> [is section closed]. /// </param> public void AddTube( IList<Vector3> path, IList<double> values, IList<double> diameters, IList<Vector2> section, bool isTubeClosed, bool isSectionClosed) { if (values != null && values.Count == 0) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } if (diameters != null && diameters.Count == 0) { throw new InvalidOperationException(WrongNumberOfDiameters); } int index0 = this.positions.Count; int pathLength = path.Count; int sectionLength = section.Count; if (pathLength < 2 || sectionLength < 2) { return; } var up = (path[1] - path[0]).FindAnyPerpendicular(); int diametersCount = diameters != null ? diameters.Count : 0; int valuesCount = values != null ? values.Count : 0; //******************************* //*** PROPOSED SOLUTION ********* var lastUp = new Vector3(); var lastForward = new Vector3(); //*** PROPOSED SOLUTION ********* //******************************* for (int i = 0; i < pathLength; i++) { double r = diameters != null ? diameters[i % diametersCount] / 2 : 1; int i0 = i > 0 ? i - 1 : i; int i1 = i + 1 < pathLength ? i + 1 : i; var forward = path[i1] - path[i0]; var right = up.Cross(forward); up = forward.Cross(right); up.Normalize(); right.Normalize(); var u = right; var v = up; //******************************* //*** PROPOSED SOLUTION ********* // ** I think this will work because if path[n-1] is same Vector2, // ** it is always a reflection of the current move // ** so reversing the last move vector should work? //******************************* if (u.IsInvalid || v.IsInvalid) { forward = lastForward; forward = Vector3.Negate(forward); up = lastUp; //** Please verify that negation of "up" is correct here up = Vector3.Negate(up); right = up.Cross(forward); up.Normalize(); right.Normalize(); u = right; v = up; } lastForward = forward; lastUp = up; //*** PROPOSED SOLUTION ********* //******************************* for (int j = 0; j < sectionLength; j++) { var w = (section[j].x * u * r) + (section[j].y * v * r); var q = path[i] + w; this.positions.Add(q); if (this.normals != null) { w.Normalize(); this.normals.Add(w); } if (this.textureCoordinates != null) { this.textureCoordinates.Add( values != null ? new Vector2(values[i % valuesCount], (double)j / (sectionLength - 1)) : new Vector2()); } } } this.AddRectangularMeshTriangleIndices(index0, pathLength, sectionLength, isSectionClosed, isTubeClosed); } /// <summary> /// Adds a tube with a custom section. /// </summary> /// <param name="path">A list of Vector2s defining the centers of the tube.</param> /// <param name="angles">The rotation of the section as it moves along the path</param> /// <param name="values">The texture coordinate X values (optional).</param> /// <param name="diameters">The diameters (optional).</param> /// <param name="section">The section to extrude along the tube path.</param> /// <param name="sectionXAxis">The initial alignment of the x-axis of the section into the /// 3D viewport</param> /// <param name="isTubeClosed">If the tube is closed set to <c>true</c> .</param> /// <param name="isSectionClosed">if set to <c>true</c> [is section closed].</param> public void AddTube( IList<Vector3> path, IList<double> angles, IList<double> values, IList<double> diameters, IList<Vector2> section, Vector3 sectionXAxis, bool isTubeClosed, bool isSectionClosed) { if (values != null && values.Count == 0) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } if (diameters != null && diameters.Count == 0) { throw new InvalidOperationException(WrongNumberOfDiameters); } if (angles != null && angles.Count == 0) { throw new InvalidOperationException(WrongNumberOfAngles); } int index0 = this.positions.Count; int pathLength = path.Count; int sectionLength = section.Count; if (pathLength < 2 || sectionLength < 2) { return; } var forward = path[1] - path[0]; var right = sectionXAxis; var up = forward.Cross(right); up.Normalize(); right.Normalize(); int diametersCount = diameters != null ? diameters.Count : 0; int valuesCount = values != null ? values.Count : 0; int anglesCount = angles != null ? angles.Count : 0; for (int i = 0; i < pathLength; i++) { double radius = diameters != null ? diameters[i % diametersCount] / 2 : 1; double theta = angles != null ? angles[i % anglesCount] : 0.0; double ct = Math.Cos(theta); double st = Math.Sin(theta); int i0 = i > 0 ? i - 1 : i; int i1 = i + 1 < pathLength ? i + 1 : i; forward = path[i1] - path[i0]; right = up.Cross(forward); if (right.LengthSquared > 1e-6) { up = forward.Cross(right); } up.Normalize(); right.Normalize(); for (int j = 0; j < sectionLength; j++) { var x = (section[j].x * ct) - (section[j].y * st); var y = (section[j].x * st) + (section[j].y * ct); var w = (x * right * radius) + (y * up * radius); var q = path[i] + w; this.positions.Add(q); if (this.normals != null) { w.Normalize(); this.normals.Add(w); } if (this.textureCoordinates != null) { this.textureCoordinates.Add( values != null ? new Vector2(values[i % valuesCount], (double)j / (sectionLength - 1)) : new Vector2()); } } } this.AddRectangularMeshTriangleIndices(index0, pathLength, sectionLength, isSectionClosed, isTubeClosed); } /// <summary> /// Appends the specified mesh. /// </summary> /// <param name="mesh"> /// The mesh. /// </param> public void Append(MeshBuilder mesh) { if (mesh == null) { throw new ArgumentNullException("mesh"); } this.Append(mesh.positions, mesh.triangleIndices, mesh.normals, mesh.textureCoordinates); } /// <summary> /// Appends the specified mesh. /// </summary> /// <param name="mesh"> /// The mesh. /// </param> public void Append(MeshGeometry3D mesh) { if (mesh == null) { throw new ArgumentNullException("mesh"); } this.Append(mesh.Positions, mesh.TriangleIndices, this.normals != null ? mesh.Normals : null, this.textureCoordinates != null ? mesh.TextureCoordinates : null); } /// <summary> /// Appends the specified Vector2s and triangles. /// </summary> /// <param name="positionsToAppend"> /// The Vector2s to append. /// </param> /// <param name="triangleIndicesToAppend"> /// The triangle indices to append. /// </param> /// <param name="normalsToAppend"> /// The normal vectors to append. /// </param> /// <param name="textureCoordinatesToAppend"> /// The texture coordinates to append. /// </param> public void Append( IList<Vector3> positionsToAppend, IList<int> triangleIndicesToAppend, IList<Vector3> normalsToAppend = null, IList<Vector2> textureCoordinatesToAppend = null) { if (positionsToAppend == null) { throw new ArgumentNullException("positionsToAppend"); } if (this.normals != null && normalsToAppend == null) { throw new InvalidOperationException(SourceMeshNormalsShouldNotBeNull); } if (this.textureCoordinates != null && textureCoordinatesToAppend == null) { throw new InvalidOperationException(SourceMeshTextureCoordinatesShouldNotBeNull); } if (normalsToAppend != null && normalsToAppend.Count != positionsToAppend.Count) { throw new InvalidOperationException(WrongNumberOfNormals); } if (textureCoordinatesToAppend != null && textureCoordinatesToAppend.Count != positionsToAppend.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } int index0 = this.positions.Count; foreach (var p in positionsToAppend) { this.positions.Add(p); } if (this.normals != null && normalsToAppend != null) { foreach (var n in normalsToAppend) { this.normals.Add(n); } } if (this.textureCoordinates != null && textureCoordinatesToAppend != null) { foreach (var t in textureCoordinatesToAppend) { this.textureCoordinates.Add(t); } } foreach (int i in triangleIndicesToAppend) { this.triangleIndices.Add(index0 + i); } } /// <summary> /// Chamfers the specified corner (experimental code). /// </summary> /// <param name="p"> /// The corner Vector2. /// </param> /// <param name="d"> /// The chamfer distance. /// </param> /// <param name="eps"> /// The corner search limit distance. /// </param> /// <param name="chamferVector2s"> /// If this parameter is provided, the collection will be filled with the generated chamfer Vector2s. /// </param> public void ChamferCorner(Vector3 p, double d, double eps = 1e-6, IList<Vector3> chamferVector2s = null) { this.NoSharedVertices(); this.normals = null; this.textureCoordinates = null; var cornerNormal = this.FindCornerNormal(p, eps); var newCornerVector2 = p - (cornerNormal * d); int index0 = this.positions.Count; this.positions.Add(newCornerVector2); var plane = new Plane3D(newCornerVector2, cornerNormal); int ntri = this.triangleIndices.Count; for (int i = 0; i < ntri; i += 3) { int i0 = i; int i1 = i + 1; int i2 = i + 2; var p0 = this.positions[this.triangleIndices[i0]]; var p1 = this.positions[this.triangleIndices[i1]]; var p2 = this.positions[this.triangleIndices[i2]]; double d0 = (p - p0).LengthSquared; double d1 = (p - p1).LengthSquared; double d2 = (p - p2).LengthSquared; double mind = Math.Min(d0, Math.Min(d1, d2)); if (mind > eps) { continue; } if (d1 < eps) { i0 = i + 1; i1 = i + 2; i2 = i; } if (d2 < eps) { i0 = i + 2; i1 = i; i2 = i + 1; } p0 = this.positions[this.triangleIndices[i0]]; p1 = this.positions[this.triangleIndices[i1]]; p2 = this.positions[this.triangleIndices[i2]]; // origin is the corner vertex (at index i0) // find the intersections between the chamfer plane and the two edges connected to the corner var p01 = plane.LineIntersection(p0, p1); var p02 = plane.LineIntersection(p0, p2); if (p01 == null) { continue; } if (p02 == null) { continue; } if (chamferVector2s != null) { // add the chamfered Vector2s if (!chamferVector2s.Contains(p01.Value)) { chamferVector2s.Add(p01.Value); } if (!chamferVector2s.Contains(p02.Value)) { chamferVector2s.Add(p02.Value); } } int i01 = i0; // change the original triangle to use the first chamfer Vector2 this.positions[this.triangleIndices[i01]] = p01.Value; int i02 = this.positions.Count; this.positions.Add(p02.Value); // add a new triangle for the other chamfer Vector2 this.triangleIndices.Add(i01); this.triangleIndices.Add(i2); this.triangleIndices.Add(i02); // add a triangle connecting the chamfer Vector2s and the new corner Vector2 this.triangleIndices.Add(index0); this.triangleIndices.Add(i01); this.triangleIndices.Add(i02); } this.NoSharedVertices(); } /// <summary> /// Checks the performance limits. /// </summary> /// <remarks> /// See <a href="http://msdn.microsoft.com/en-us/library/bb613553.aspx">MSDN</a>. /// Try to keep mesh sizes under these limits: /// Positions : 20,001 Vector2 instances /// TriangleIndices : 60,003 integer instances /// </remarks> public void CheckPerformanceLimits() { if (this.positions.Count > 20000) { LogManager.Instance.Write(string.Format("Too many positions ({0}).", this.positions.Count)); } if (this.triangleIndices.Count > 60002) { LogManager.Instance.Write(string.Format("Too many triangle indices ({0}).", this.triangleIndices.Count)); } } /// <summary> /// Scales the positions (and normal vectors). /// </summary> /// <param name="scaleX"> /// The X scale factor. /// </param> /// <param name="scaleY"> /// The Y scale factor. /// </param> /// <param name="scaleZ"> /// The Z scale factor. /// </param> public void Scale(double scaleX, double scaleY, double scaleZ) { for (int i = 0; i < this.Positions.Count; i++) { this.Positions[i] = new Vector3( this.Positions[i].x * scaleX, this.Positions[i].y * scaleY, this.Positions[i].z * scaleZ); } if (this.Normals != null) { for (int i = 0; i < this.Normals.Count; i++) { this.Normals[i] = new Vector3( this.Normals[i].x * scaleX, this.Normals[i].y * scaleY, this.Normals[i].z * scaleZ); this.Normals[i].Normalize(); } } } /// <summary> /// Performs a linear subdivision of the mesh. /// </summary> /// <param name="barycentric"> /// Add a vertex in the center if set to <c>true</c> . /// </param> public void SubdivideLinear(bool barycentric = false) { if (barycentric) { this.SubdivideBarycentric(); } else { this.Subdivide4(); } } /// <summary> /// Converts the geometry to a <see cref="MeshGeometry3D"/> . /// </summary> /// <param name="freeze"> /// freeze the mesh if set to <c>true</c> . /// </param> /// <returns> /// A mesh geometry. /// </returns> public MeshGeometry3D ToMesh(bool freeze = false) { if (this.triangleIndices.Count == 0) { var emptyGeometry = new MeshGeometry3D(); return emptyGeometry; } if (this.normals != null && this.positions.Count != this.normals.Count) { throw new InvalidOperationException(WrongNumberOfNormals); } if (this.textureCoordinates != null && this.positions.Count != this.textureCoordinates.Count) { throw new InvalidOperationException(WrongNumberOfTextureCoordinates); } var mg = new MeshGeometry3D { Positions = new List<Vector3>(this.positions), TriangleIndices = new List<int>(this.triangleIndices) }; if (this.normals != null) { mg.Normals = new List<Vector3>(this.normals); } if (this.textureCoordinates != null) { mg.TextureCoordinates = new List<Vector2>(this.textureCoordinates); } return mg; } public Mesh ToMesh(string name, string materialName = "") { MeshGeometry3D g3d = ToMesh(); var mesh = g3d.ToMesh(name, materialName); return mesh; } public void ToManualObjectSection(ManualObject mo, string material) { MeshGeometry3D g3d = ToMesh(); mo.Begin(material, OperationType.TriangleList); foreach (var pos in g3d.Positions) { mo.Position(pos); } foreach (var tri in g3d.TriangleIndices) { mo.Index((ushort)tri); } mo.End(); } /// <summary> /// Gets a unit sphere from the cache. /// </summary> /// <param name="subdivisions"> /// The number of subdivisions. /// </param> /// <returns> /// A unit sphere mesh. /// </returns> private static MeshGeometry3D GetUnitSphere(int subdivisions) { if (UnitSphereCache.Value.ContainsKey(subdivisions)) { return UnitSphereCache.Value[subdivisions]; } var mb = new MeshBuilder(false, false); mb.AddRegularIcosahedron(new Vector3(), 1, false); for (int i = 0; i < subdivisions; i++) { mb.SubdivideLinear(); } for (int i = 0; i < mb.positions.Count; i++) { var v = mb.Positions[i]; v.Normalize(); mb.Positions[i] = v; } var mesh = mb.ToMesh(); UnitSphereCache.Value[subdivisions] = mesh; return mesh; } /// <summary> /// Adds normal vectors for a rectangular mesh. /// </summary> /// <param name="index0"> /// The index 0. /// </param> /// <param name="rows"> /// The number of rows. /// </param> /// <param name="columns"> /// The number of columns. /// </param> private void AddRectangularMeshNormals(int index0, int rows, int columns) { for (int i = 0; i < rows; i++) { int i1 = i + 1; if (i1 == rows) { i1--; } int i0 = i1 - 1; for (int j = 0; j < columns; j++) { int j1 = j + 1; if (j1 == columns) { j1--; } int j0 = j1 - 1; var u = Vector3.Subtract( this.positions[index0 + (i1 * columns) + j0], this.positions[index0 + (i0 * columns) + j0]); var v = Vector3.Subtract( this.positions[index0 + (i0 * columns) + j1], this.positions[index0 + (i0 * columns) + j0]); var normal = u.Cross(v); normal.Normalize(); this.normals.Add(normal); } } } /// <summary> /// Adds texture coordinates for a rectangular mesh. /// </summary> /// <param name="rows"> /// The number of rows. /// </param> /// <param name="columns"> /// The number of columns. /// </param> private void AddRectangularMeshTextureCoordinates(int rows, int columns) { for (int i = 0; i < rows; i++) { double v = (double)i / (rows - 1); for (int j = 0; j < columns; j++) { double u = (double)j / (columns - 1); this.textureCoordinates.Add(new Vector2(u, v)); } } } /// <summary> /// Add triangle indices for a rectangular mesh. /// </summary> /// <param name="index0"> /// The index offset. /// </param> /// <param name="rows"> /// The number of rows. /// </param> /// <param name="columns"> /// The number of columns. /// </param> /// <param name="isSpherical"> /// set the flag to true to create a sphere mesh (triangles at top and bottom). /// </param> private void AddRectangularMeshTriangleIndices(int index0, int rows, int columns, bool isSpherical = false) { for (int i = 0; i < rows - 1; i++) { for (int j = 0; j < columns - 1; j++) { int ij = (i * columns) + j; if (!isSpherical || i > 0) { this.triangleIndices.Add(index0 + ij); this.triangleIndices.Add(index0 + ij + 1 + columns); this.triangleIndices.Add(index0 + ij + 1); } if (!isSpherical || i < rows - 2) { this.triangleIndices.Add(index0 + ij + 1 + columns); this.triangleIndices.Add(index0 + ij); this.triangleIndices.Add(index0 + ij + columns); } } } } /// <summary> /// Adds triangular indices for a rectangular mesh. /// </summary> /// <param name="index0"> /// The index 0. /// </param> /// <param name="rows"> /// The rows. /// </param> /// <param name="columns"> /// The columns. /// </param> /// <param name="rowsClosed"> /// True if rows are closed. /// </param> /// <param name="columnsClosed"> /// True if columns are closed. /// </param> private void AddRectangularMeshTriangleIndices( int index0, int rows, int columns, bool rowsClosed, bool columnsClosed) { int m2 = rows - 1; int n2 = columns - 1; if (columnsClosed) { m2++; } if (rowsClosed) { n2++; } for (int i = 0; i < m2; i++) { for (int j = 0; j < n2; j++) { int i00 = index0 + (i * columns) + j; int i01 = index0 + (i * columns) + ((j + 1) % columns); int i10 = index0 + (((i + 1) % rows) * columns) + j; int i11 = index0 + (((i + 1) % rows) * columns) + ((j + 1) % columns); this.triangleIndices.Add(i00); this.triangleIndices.Add(i11); this.triangleIndices.Add(i01); this.triangleIndices.Add(i11); this.triangleIndices.Add(i00); this.triangleIndices.Add(i10); } } } /// <summary> /// Finds the average normal to the specified corner (experimental code). /// </summary> /// <param name="p"> /// The corner Vector2. /// </param> /// <param name="eps"> /// The corner search limit distance. /// </param> /// <returns> /// The normal. /// </returns> private Vector3 FindCornerNormal(Vector3 p, double eps) { var sum = new Vector3(); int count = 0; var addedNormals = new HashSet<Vector3>(); for (int i = 0; i < this.triangleIndices.Count; i += 3) { int i0 = i; int i1 = i + 1; int i2 = i + 2; var p0 = this.positions[this.triangleIndices[i0]]; var p1 = this.positions[this.triangleIndices[i1]]; var p2 = this.positions[this.triangleIndices[i2]]; // check if any of the vertices are on the corner double d0 = (p - p0).LengthSquared; double d1 = (p - p1).LengthSquared; double d2 = (p - p2).LengthSquared; double mind = Math.Min(d0, Math.Min(d1, d2)); if (mind > eps) { continue; } // calculate the triangle normal and check if this face is already added var normal = (p1 - p0).Cross(p2 - p0); normal.Normalize(); // todo: need to use the epsilon value to compare the normals? if (addedNormals.Contains(normal)) { continue; } // todo: this does not work yet // double dp = 1; // foreach (var n in addedNormals) // { // dp = Math.Abs(Vector3.DotProduct(n, normal) - 1); // if (dp < eps) // continue; // } // if (dp < eps) // { // continue; // } count++; sum += normal; addedNormals.Add(normal); } if (count == 0) { return new Vector3(); } return sum * (1.0 / count); } /// <summary> /// Makes sure no triangles share the same vertex. /// </summary> private void NoSharedVertices() { var p = new List<Vector3>(); var ti = new List<int>(); List<Vector3> n = null; if (this.normals != null) { n = new List<Vector3>(); } List<Vector2> tc = null; if (this.textureCoordinates != null) { tc = new List<Vector2>(); } for (int i = 0; i < this.triangleIndices.Count; i += 3) { int i0 = i; int i1 = i + 1; int i2 = i + 2; int index0 = this.triangleIndices[i0]; int index1 = this.triangleIndices[i1]; int index2 = this.triangleIndices[i2]; var p0 = this.positions[index0]; var p1 = this.positions[index1]; var p2 = this.positions[index2]; p.Add(p0); p.Add(p1); p.Add(p2); ti.Add(i0); ti.Add(i1); ti.Add(i2); if (n != null) { n.Add(this.normals[index0]); n.Add(this.normals[index1]); n.Add(this.normals[index2]); } if (tc != null) { tc.Add(this.textureCoordinates[index0]); tc.Add(this.textureCoordinates[index1]); tc.Add(this.textureCoordinates[index2]); } } this.positions = p; this.triangleIndices = ti; this.normals = n; this.textureCoordinates = tc; } /// <summary> /// Subdivides each triangle into four sub-triangles. /// </summary> private void Subdivide4() { // Each triangle is divided into four subtriangles, adding new vertices in the middle of each edge. int ip = this.Positions.Count; int ntri = this.TriangleIndices.Count; for (int i = 0; i < ntri; i += 3) { int i0 = this.TriangleIndices[i]; int i1 = this.TriangleIndices[i + 1]; int i2 = this.TriangleIndices[i + 2]; var p0 = this.Positions[i0]; var p1 = this.Positions[i1]; var p2 = this.Positions[i2]; var v01 = p1 - p0; var v12 = p2 - p1; var v20 = p0 - p2; var p01 = p0 + (v01 * 0.5); var p12 = p1 + (v12 * 0.5); var p20 = p2 + (v20 * 0.5); int i01 = ip++; int i12 = ip++; int i20 = ip++; this.Positions.Add(p01); this.Positions.Add(p12); this.Positions.Add(p20); if (this.normals != null) { var n = this.Normals[i0]; this.Normals.Add(n); this.Normals.Add(n); this.Normals.Add(n); } if (this.textureCoordinates != null) { var uv0 = this.TextureCoordinates[i0]; var uv1 = this.TextureCoordinates[i0 + 1]; var uv2 = this.TextureCoordinates[i0 + 2]; var t01 = uv1 - uv0; var t12 = uv2 - uv1; var t20 = uv0 - uv2; var u01 = uv0 + (t01 * 0.5); var u12 = uv1 + (t12 * 0.5); var u20 = uv2 + (t20 * 0.5); this.TextureCoordinates.Add(u01); this.TextureCoordinates.Add(u12); this.TextureCoordinates.Add(u20); } // TriangleIndices[i ] = i0; this.TriangleIndices[i + 1] = i01; this.TriangleIndices[i + 2] = i20; this.TriangleIndices.Add(i01); this.TriangleIndices.Add(i1); this.TriangleIndices.Add(i12); this.TriangleIndices.Add(i12); this.TriangleIndices.Add(i2); this.TriangleIndices.Add(i20); this.TriangleIndices.Add(i01); this.TriangleIndices.Add(i12); this.TriangleIndices.Add(i20); } } /// <summary> /// Subdivides each triangle into six triangles. Adds a vertex at the midVector2 of each triangle. /// </summary> /// <remarks> /// See <a href="http://en.wikipedia.org/wiki/Barycentric_subdivision">wikipedia</a>. /// </remarks> private void SubdivideBarycentric() { // The BCS of a triangle S divides it into six triangles; each part has one vertex v2 at the // barycenter of S, another one v1 at the midVector2 of some side, and the last one v0 at one // of the original vertices. int im = this.Positions.Count; int ntri = this.TriangleIndices.Count; for (int i = 0; i < ntri; i += 3) { int i0 = this.TriangleIndices[i]; int i1 = this.TriangleIndices[i + 1]; int i2 = this.TriangleIndices[i + 2]; var p0 = this.Positions[i0]; var p1 = this.Positions[i1]; var p2 = this.Positions[i2]; var v01 = p1 - p0; var v12 = p2 - p1; var v20 = p0 - p2; var p01 = p0 + (v01 * 0.5); var p12 = p1 + (v12 * 0.5); var p20 = p2 + (v20 * 0.5); var m = new Vector3((p0.x + p1.x + p2.x) / 3, (p0.y + p1.y + p2.y) / 3, (p0.z + p1.z + p2.z) / 3); int i01 = im + 1; int i12 = im + 2; int i20 = im + 3; this.Positions.Add(m); this.Positions.Add(p01); this.Positions.Add(p12); this.Positions.Add(p20); if (this.normals != null) { var n = this.Normals[i0]; this.Normals.Add(n); this.Normals.Add(n); this.Normals.Add(n); this.Normals.Add(n); } if (this.textureCoordinates != null) { var uv0 = this.TextureCoordinates[i0]; var uv1 = this.TextureCoordinates[i0 + 1]; var uv2 = this.TextureCoordinates[i0 + 2]; var t01 = uv1 - uv0; var t12 = uv2 - uv1; var t20 = uv0 - uv2; var u01 = uv0 + (t01 * 0.5); var u12 = uv1 + (t12 * 0.5); var u20 = uv2 + (t20 * 0.5); var uvm = new Vector2((uv0.x + uv1.x) * 0.5, (uv0.y + uv1.y) * 0.5); this.TextureCoordinates.Add(uvm); this.TextureCoordinates.Add(u01); this.TextureCoordinates.Add(u12); this.TextureCoordinates.Add(u20); } // TriangleIndices[i ] = i0; this.TriangleIndices[i + 1] = i01; this.TriangleIndices[i + 2] = im; this.TriangleIndices.Add(i01); this.TriangleIndices.Add(i1); this.TriangleIndices.Add(im); this.TriangleIndices.Add(i1); this.TriangleIndices.Add(i12); this.TriangleIndices.Add(im); this.TriangleIndices.Add(i12); this.TriangleIndices.Add(i2); this.TriangleIndices.Add(im); this.TriangleIndices.Add(i2); this.TriangleIndices.Add(i20); this.TriangleIndices.Add(im); this.TriangleIndices.Add(i20); this.TriangleIndices.Add(i0); this.TriangleIndices.Add(im); im += 4; } } }
public class MeshElement3D { public Material Material { get; set; } public bool Visible { get; set; } private MeshGeometry3D content; protected virtual MeshGeometry3D Tessellate() { return this.content; } public MeshElement3D() { content = new MeshGeometry3D(); } public Mesh ToMesh(string name, string materialName) { this.content = Tessellate(); return this.content.ToMesh(name, materialName); } public Mesh ToMesh(string name, ColorEx color) { return ToMesh(name, MaterialHelper.GetAbmientMaterial(color).Name); } public Mesh ToMesh(string name, Color color) { return ToMesh(name, MaterialHelper.GetAbmientMaterial(color).Name); } public virtual void ToManualObjectSection(ManualObject mo, string material) { MeshGeometry3D g3d = Tessellate(); mo.Begin(material, OperationType.TriangleList); foreach (var pos in g3d.Positions) { mo.Position(pos); } foreach (var tri in g3d.TriangleIndices) { mo.Index((ushort)tri); } mo.End(); } public virtual void ToManualObjectSection(ManualObject mo, ColorEx color) { MeshGeometry3D g3d = Tessellate(); foreach (var pos in g3d.Positions) { mo.Position(pos); mo.Color(color); } foreach (var tri in g3d.TriangleIndices) { mo.Index((ushort)tri); } } } public class Arrow3D : MeshElement3D { public double Diameter { get; set; } public double HeadLength { get; set; } public Vector3 Point1 { get; set; } public Vector3 Point2 { get; set; } public int ThetaDiv { get; set; } public Arrow3D() { this.Diameter = 1.0; this.HeadLength = 3.0; this.Point1 = Vector3.Zero; this.Point2 = new Vector3(0, 0, 10); ThetaDiv = 36; } protected override MeshGeometry3D Tessellate() { if (this.Diameter <= 0) { return null; } var builder = new MeshBuilder(true, true); builder.AddArrow(this.Point1, this.Point2, this.Diameter, this.HeadLength, this.ThetaDiv); return builder.ToMesh(); } } public class Box3D : MeshElement3D { public bool IsBottom { get; set; } public bool IsTop { get; set; } public Vector3 Center { get; set; } public Vector3 LHW { get; set; } public Box3D() { IsBottom = true; IsTop = true; Center = Vector3.Zero; LHW = Vector3.UnitScale; } protected override MeshGeometry3D Tessellate() { var b = new MeshBuilder(false, true); b.AddCubeFace( this.Center, new Vector3(-1, 0, 0), new Vector3(0, 0, 1), LHW.x, LHW.z, LHW.y); b.AddCubeFace( this.Center, new Vector3(1, 0, 0), new Vector3(0, 0, -1), LHW.x, LHW.z, LHW.y); b.AddCubeFace( this.Center, new Vector3(0, -1, 0), new Vector3(0, 0, 1), LHW.z, LHW.x, LHW.y); b.AddCubeFace( this.Center, new Vector3(0, 1, 0), new Vector3(0, 0, -1), LHW.z, LHW.x, LHW.y); if (this.IsTop) { b.AddCubeFace( this.Center, new Vector3(0, 0, 1), new Vector3(0, -1, 0), LHW.y, LHW.x, LHW.z); } if (this.IsBottom) { b.AddCubeFace( this.Center, new Vector3(0, 0, -1), new Vector3(0, 1, 0), LHW.y, LHW.x, LHW.z); } return b.ToMesh(); } } public class Cube3D : MeshElement3D { public Vector3 Center { get; set; } public double SideLength { get; set; } public Cube3D() { Center = Vector3.Zero; SideLength = 10; } protected override MeshGeometry3D Tessellate() { var b = new MeshBuilder(false, true); b.AddCubeFace( this.Center, new Vector3(-1, 0, 0), new Vector3(0, 0, 1), this.SideLength, this.SideLength, this.SideLength); b.AddCubeFace( this.Center, new Vector3(1, 0, 0), new Vector3(0, 0, -1), this.SideLength, this.SideLength, this.SideLength); b.AddCubeFace( this.Center, new Vector3(0, -1, 0), new Vector3(0, 0, 1), this.SideLength, this.SideLength, this.SideLength); b.AddCubeFace( this.Center, new Vector3(0, 1, 0), new Vector3(0, 0, -1), this.SideLength, this.SideLength, this.SideLength); b.AddCubeFace( this.Center, new Vector3(0, 0, 1), new Vector3(0, -1, 0), this.SideLength, this.SideLength, this.SideLength); b.AddCubeFace( this.Center, new Vector3(0, 0, -1), new Vector3(0, 1, 0), this.SideLength, this.SideLength, this.SideLength); return b.ToMesh(); } } public class Ellipsoid3D : MeshElement3D { public Vector3 Center { get; set; } public int PhiDiv { get; set; } public double RadiusX { get; set; } public double RadiusY { get; set; } public double RadiusZ { get; set; } public int ThetaDiv { get; set; } public Ellipsoid3D() { this.Center = Vector3.Zero; this.PhiDiv = 30; this.RadiusX = 1.0; this.RadiusY = 1.0; this.RadiusZ = 1.0; this.ThetaDiv = 60; } protected override MeshGeometry3D Tessellate() { var builder = new MeshBuilder(false, true); builder.AddEllipsoid(this.Center, this.RadiusX, this.RadiusY, this.RadiusZ, this.ThetaDiv, this.PhiDiv); return builder.ToMesh(); } } public class GridLines3D : MeshElement3D { public Vector3 Center { get; set; } public double MinorDistance { get; set; } public Vector3 LengthDirection { get; set; } public double Length { get; set; } public double MajorDistance { get; set; } public Vector3 Normal { get; set; } public double Thickness { get; set; } public double Width { get; set; } private Vector3 lengthDirection; private Vector3 widthDirection; public GridLines3D() { Center = Vector3.Zero; this.MinorDistance = 2.5; this.LengthDirection = Vector3.UnitX; this.Length = 200; this.MajorDistance = 10; this.Normal = Vector3.UnitY; this.Thickness = 0.1; this.Width = 200; } protected override MeshGeometry3D Tessellate() { this.lengthDirection = this.LengthDirection; this.lengthDirection.Normalize(); this.widthDirection = this.Normal.Cross(this.lengthDirection); this.widthDirection.Normalize(); var mesh = new MeshBuilder(true, false); double minX = -this.Width / 2; double minY = -this.Length / 2; double maxX = this.Width / 2; double maxY = this.Length / 2; double x = minX; double eps = this.MinorDistance / 10; while (x <= maxX + eps) { double t = this.Thickness; if (IsMultipleOf(x, this.MajorDistance)) { t *= 2; } this.AddLineX(mesh, x, minY, maxY, t); x += this.MinorDistance; } double y = minY; while (y <= maxY + eps) { double t = this.Thickness; if (IsMultipleOf(y, this.MajorDistance)) { t *= 2; } this.AddLineY(mesh, y, minX, maxX, t); y += this.MinorDistance; } var m = mesh.ToMesh(); return m; } private static bool IsMultipleOf(double y, double d) { double y2 = d * (int)(y / d); return Math.Abs(y - y2) < 1e-3; } private void AddLineX(MeshBuilder mesh, double x, double minY, double maxY, double thickness) { int i0 = mesh.Positions.Count; mesh.Positions.Add(this.GetPoint(x - (thickness / 2), minY)); mesh.Positions.Add(this.GetPoint(x - (thickness / 2), maxY)); mesh.Positions.Add(this.GetPoint(x + (thickness / 2), maxY)); mesh.Positions.Add(this.GetPoint(x + (thickness / 2), minY)); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.TriangleIndices.Add(i0); mesh.TriangleIndices.Add(i0 + 1); mesh.TriangleIndices.Add(i0 + 2); mesh.TriangleIndices.Add(i0 + 2); mesh.TriangleIndices.Add(i0 + 3); mesh.TriangleIndices.Add(i0); } private void AddLineY(MeshBuilder mesh, double y, double minX, double maxX, double thickness) { int i0 = mesh.Positions.Count; mesh.Positions.Add(this.GetPoint(minX, y + (thickness / 2))); mesh.Positions.Add(this.GetPoint(maxX, y + (thickness / 2))); mesh.Positions.Add(this.GetPoint(maxX, y - (thickness / 2))); mesh.Positions.Add(this.GetPoint(minX, y - (thickness / 2))); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.Normals.Add(this.Normal); mesh.TriangleIndices.Add(i0); mesh.TriangleIndices.Add(i0 + 1); mesh.TriangleIndices.Add(i0 + 2); mesh.TriangleIndices.Add(i0 + 2); mesh.TriangleIndices.Add(i0 + 3); mesh.TriangleIndices.Add(i0); } /// <summary> /// Gets a point on the plane. /// </summary> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> /// <returns>A <see cref="Point3D"/>.</returns> private Vector3 GetPoint(double x, double y) { return this.Center + (this.widthDirection * x) + (this.lengthDirection * y); } } public class PieSlice3D : MeshElement3D { public Vector3 Center { get; set; } public double EndAngle { get; set; } public double InnerRadius { get; set; } public Vector3 Normal { get; set; } public double OuterRadius { get; set; } public double StartAngle { get; set; } public int ThetaDiv { get; set; } public Vector3 UpVector { get; set; } public PieSlice3D() { this.Center = Vector3.Zero; this.EndAngle = 90; this.InnerRadius = 0.5; this.Normal = Vector3.UnitZ; this.OuterRadius = 1.0; this.StartAngle = 0; this.ThetaDiv = 20; this.UpVector = Vector3.UnitY; } protected override MeshGeometry3D Tessellate() { var pts = new List<Vector3>(); var right = this.UpVector.Cross(this.Normal); for (int i = 0; i < this.ThetaDiv; i++) { double angle = this.StartAngle + ((this.EndAngle - this.StartAngle) * i / (this.ThetaDiv - 1)); double angleRad = angle / 180 * Math.PI; var dir = (right * Math.Cos(angleRad)) + (this.UpVector * Math.Sin(angleRad)); pts.Add(this.Center + (dir * this.InnerRadius)); pts.Add(this.Center + (dir * this.OuterRadius)); } var b = new MeshBuilder(false, false); b.AddTriangleStrip(pts); return b.ToMesh(); } } public class Pipe3D : MeshElement3D { public double Diameter { get; set; } public double InnerDiameter { get; set; } public Vector3 Point1 { get; set; } public Vector3 Point2 { get; set; } public int ThetaDiv { get; set; } public Pipe3D() { this.Diameter = 1.0; this.InnerDiameter = 0.0; this.Point1 = Vector3.Zero; this.Point2 = new Vector3(0, 0, 10); this.ThetaDiv = 36; } protected override MeshGeometry3D Tessellate() { var builder = new MeshBuilder(false, true); builder.AddPipe(this.Point1, this.Point2, this.InnerDiameter, this.Diameter, this.ThetaDiv); return builder.ToMesh(); } } public class Quad3D : MeshElement3D { public Vector3 Point1 { get; set; } public Vector3 Point2 { get; set; } public Vector3 Point3 { get; set; } public Vector3 Point4 { get; set; } public Quad3D() { Point1 = Vector3.Zero; Point2 = Vector3.UnitX; Point3 = new Vector3(1, 1, 0); Point4 = Vector3.UnitY; } protected override MeshGeometry3D Tessellate() { var builder = new MeshBuilder(false, true); builder.AddQuad( this.Point1, this.Point2, this.Point3, this.Point4, new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0), new Vector2(0, 0)); return builder.ToMesh(); } } public class Rectangle3D : MeshElement3D { public int DivLength { get; set; } public int DivWidth { get; set; } public Vector3 LengthDirection { get; set; } public double Length { get; set; } public Vector3 Normal { get; set; } public Vector3 Origin { get; set; } public double Width { get; set; } public Rectangle3D() { this.DivLength = 10; this.DivWidth = 10; this.LengthDirection = Vector3.UnitX; this.Length = 10.0; this.Normal = Vector3.UnitZ; this.Origin = Vector3.Zero; this.Width = 10.0; } protected override MeshGeometry3D Tessellate() { Vector3 u = this.LengthDirection; Vector3 w = this.Normal; Vector3 v = w.Cross(u); u = v.Cross(w); u.Normalize(); v.Normalize(); w.Normalize(); double le = this.Length; double wi = this.Width; var pts = new List<Vector3>(); for (int i = 0; i < this.DivLength; i++) { double fi = -0.5 + ((double)i / (this.DivLength - 1)); for (int j = 0; j < this.DivWidth; j++) { double fj = -0.5 + ((double)j / (this.DivWidth - 1)); pts.Add(this.Origin + (u * le * fi) + (v * wi * fj)); } } var builder = new MeshBuilder(false, true); builder.AddRectangularMesh(pts, this.DivWidth); return builder.ToMesh(); } } public class Sphere3D : MeshElement3D { public Vector3 Center { get; set; } public int PhiDiv { get; set; } public double Radius { get; set; } public int ThetaDiv { get; set; } public Sphere3D() { this.Center = Vector3.Zero; this.PhiDiv = 30; this.Radius = 1.0; this.ThetaDiv = 60; } protected override MeshGeometry3D Tessellate() { var builder = new MeshBuilder(true, true); builder.AddSphere(this.Center, this.Radius, this.ThetaDiv, this.PhiDiv); return builder.ToMesh(); } } public class TruncatedCone3D : MeshElement3D { public bool BaseCap { get; set; } public double BaseRadius { get; set; } public double Height { get; set; } public Vector3 Normal { get; set; } public Vector3 Origin { get; set; } public int ThetaDiv { get; set; } public bool TopCap { get; set; } public double TopRadius { get; set; } public TruncatedCone3D() { this.BaseCap = true; this.BaseRadius = 1.0; this.Height = 2.0; this.Normal = Vector3.UnitZ; this.Origin = Vector3.Zero; this.ThetaDiv = 35; this.TopCap = true; this.TopRadius = 0.0; } protected override MeshGeometry3D Tessellate() { var builder = new MeshBuilder(false, true); builder.AddCone( this.Origin, this.Normal, this.BaseRadius, this.TopRadius, this.Height, this.BaseCap, this.TopCap, this.ThetaDiv); return builder.ToMesh(); } } public abstract class ParametricSurface3D : MeshElement3D { public int MeshSizeV { get; set; } public int MeshSizeU { get; set; } public ParametricSurface3D() { this.MeshSizeU = 200; this.MeshSizeV = 200; } protected abstract Vector3 Evaluate(double u, double v, out Vector2 textureCoord); protected override MeshGeometry3D Tessellate() { var mesh = new MeshGeometry3D(); mesh.Positions = new List<Vector3>(); mesh.TextureCoordinates = new List<Vector2>(); mesh.TriangleIndices = new List<int>(); int n = this.MeshSizeU; int m = this.MeshSizeV; var p = new Vector3[m * n]; var tc = new Vector2[m * n]; // todo: use MeshBuilder // todo: parallel execution... // Parallel.For(0, n, (i) => for (int i = 0; i < n; i++) { double u = 1.0 * i / (n - 1); for (int j = 0; j < m; j++) { double v = 1.0 * j / (m - 1); int ij = (i * m) + j; p[ij] = this.Evaluate(u, v, out tc[ij]); } } // ); int idx = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { mesh.Positions.Add(p[idx]); mesh.TextureCoordinates.Add(tc[idx]); idx++; } } for (int i = 0; i + 1 < n; i++) { for (int j = 0; j + 1 < m; j++) { int x0 = i * m; int x1 = (i + 1) * m; int y0 = j; int y1 = j + 1; AddTriangle(mesh, x0 + y0, x0 + y1, x1 + y0); AddTriangle(mesh, x1 + y0, x0 + y1, x1 + y1); } } return mesh; } private static void AddTriangle(MeshGeometry3D mesh, int i1, int i2, int i3) { var p1 = mesh.Positions[i1]; if (!IsDefined(p1)) { return; } var p2 = mesh.Positions[i2]; if (!IsDefined(p2)) { return; } var p3 = mesh.Positions[i3]; if (!IsDefined(p3)) { return; } mesh.TriangleIndices.Add(i1); mesh.TriangleIndices.Add(i2); mesh.TriangleIndices.Add(i3); } private static bool IsDefined(Vector3 point) { return !double.IsNaN(point.x) && !double.IsNaN(point.y) && !double.IsNaN(point.z); } } public class Helix3D : ParametricSurface3D { public Vector3 Origin { get; set; } public double Diameter { get; set; } public double Length { get; set; } public double Phase { get; set; } public double Radius { get; set; } public double Turns { get; set; } public Helix3D() { this.Origin = Vector3.Zero; this.Diameter = 0.5; this.Length = 1.0; this.Phase = 0.0; this.Radius = 1.0; this.Turns = 1.0; } protected override Vector3 Evaluate(double u, double v, out Vector2 texCoord) { double color = u; v *= 2 * Math.PI; double b = this.Turns * 2 * Math.PI; double r = this.Radius / 2; double d = this.Diameter; double dr = this.Diameter / r; double p = this.Phase / 180 * Math.PI; double x = r * Math.Cos((b * u) + p) * (2 + (dr * Math.Cos(v))); double y = r * Math.Sin((b * u) + p) * (2 + (dr * Math.Cos(v))); double z = (u * this.Length) + (d * Math.Sin(v)); texCoord = new Vector2(color, 0); return this.Origin + new Vector3(x, y, z); } } public class Extruded3D : MeshElement3D { public List<double> Diameters { get; set; } public Vector3 SectionXAxis { get; set; } public List<double> Angles { get; set; } public bool IsPathClosed { get; set; } public bool IsSectionClosed { get; set; } public List<Vector3> Path { get; set; } public List<Vector2> Section { get; set; } public List<double> TextureCoordinates { get; set; } public Extruded3D() { this.Diameters = null; this.SectionXAxis = new Vector3(); this.Angles = null; this.IsPathClosed = false; this.IsSectionClosed = true; this.Path = new List<Vector3>(); this.Section = new List<Vector2>(); this.TextureCoordinates = null; } protected override MeshGeometry3D Tessellate() { if (this.Path == null || this.Path.Count == 0) { return null; } // See also "The GLE Tubing and Extrusion Library": // http://linas.org/gle/ // http://sharpmap.codeplex.com/Thread/View.aspx?ThreadId=18864 var builder = new MeshBuilder(false, this.TextureCoordinates != null); var sectionXAxis = this.SectionXAxis; if (sectionXAxis.Length < 1e-6) { sectionXAxis = new Vector3(1, 0, 0); } var forward = this.Path[1] - this.Path[0]; var up = forward.Cross(sectionXAxis); if (up.LengthSquared < 1e-6) { sectionXAxis = forward.FindAnyPerpendicular(); } builder.AddTube( this.Path, this.Angles, this.TextureCoordinates, this.Diameters, this.Section, sectionXAxis, this.IsPathClosed, this.IsSectionClosed); return builder.ToMesh(); } } public class TubeVisual3D : Extruded3D { public double Diameter { get; set; } public int ThetaDiv { get; set; } public TubeVisual3D() { this.Diameter = 1.0; this.ThetaDiv = 36; } protected override MeshGeometry3D Tessellate() { var pc = new List<Vector2>(); var circle = MeshBuilder.GetCircle(this.ThetaDiv); // If Diameters is set, create a unit circle // otherwise, create a circle with the specified diameter double r = this.Diameters != null ? 1 : this.Diameter / 2; for (int j = 0; j < this.ThetaDiv; j++) { pc.Add(new Vector2(circle[j].x * r, circle[j].y * r)); } this.Section = pc; return base.Tessellate(); } }
配合我们上面的几个类一起使用,就是我们最前面看到的效果,顶点上显示元素,FPS几乎没有任何变化.要知道整合快300多个球体后,如上图,顶点索引达到快20W了,用ManualObject直接渲染出错,因为ManualObject的索引固定只能是Int16,最高索引值只用6W多点.
string pointName = this.Parent.MeshName + "/point/Show"; Root.Instance.SceneManager.DeleteEntityAndMesh(pointName); MergerBatch merBatch = new MergerBatch(); foreach (var vect in this) { //SelectPoint和别的顶点混在一起,直接改变SelectPoint可能被别的遮住. //单独渲染这个被选择了的 if (vect == this.SelectPoint) { ManualObject mo = new ManualObject(this.Parent.MeshName + "point/Show/Select"); //以Overlay层渲染,几乎不会被遮住 mo.RenderQueueGroup = RenderQueueGroupID.Overlay; var sphereX1 = new Sphere3D(); sphereX1.Center = vect.VertexInfo.Position; sphereX1.ThetaDiv = 24; sphereX1.Radius = this.Scale; sphereX1.ToManualObjectSection(mo, "Vertex/Selected"); this.Parent.PointNode.AttachObject(mo); continue; } var builder = new MeshBuilder(false, false); builder.AddSphere(vect.VertexInfo.Position, Scale, 12, 10); merBatch.AddBatch(builder.Positions, builder.TriangleIndices, vect.Color); } Mesh mesh = merBatch.ToMesh(pointName); Entity ent = Root.Instance.SceneManager.CreateEntity(pointName, mesh); //一般的是Main,50 ent.RenderQueueGroup = RenderQueueGroupID.Six; this.Parent.PointNode.AttachObject(ent);
现在可以说达到我想要的效果,FPS不掉,顶点上能显示任何几何模型(虽然图上全是点,但是每个顶点可以显示不同的也在同一批次),顶点上几何模型能独立设置颜色.上面代码中添加的SelectPoint是单独作用一个批次渲染,其实是可以合到一起,但是会出现一个问题,如果多个顶点在你摄像机前重合在一起,你后面的拾取到的顶点,你可能并没有看到他变颜色,因为被遮住了,现在单独渲染,提高被选择顶点的渲染优先级.
但是这个MergerBatch合并限制和缺点也很多,如只能合并同材质的,合并后的拾取只针对统一整体,单独需要自己计算,摄像机的可见测试也是以整体来测试.但是这些对于这种编辑显示顶点的来说不需要考虑.