From 07ee2c3504edb0c328020aed6f5d3f182a3b67c5 Mon Sep 17 00:00:00 2001 From: Dahlia Trimble Date: Sun, 30 Nov 2008 03:52:18 +0000 Subject: [PATCH] Revert r7548 and r7549 until someone with prebuild-fu can help structure the dependencies --- OpenSim/Region/Physics/Meshing/PrimMesher.cs | 2168 ++++++++++++++++++ OpenSim/Region/Physics/Meshing/SculptMesh.cs | 343 +++ bin/Physics/PrimMesher.dll | Bin 32768 -> 0 bytes prebuild.xml | 1 - 4 files changed, 2511 insertions(+), 1 deletion(-) create mode 100644 OpenSim/Region/Physics/Meshing/PrimMesher.cs create mode 100644 OpenSim/Region/Physics/Meshing/SculptMesh.cs delete mode 100644 bin/Physics/PrimMesher.dll diff --git a/OpenSim/Region/Physics/Meshing/PrimMesher.cs b/OpenSim/Region/Physics/Meshing/PrimMesher.cs new file mode 100644 index 0000000000..1fed64d742 --- /dev/null +++ b/OpenSim/Region/Physics/Meshing/PrimMesher.cs @@ -0,0 +1,2168 @@ +/* + * Copyright (c) Contributors + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace PrimMesher +{ + public struct Quat + { + /// X value + public float X; + /// Y value + public float Y; + /// Z value + public float Z; + /// W value + public float W; + + public Quat(float x, float y, float z, float w) + { + X = x; + Y = y; + Z = z; + W = w; + } + + public Quat(Coord axis, float angle) + { + axis = axis.Normalize(); + + angle *= 0.5f; + float c = (float)Math.Cos(angle); + float s = (float)Math.Sin(angle); + + X = axis.X * s; + Y = axis.Y * s; + Z = axis.Z * s; + W = c; + + Normalize(); + } + + public float Length() + { + return (float)Math.Sqrt(X * X + Y * Y + Z * Z + W * W); + } + + public Quat Normalize() + { + const float MAG_THRESHOLD = 0.0000001f; + float mag = Length(); + + // Catch very small rounding errors when normalizing + if (mag > MAG_THRESHOLD) + { + float oomag = 1f / mag; + X *= oomag; + Y *= oomag; + Z *= oomag; + W *= oomag; + } + else + { + X = 0f; + Y = 0f; + Z = 0f; + W = 1f; + } + + return this; + } + + public override string ToString() + { + return "< X: " + this.X.ToString() + ", Y: " + this.Y.ToString() + ", Z: " + this.Z.ToString() + ", W: " + this.W.ToString() + ">"; + } + } + + public struct Coord + { + public float X; + public float Y; + public float Z; + + public Coord(float x, float y, float z) + { + this.X = x; + this.Y = y; + this.Z = z; + } + + public float Length() + { + return (float)Math.Sqrt(this.X * this.X + this.Y * this.Y + this.Z * this.Z); + } + + public Coord Invert() + { + this.X = -this.X; + this.Y = -this.Y; + this.Z = -this.Z; + + return this; + } + + public Coord Normalize() + { + const float MAG_THRESHOLD = 0.0000001f; + float mag = Length(); + + // Catch very small rounding errors when normalizing + if (mag > MAG_THRESHOLD) + { + float oomag = 1.0f / mag; + this.X *= oomag; + this.Y *= oomag; + this.Z *= oomag; + } + else + { + this.X = 0.0f; + this.Y = 0.0f; + this.Z = 0.0f; + } + + return this; + } + + public override string ToString() + { + return this.X.ToString() + " " + this.Y.ToString() + " " + this.Z.ToString(); + } + + public static Coord Cross(Coord c1, Coord c2) + { + return new Coord( + c1.Y * c2.Z - c2.Y * c1.Z, + c1.Z * c2.X - c2.Z * c1.X, + c1.X * c2.Y - c2.X * c1.Y + ); + } + + public static Coord operator +(Coord v, Coord a) + { + return new Coord(v.X + a.X, v.Y + a.Y, v.Z + a.Z); + } + + public static Coord operator *(Coord v, Coord m) + { + return new Coord(v.X * m.X, v.Y * m.Y, v.Z * m.Z); + } + + public static Coord operator *(Coord v, Quat q) + { + // From http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/transforms/ + + Coord c2 = new Coord(0.0f, 0.0f, 0.0f); + + c2.X = q.W * q.W * v.X + + 2f * q.Y * q.W * v.Z - + 2f * q.Z * q.W * v.Y + + q.X * q.X * v.X + + 2f * q.Y * q.X * v.Y + + 2f * q.Z * q.X * v.Z - + q.Z * q.Z * v.X - + q.Y * q.Y * v.X; + + c2.Y = + 2f * q.X * q.Y * v.X + + q.Y * q.Y * v.Y + + 2f * q.Z * q.Y * v.Z + + 2f * q.W * q.Z * v.X - + q.Z * q.Z * v.Y + + q.W * q.W * v.Y - + 2f * q.X * q.W * v.Z - + q.X * q.X * v.Y; + + c2.Z = + 2f * q.X * q.Z * v.X + + 2f * q.Y * q.Z * v.Y + + q.Z * q.Z * v.Z - + 2f * q.W * q.Y * v.X - + q.Y * q.Y * v.Z + + 2f * q.W * q.X * v.Y - + q.X * q.X * v.Z + + q.W * q.W * v.Z; + + return c2; + } + } + + public struct UVCoord + { + public float U; + public float V; + + + public UVCoord(float u, float v) + { + this.U = u; + this.V = v; + } + } + + public struct Face + { + public int primFace; + + // vertices + public int v1; + public int v2; + public int v3; + + //normals + public int n1; + public int n2; + public int n3; + + // uvs + public int uv1; + public int uv2; + public int uv3; + + + public Face(int v1, int v2, int v3) + { + primFace = 0; + + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + + this.n1 = 0; + this.n2 = 0; + this.n3 = 0; + + this.uv1 = 0; + this.uv2 = 0; + this.uv3 = 0; + + } + + public Face(int v1, int v2, int v3, int n1, int n2, int n3) + { + primFace = 0; + + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + + this.n1 = n1; + this.n2 = n2; + this.n3 = n3; + + this.uv1 = 0; + this.uv2 = 0; + this.uv3 = 0; + } + + public Coord SurfaceNormal(List coordList) + { + Coord c1 = coordList[this.v1]; + Coord c2 = coordList[this.v2]; + Coord c3 = coordList[this.v3]; + + Coord edge1 = new Coord(c2.X - c1.X, c2.Y - c1.Y, c2.Z - c1.Z); + Coord edge2 = new Coord(c3.X - c1.X, c3.Y - c1.Y, c3.Z - c1.Z); + + return Coord.Cross(edge1, edge2).Normalize(); + } + } + + public struct ViewerFace + { + public int primFaceNumber; + + public Coord v1; + public Coord v2; + public Coord v3; + + public Coord n1; + public Coord n2; + public Coord n3; + + public UVCoord uv1; + public UVCoord uv2; + public UVCoord uv3; + + public ViewerFace(int primFaceNumber) + { + this.primFaceNumber = primFaceNumber; + + this.v1 = new Coord(); + this.v2 = new Coord(); + this.v3 = new Coord(); + + this.n1 = new Coord(); + this.n2 = new Coord(); + this.n3 = new Coord(); + + this.uv1 = new UVCoord(); + this.uv2 = new UVCoord(); + this.uv3 = new UVCoord(); + } + + public void Scale(float x, float y, float z) + { + this.v1.X *= x; + this.v1.Y *= y; + this.v1.Z *= z; + + this.v2.X *= x; + this.v2.Y *= y; + this.v2.Z *= z; + + this.v3.X *= x; + this.v3.Y *= y; + this.v3.Z *= z; + } + + public void AddRot(Quat q) + { + this.v1 *= q; + this.v2 *= q; + this.v3 *= q; + + this.n1 *= q; + this.n2 *= q; + this.n3 *= q; + } + + public void CalcSurfaceNormal() + { + + Coord edge1 = new Coord(this.v2.X - this.v1.X, this.v2.Y - this.v1.Y, this.v2.Z - this.v1.Z); + Coord edge2 = new Coord(this.v3.X - this.v1.X, this.v3.Y - this.v1.Y, this.v3.Z - this.v1.Z); + + this.n1 = this.n2 = this.n3 = Coord.Cross(edge1, edge2).Normalize(); + } + } + + internal struct Angle + { + internal float angle; + internal float X; + internal float Y; + + internal Angle(float angle, float x, float y) + { + this.angle = angle; + this.X = x; + this.Y = y; + } + } + + internal class AngleList + { + private float iX, iY; // intersection point + + private Angle[] angles3 = + { + new Angle(0.0f, 1.0f, 0.0f), + new Angle(0.33333333333333333f, -0.5f, 0.86602540378443871f), + new Angle(0.66666666666666667f, -0.5f, -0.86602540378443837f), + new Angle(1.0f, 1.0f, 0.0f) + }; + + private Coord[] normals3 = + { + new Coord(0.25f, 0.4330127019f, 0.0f).Normalize(), + new Coord(-0.5f, 0.0f, 0.0f).Normalize(), + new Coord(0.25f, -0.4330127019f, 0.0f).Normalize(), + new Coord(0.25f, 0.4330127019f, 0.0f).Normalize() + }; + + private Angle[] angles4 = + { + new Angle(0.0f, 1.0f, 0.0f), + new Angle(0.25f, 0.0f, 1.0f), + new Angle(0.5f, -1.0f, 0.0f), + new Angle(0.75f, 0.0f, -1.0f), + new Angle(1.0f, 1.0f, 0.0f) + }; + + private Coord[] normals4 = + { + new Coord(0.5f, 0.5f, 0.0f).Normalize(), + new Coord(-0.5f, 0.5f, 0.0f).Normalize(), + new Coord(-0.5f, -0.5f, 0.0f).Normalize(), + new Coord(0.5f, -0.5f, 0.0f).Normalize(), + new Coord(0.5f, 0.5f, 0.0f).Normalize() + }; + + private Angle[] angles24 = + { + new Angle(0.0f, 1.0f, 0.0f), + new Angle(0.041666666666666664f, 0.96592582628906831f, 0.25881904510252074f), + new Angle(0.083333333333333329f, 0.86602540378443871f, 0.5f), + new Angle(0.125f, 0.70710678118654757f, 0.70710678118654746f), + new Angle(0.16666666666666667f, 0.5f, 0.8660254037844386f), + new Angle(0.20833333333333331f, 0.25881904510252096f, 0.9659258262890682f), + new Angle(0.25f, 0.0f, 1.0f), + new Angle(0.29166666666666663f, -0.25881904510252063f, 0.96592582628906831f), + new Angle(0.33333333333333333f, -0.5f, 0.86602540378443871f), + new Angle(0.375f, -0.70710678118654746f, 0.70710678118654757f), + new Angle(0.41666666666666663f, -0.86602540378443849f, 0.5f), + new Angle(0.45833333333333331f, -0.9659258262890682f, 0.25881904510252102f), + new Angle(0.5f, -1.0f, 0.0f), + new Angle(0.54166666666666663f, -0.96592582628906842f, -0.25881904510252035f), + new Angle(0.58333333333333326f, -0.86602540378443882f, -0.5f), + new Angle(0.62499999999999989f, -0.70710678118654791f, -0.70710678118654713f), + new Angle(0.66666666666666667f, -0.5f, -0.86602540378443837f), + new Angle(0.70833333333333326f, -0.25881904510252152f, -0.96592582628906809f), + new Angle(0.75f, 0.0f, -1.0f), + new Angle(0.79166666666666663f, 0.2588190451025203f, -0.96592582628906842f), + new Angle(0.83333333333333326f, 0.5f, -0.86602540378443904f), + new Angle(0.875f, 0.70710678118654735f, -0.70710678118654768f), + new Angle(0.91666666666666663f, 0.86602540378443837f, -0.5f), + new Angle(0.95833333333333326f, 0.96592582628906809f, -0.25881904510252157f), + new Angle(1.0f, 1.0f, 0.0f) + }; + + private Angle interpolatePoints(float newPoint, Angle p1, Angle p2) + { + float m = (newPoint - p1.angle) / (p2.angle - p1.angle); + return new Angle(newPoint, p1.X + m * (p2.X - p1.X), p1.Y + m * (p2.Y - p1.Y)); + } + + private void intersection(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { // ref: http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/ + double denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + double uaNumerator = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); + + if (denom != 0.0) + { + double ua = uaNumerator / denom; + iX = (float)(x1 + ua * (x2 - x1)); + iY = (float)(y1 + ua * (y2 - y1)); + } + } + + internal List angles; + internal List normals; + + internal void makeAngles(int sides, float startAngle, float stopAngle) + { + angles = new List(); + normals = new List(); + + double twoPi = System.Math.PI * 2.0; + float twoPiInv = 1.0f / (float)twoPi; + + if (sides < 1) + throw new Exception("number of sides not greater than zero"); + if (stopAngle <= startAngle) + throw new Exception("stopAngle not greater than startAngle"); + + if ((sides == 3 || sides == 4 || sides == 24)) + { + startAngle *= twoPiInv; + stopAngle *= twoPiInv; + + Angle[] sourceAngles; + if (sides == 3) + sourceAngles = angles3; + else if (sides == 4) + sourceAngles = angles4; + else sourceAngles = angles24; + + int startAngleIndex = (int)(startAngle * sides); + int endAngleIndex = sourceAngles.Length - 1; + if (stopAngle < 1.0f) + endAngleIndex = (int)(stopAngle * sides) + 1; + if (endAngleIndex == startAngleIndex) + endAngleIndex++; + + for (int angleIndex = startAngleIndex; angleIndex < endAngleIndex + 1; angleIndex++) + { + angles.Add(sourceAngles[angleIndex]); + if (sides == 3) + normals.Add(normals3[angleIndex]); + else if (sides == 4) + normals.Add(normals4[angleIndex]); + } + + if (startAngle > 0.0f) + angles[0] = interpolatePoints(startAngle, angles[0], angles[1]); + + if (stopAngle < 1.0f) + { + int lastAngleIndex = angles.Count - 1; + angles[lastAngleIndex] = interpolatePoints(stopAngle, angles[lastAngleIndex - 1], angles[lastAngleIndex]); + } + } + else + { + double stepSize = twoPi / sides; + + int startStep = (int)(startAngle / stepSize); + double angle = stepSize * startStep; + int step = startStep; + double stopAngleTest = stopAngle; + if (stopAngle < twoPi) + { + stopAngleTest = stepSize * ((int)(stopAngle / stepSize) + 1); + if (stopAngleTest < stopAngle) + stopAngleTest += stepSize; + if (stopAngleTest > twoPi) + stopAngleTest = twoPi; + } + + while (angle <= stopAngleTest) + { + Angle newAngle; + newAngle.angle = (float)angle; + newAngle.X = (float)System.Math.Cos(angle); + newAngle.Y = (float)System.Math.Sin(angle); + angles.Add(newAngle); + step += 1; + angle = stepSize * step; + } + + if (startAngle > angles[0].angle) + { + Angle newAngle; + intersection(angles[0].X, angles[0].Y, angles[1].X, angles[1].Y, 0.0f, 0.0f, (float)Math.Cos(startAngle), (float)Math.Sin(startAngle)); + newAngle.angle = startAngle; + newAngle.X = iX; + newAngle.Y = iY; + angles[0] = newAngle; + } + + int index = angles.Count - 1; + if (stopAngle < angles[index].angle) + { + Angle newAngle; + intersection(angles[index - 1].X, angles[index - 1].Y, angles[index].X, angles[index].Y, 0.0f, 0.0f, (float)Math.Cos(stopAngle), (float)Math.Sin(stopAngle)); + newAngle.angle = stopAngle; + newAngle.X = iX; + newAngle.Y = iY; + angles[index] = newAngle; + } + } + } + } + + /// + /// generates a profile for extrusion + /// + internal class Profile + { + private const float twoPi = 2.0f * (float)Math.PI; + + internal List coords; + internal List faces; + internal List vertexNormals; + internal List us; + internal List faceUVs; + internal List faceNumbers; + + internal Coord faceNormal = new Coord(0.0f, 0.0f, 1.0f); + internal Coord cutNormal1 = new Coord(); + internal Coord cutNormal2 = new Coord(); + + internal int numOuterVerts = 0; + internal int numHollowVerts = 0; + + internal bool calcVertexNormals = false; + internal int bottomFaceNumber = 0; + internal int numPrimFaces = 0; + + internal Profile() + { + this.coords = new List(); + this.faces = new List(); + this.vertexNormals = new List(); + this.us = new List(); + this.faceUVs = new List(); + this.faceNumbers = new List(); + } + + internal Profile(int sides, float profileStart, float profileEnd, float hollow, int hollowSides, bool createFaces, bool calcVertexNormals) + { + this.calcVertexNormals = calcVertexNormals; + this.coords = new List(); + this.faces = new List(); + this.vertexNormals = new List(); + this.us = new List(); + this.faceUVs = new List(); + this.faceNumbers = new List(); + + Coord center = new Coord(0.0f, 0.0f, 0.0f); + + List hollowCoords = new List(); + List hollowNormals = new List(); + List hollowUs = new List(); + + bool hasHollow = (hollow > 0.0f); + + bool hasProfileCut = (profileStart > 0.0f || profileEnd < 1.0f); + + AngleList angles = new AngleList(); + AngleList hollowAngles = new AngleList(); + + float xScale = 0.5f; + float yScale = 0.5f; + if (sides == 4) // corners of a square are sqrt(2) from center + { + xScale = 0.707f; + yScale = 0.707f; + } + + float startAngle = profileStart * twoPi; + float stopAngle = profileEnd * twoPi; + + try { angles.makeAngles(sides, startAngle, stopAngle); } + catch (Exception ex) + { + Console.WriteLine("makeAngles failed: Exception: " + ex.ToString()); + Console.WriteLine("sides: " + sides.ToString() + " startAngle: " + startAngle.ToString() + " stopAngle: " + stopAngle.ToString()); + return; + } + + this.numOuterVerts = angles.angles.Count; + + // flag to create as few triangles as possible for 3 or 4 side profile + bool simpleFace = (sides < 5 && !(hasHollow || hasProfileCut)); + + if (hasHollow) + { + if (sides == hollowSides) + hollowAngles = angles; + else + { + try { hollowAngles.makeAngles(hollowSides, startAngle, stopAngle); } + catch (Exception ex) + { + Console.WriteLine("makeAngles failed: Exception: " + ex.ToString()); + Console.WriteLine("sides: " + sides.ToString() + " startAngle: " + startAngle.ToString() + " stopAngle: " + stopAngle.ToString()); + return; + } + } + this.numHollowVerts = hollowAngles.angles.Count; + } + else if (!simpleFace) + { + this.coords.Add(center); + if (this.calcVertexNormals) + this.vertexNormals.Add(new Coord(0.0f, 0.0f, 1.0f)); + this.us.Add(0.0f); + } + + float z = 0.0f; + + Angle angle; + Coord newVert = new Coord(); + if (hasHollow && hollowSides != sides) + { + int numHollowAngles = hollowAngles.angles.Count; + for (int i = 0; i < numHollowAngles; i++) + { + angle = hollowAngles.angles[i]; + newVert.X = hollow * xScale * angle.X; + newVert.Y = hollow * yScale * angle.Y; + newVert.Z = z; + + hollowCoords.Add(newVert); + if (this.calcVertexNormals) + { + if (hollowSides < 5) + hollowNormals.Add(hollowAngles.normals[i].Invert()); + else + hollowNormals.Add(new Coord(-angle.X, -angle.Y, 0.0f)); + + hollowUs.Add(angle.angle * hollow); + } + } + } + + int index = 0; + int numAngles = angles.angles.Count; + + for (int i = 0; i < numAngles; i++) + { + int iNext = i == numAngles ? i + 1 : 0; + angle = angles.angles[i]; + newVert.X = angle.X * xScale; + newVert.Y = angle.Y * yScale; + newVert.Z = z; + this.coords.Add(newVert); + if (this.calcVertexNormals) + { + + if (sides < 5) + { + this.vertexNormals.Add(angles.normals[i]); + float u = angle.angle; + this.us.Add(u); + } + else + { + this.vertexNormals.Add(new Coord(angle.X, angle.Y, 0.0f)); + this.us.Add(angle.angle); + } + } + + if (hollow > 0.0f) + { + if (hollowSides == sides) + { + newVert.X *= hollow; + newVert.Y *= hollow; + newVert.Z = z; + hollowCoords.Add(newVert); + if (this.calcVertexNormals) + { + if (sides < 5) + { + hollowNormals.Add(angles.normals[i].Invert()); + } + + else + hollowNormals.Add(new Coord(-angle.X, -angle.Y, 0.0f)); + + hollowUs.Add(angle.angle * hollow); + } + } + } + else if (!simpleFace && createFaces && angle.angle > 0.0001f) + { + Face newFace = new Face(); + newFace.v1 = 0; + newFace.v2 = index; + newFace.v3 = index + 1; + + this.faces.Add(newFace); + } + index += 1; + } + + if (hasHollow) + { + hollowCoords.Reverse(); + if (this.calcVertexNormals) + { + hollowNormals.Reverse(); + hollowUs.Reverse(); + } + + if (createFaces) + { + int numOuterVerts = this.coords.Count; + int numHollowVerts = hollowCoords.Count; + int numTotalVerts = numOuterVerts + numHollowVerts; + + if (numOuterVerts == numHollowVerts) + { + Face newFace = new Face(); + + for (int coordIndex = 0; coordIndex < numOuterVerts - 1; coordIndex++) + { + newFace.v1 = coordIndex; + newFace.v2 = coordIndex + 1; + newFace.v3 = numTotalVerts - coordIndex - 1; + this.faces.Add(newFace); + + newFace.v1 = coordIndex + 1; + newFace.v2 = numTotalVerts - coordIndex - 2; + newFace.v3 = numTotalVerts - coordIndex - 1; + this.faces.Add(newFace); + } + } + else + { + if (numOuterVerts < numHollowVerts) + { + Face newFace = new Face(); + int j = 0; // j is the index for outer vertices + int maxJ = numOuterVerts - 1; + for (int i = 0; i < numHollowVerts; i++) // i is the index for inner vertices + { + if (j < maxJ) + if (angles.angles[j + 1].angle - hollowAngles.angles[i].angle <= hollowAngles.angles[i].angle - angles.angles[j].angle) + { + newFace.v1 = numTotalVerts - i - 1; + newFace.v2 = j; + newFace.v3 = j + 1; + + this.faces.Add(newFace); + j += 1; + } + + newFace.v1 = j; + newFace.v2 = numTotalVerts - i - 2; + newFace.v3 = numTotalVerts - i - 1; + + this.faces.Add(newFace); + } + } + else // numHollowVerts < numOuterVerts + { + Face newFace = new Face(); + int j = 0; // j is the index for inner vertices + int maxJ = numHollowVerts - 1; + for (int i = 0; i < numOuterVerts; i++) + { + if (j < maxJ) + if (hollowAngles.angles[j + 1].angle - angles.angles[i].angle <= angles.angles[i].angle - hollowAngles.angles[j].angle) + { + newFace.v1 = i; + newFace.v2 = numTotalVerts - j - 2; + newFace.v3 = numTotalVerts - j - 1; + + this.faces.Add(newFace); + j += 1; + } + + newFace.v1 = numTotalVerts - j - 1; + newFace.v2 = i; + newFace.v3 = i + 1; + + this.faces.Add(newFace); + } + } + } + } + + this.coords.AddRange(hollowCoords); + if (this.calcVertexNormals) + { + this.vertexNormals.AddRange(hollowNormals); + this.us.AddRange(hollowUs); + + } + } + + if (simpleFace && createFaces) + { + if (sides == 3) + this.faces.Add(new Face(0, 1, 2)); + else if (sides == 4) + { + this.faces.Add(new Face(0, 1, 2)); + this.faces.Add(new Face(0, 2, 3)); + } + } + + if (calcVertexNormals && hasProfileCut) + { + if (hasHollow) + { + int lastOuterVertIndex = this.numOuterVerts - 1; + + this.cutNormal1.X = this.coords[0].Y - this.coords[this.coords.Count - 1].Y; + this.cutNormal1.Y = -(this.coords[0].X - this.coords[this.coords.Count - 1].X); + + this.cutNormal2.X = this.coords[lastOuterVertIndex + 1].Y - this.coords[lastOuterVertIndex].Y; + this.cutNormal2.Y = -(this.coords[lastOuterVertIndex + 1].X - this.coords[lastOuterVertIndex].X); + } + + else + { + this.cutNormal1.X = this.vertexNormals[1].Y; + this.cutNormal1.Y = -this.vertexNormals[1].X; + + this.cutNormal2.X = -this.vertexNormals[this.vertexNormals.Count - 2].Y; + this.cutNormal2.Y = this.vertexNormals[this.vertexNormals.Count - 2].X; + + } + this.cutNormal1.Normalize(); + this.cutNormal2.Normalize(); + } + + this.MakeFaceUVs(); + + hollowCoords = null; + hollowNormals = null; + hollowUs = null; + + if (calcVertexNormals) + { // calculate prim face numbers + // I know it's ugly but so is the whole concept of prim face numbers + int faceNum = 1; + int startVert = hasProfileCut && !hasHollow ? 1 : 0; + if (startVert > 0) + this.faceNumbers.Add(0); + for (int i = 0; i < numOuterVerts; i++) + this.faceNumbers.Add(sides < 5 ? faceNum++ : faceNum); + if (sides > 4) + faceNum++; + if (hasProfileCut) + this.faceNumbers.Add(0); + for (int i = 0; i < numHollowVerts; i++) + this.faceNumbers.Add(faceNum++); + this.bottomFaceNumber = faceNum++; + if (hasHollow && hasProfileCut) + this.faceNumbers.Add(faceNum++); + for (int i = 0; i < this.faceNumbers.Count; i++) + if (this.faceNumbers[i] == 0) + this.faceNumbers[i] = faceNum++; + + this.numPrimFaces = faceNum; + } + + } + + internal void MakeFaceUVs() + { + this.faceUVs = new List(); + foreach (Coord c in this.coords) + this.faceUVs.Add(new UVCoord(1.0f - (0.5f + c.X), 1.0f - (0.5f - c.Y))); + } + + internal Profile Clone() + { + return this.Clone(true); + } + + internal Profile Clone(bool needFaces) + { + Profile clone = new Profile(); + + clone.coords.AddRange(this.coords); + clone.faceUVs.AddRange(this.faceUVs); + + if (needFaces) + clone.faces.AddRange(this.faces); + if ((clone.calcVertexNormals = this.calcVertexNormals) == true) + { + clone.vertexNormals.AddRange(this.vertexNormals); + clone.faceNormal = this.faceNormal; + clone.cutNormal1 = this.cutNormal1; + clone.cutNormal2 = this.cutNormal2; + clone.us.AddRange(this.us); + clone.faceNumbers.AddRange(this.faceNumbers); + } + clone.numOuterVerts = this.numOuterVerts; + clone.numHollowVerts = this.numHollowVerts; + + return clone; + } + + internal void AddPos(Coord v) + { + this.AddPos(v.X, v.Y, v.Z); + } + + internal void AddPos(float x, float y, float z) + { + int i; + int numVerts = this.coords.Count; + Coord vert; + + for (i = 0; i < numVerts; i++) + { + vert = this.coords[i]; + vert.X += x; + vert.Y += y; + vert.Z += z; + this.coords[i] = vert; + } + } + + internal void AddRot(Quat q) + { + int i; + int numVerts = this.coords.Count; + + for (i = 0; i < numVerts; i++) + this.coords[i] *= q; + + if (this.calcVertexNormals) + { + int numNormals = this.vertexNormals.Count; + for (i = 0; i < numNormals; i++) + this.vertexNormals[i] *= q; + + this.faceNormal *= q; + this.cutNormal1 *= q; + this.cutNormal2 *= q; + + } + } + + internal void Scale(float x, float y) + { + int i; + int numVerts = this.coords.Count; + Coord vert; + + for (i = 0; i < numVerts; i++) + { + vert = this.coords[i]; + vert.X *= x; + vert.Y *= y; + this.coords[i] = vert; + } + } + + /// + /// Changes order of the vertex indices and negates the center vertex normal. Does not alter vertex normals of radial vertices + /// + internal void FlipNormals() + { + int i; + int numFaces = this.faces.Count; + Face tmpFace; + int tmp; + + for (i = 0; i < numFaces; i++) + { + tmpFace = this.faces[i]; + tmp = tmpFace.v3; + tmpFace.v3 = tmpFace.v1; + tmpFace.v1 = tmp; + this.faces[i] = tmpFace; + } + + if (this.calcVertexNormals) + { + int normalCount = this.vertexNormals.Count; + if (normalCount > 0) + { + Coord n = this.vertexNormals[normalCount - 1]; + n.Z = -n.Z; + this.vertexNormals[normalCount - 1] = n; + } + } + + this.faceNormal.X = -this.faceNormal.X; + this.faceNormal.Y = -this.faceNormal.Y; + this.faceNormal.Z = -this.faceNormal.Z; + + int numfaceUVs = this.faceUVs.Count; + for (i = 0; i < numfaceUVs; i++) + { + UVCoord uv = this.faceUVs[i]; + uv.V = 1.0f - uv.V; + this.faceUVs[i] = uv; + } + } + + internal void AddValue2FaceVertexIndices(int num) + { + int numFaces = this.faces.Count; + Face tmpFace; + for (int i = 0; i < numFaces; i++) + { + tmpFace = this.faces[i]; + tmpFace.v1 += num; + tmpFace.v2 += num; + tmpFace.v3 += num; + + this.faces[i] = tmpFace; + } + } + + internal void AddValue2FaceNormalIndices(int num) + { + if (this.calcVertexNormals) + { + int numFaces = this.faces.Count; + Face tmpFace; + for (int i = 0; i < numFaces; i++) + { + tmpFace = this.faces[i]; + tmpFace.n1 += num; + tmpFace.n2 += num; + tmpFace.n3 += num; + + this.faces[i] = tmpFace; + } + } + } + + internal void DumpRaw(String path, String name, String title) + { + if (path == null) + return; + String fileName = name + "_" + title + ".raw"; + String completePath = Path.Combine(path, fileName); + StreamWriter sw = new StreamWriter(completePath); + + for (int i = 0; i < this.faces.Count; i++) + { + string s = this.coords[this.faces[i].v1].ToString(); + s += " " + this.coords[this.faces[i].v2].ToString(); + s += " " + this.coords[this.faces[i].v3].ToString(); + + sw.WriteLine(s); + } + + sw.Close(); + } + } + + public class PrimMesh + { + private const float twoPi = 2.0f * (float)Math.PI; + + public List coords; + public List normals; + public List faces; + + public List viewerFaces; + + private int sides = 4; + private int hollowSides = 4; + private float profileStart = 0.0f; + private float profileEnd = 1.0f; + private float hollow = 0.0f; + public int twistBegin = 0; + public int twistEnd = 0; + public float topShearX = 0.0f; + public float topShearY = 0.0f; + public float pathCutBegin = 0.0f; + public float pathCutEnd = 1.0f; + public float dimpleBegin = 0.0f; + public float dimpleEnd = 1.0f; + public float skew = 0.0f; + public float holeSizeX = 1.0f; // called pathScaleX in pbs + public float holeSizeY = 0.25f; + public float taperX = 0.0f; + public float taperY = 0.0f; + public float radius = 0.0f; + public float revolutions = 1.0f; + public int stepsPerRevolution = 24; + + private bool hasProfileCut = false; + private bool hasHollow = false; + public bool calcVertexNormals = false; + private bool normalsProcessed = false; + public bool viewerMode = false; + + public int numPrimFaces = 0; + + /// + /// Human readable string representation of the parameters used to create a mesh. + /// + /// + public string ParamsToDisplayString() + { + string s = ""; + s += "sides..................: " + this.sides.ToString(); + s += "\nhollowSides..........: " + this.hollowSides.ToString(); + s += "\nprofileStart.........: " + this.profileStart.ToString(); + s += "\nprofileEnd...........: " + this.profileEnd.ToString(); + s += "\nhollow...............: " + this.hollow.ToString(); + s += "\ntwistBegin...........: " + this.twistBegin.ToString(); + s += "\ntwistEnd.............: " + this.twistEnd.ToString(); + s += "\ntopShearX............: " + this.topShearX.ToString(); + s += "\ntopShearY............: " + this.topShearY.ToString(); + s += "\npathCutBegin.........: " + this.pathCutBegin.ToString(); + s += "\npathCutEnd...........: " + this.pathCutEnd.ToString(); + s += "\ndimpleBegin..........: " + this.dimpleBegin.ToString(); + s += "\ndimpleEnd............: " + this.dimpleEnd.ToString(); + s += "\nskew.................: " + this.skew.ToString(); + s += "\nholeSizeX............: " + this.holeSizeX.ToString(); + s += "\nholeSizeY............: " + this.holeSizeY.ToString(); + s += "\ntaperX...............: " + this.taperX.ToString(); + s += "\ntaperY...............: " + this.taperY.ToString(); + s += "\nradius...............: " + this.radius.ToString(); + s += "\nrevolutions..........: " + this.revolutions.ToString(); + s += "\nstepsPerRevolution...: " + this.stepsPerRevolution.ToString(); + + return s; + } + + /// + /// Constructs a PrimMesh object and creates the profile for extrusion. + /// + /// + /// + /// + /// + /// + public PrimMesh(int sides, float profileStart, float profileEnd, float hollow, int hollowSides) + { + this.coords = new List(); + this.faces = new List(); + + this.sides = sides; + this.profileStart = profileStart; + this.profileEnd = profileEnd; + this.hollow = hollow; + this.hollowSides = hollowSides; + + if (sides < 3) + this.sides = 3; + if (hollowSides < 3) + this.hollowSides = 3; + if (profileStart < 0.0f) + this.profileStart = 0.0f; + if (profileEnd > 1.0f) + this.profileEnd = 1.0f; + if (profileEnd < 0.02f) + this.profileEnd = 0.02f; + if (profileStart >= profileEnd) + this.profileStart = profileEnd - 0.02f; + if (hollow > 1.0f) + this.hollow = 1.0f; + if (hollow < 0.0f) + this.hollow = 0.0f; + + this.hasProfileCut = (this.profileStart > 0.0f || this.profileEnd < 1.0f); + this.hasHollow = (this.hollow > 0.001f); + } + + /// + /// Extrudes a profile along a straight line path. Used for prim types box, cylinder, and prism. + /// + public void ExtrudeLinear() + { + this.coords = new List(); + this.faces = new List(); + + if (this.viewerMode) + { + this.viewerFaces = new List(); + this.calcVertexNormals = true; + } + + if (this.calcVertexNormals) + this.normals = new List(); + + int step = 0; + int steps = 1; + + float length = this.pathCutEnd - this.pathCutBegin; + normalsProcessed = false; + + if (this.viewerMode && this.sides == 3) + { + // prisms don't taper well so add some vertical resolution + // other prims may benefit from this but just do prisms for now + if (Math.Abs(this.taperX) > 0.01 || Math.Abs(this.taperY) > 0.01) + steps = (int)(steps * 4.5 * length); + } + + + float twistBegin = this.twistBegin / 360.0f * twoPi; + float twistEnd = this.twistEnd / 360.0f * twoPi; + float twistTotal = twistEnd - twistBegin; + float twistTotalAbs = Math.Abs(twistTotal); + if (twistTotalAbs > 0.01f) + steps += (int)(twistTotalAbs * 3.66); // dahlia's magic number + + float start = -0.5f; + float stepSize = length / (float)steps; + float percentOfPathMultiplier = stepSize; + float xProfileScale = 1.0f; + float yProfileScale = 1.0f; + float xOffset = 0.0f; + float yOffset = 0.0f; + float zOffset = start; + float xOffsetStepIncrement = this.topShearX / steps; + float yOffsetStepIncrement = this.topShearY / steps; + + float percentOfPath = this.pathCutBegin; + zOffset += percentOfPath; + + float hollow = this.hollow; + + // sanity checks + float initialProfileRot = 0.0f; + if (this.sides == 3) + { + if (this.hollowSides == 4) + { + if (hollow > 0.7f) + hollow = 0.7f; + hollow *= 0.707f; + } + else hollow *= 0.5f; + } + else if (this.sides == 4) + { + initialProfileRot = 1.25f * (float)Math.PI; + if (this.hollowSides != 4) + hollow *= 0.707f; + } + else if (this.sides == 24 && this.hollowSides == 4) + hollow *= 1.414f; + + Profile profile = new Profile(this.sides, this.profileStart, this.profileEnd, hollow, this.hollowSides, true, calcVertexNormals); + this.numPrimFaces = profile.numPrimFaces; + + int cut1Vert = -1; + int cut2Vert = -1; + if (hasProfileCut) + { + cut1Vert = hasHollow ? profile.coords.Count - 1 : 0; + cut2Vert = hasHollow ? profile.numOuterVerts - 1 : profile.numOuterVerts; + } + + if (initialProfileRot != 0.0f) + { + profile.AddRot(new Quat(new Coord(0.0f, 0.0f, 1.0f), initialProfileRot)); + if (viewerMode) + profile.MakeFaceUVs(); + } + + + Coord lastCutNormal1 = new Coord(); + Coord lastCutNormal2 = new Coord(); + float lastV = 1.0f; + + bool done = false; + while (!done) + { + Profile newLayer = profile.Clone(); + + if (this.taperX == 0.0f) + xProfileScale = 1.0f; + else if (this.taperX > 0.0f) + xProfileScale = 1.0f - percentOfPath * this.taperX; + else xProfileScale = 1.0f + (1.0f - percentOfPath) * this.taperX; + + if (this.taperY == 0.0f) + yProfileScale = 1.0f; + else if (this.taperY > 0.0f) + yProfileScale = 1.0f - percentOfPath * this.taperY; + else yProfileScale = 1.0f + (1.0f - percentOfPath) * this.taperY; + + if (xProfileScale != 1.0f || yProfileScale != 1.0f) + newLayer.Scale(xProfileScale, yProfileScale); + + float twist = twistBegin + twistTotal * percentOfPath; + if (twist != 0.0f) + newLayer.AddRot(new Quat(new Coord(0.0f, 0.0f, 1.0f), twist)); + + newLayer.AddPos(xOffset, yOffset, zOffset); + + if (step == 0) + { + newLayer.FlipNormals(); + + // add the top faces to the viewerFaces list here + if (this.viewerMode) + { + Coord faceNormal = newLayer.faceNormal; + ViewerFace newViewerFace = new ViewerFace(0); + foreach (Face face in newLayer.faces) + { + newViewerFace.v1 = newLayer.coords[face.v1]; + newViewerFace.v2 = newLayer.coords[face.v2]; + newViewerFace.v3 = newLayer.coords[face.v3]; + + newViewerFace.n1 = faceNormal; + newViewerFace.n2 = faceNormal; + newViewerFace.n3 = faceNormal; + + newViewerFace.uv1 = newLayer.faceUVs[face.v1]; + newViewerFace.uv2 = newLayer.faceUVs[face.v2]; + newViewerFace.uv3 = newLayer.faceUVs[face.v3]; + + this.viewerFaces.Add(newViewerFace); + } + } + } + + // append this layer + + int coordsLen = this.coords.Count; + newLayer.AddValue2FaceVertexIndices(coordsLen); + + this.coords.AddRange(newLayer.coords); + + if (this.calcVertexNormals) + { + newLayer.AddValue2FaceNormalIndices(this.normals.Count); + this.normals.AddRange(newLayer.vertexNormals); + } + + if (percentOfPath < this.pathCutBegin + 0.01f || percentOfPath > this.pathCutEnd - 0.01f) + this.faces.AddRange(newLayer.faces); + + // fill faces between layers + + int numVerts = newLayer.coords.Count; + Face newFace = new Face(); + + if (step > 0) + { + int startVert = coordsLen + 1; + int endVert = this.coords.Count; + + if (sides < 5 || this.hasProfileCut || hollow > 0.0f) + startVert--; + + for (int i = startVert; i < endVert; i++) + { + int iNext = i + 1; + if (i == endVert - 1) + iNext = startVert; + + int whichVert = i - startVert; + + newFace.v1 = i; + newFace.v2 = i - numVerts; + newFace.v3 = iNext - numVerts; + this.faces.Add(newFace); + + newFace.v2 = iNext - numVerts; + newFace.v3 = iNext; + this.faces.Add(newFace); + + if (this.viewerMode) + { + // add the side faces to the list of viewerFaces here + int primFaceNum = 1; + if (whichVert >= sides) + primFaceNum = 2; + ViewerFace newViewerFace1 = new ViewerFace(primFaceNum); + ViewerFace newViewerFace2 = new ViewerFace(primFaceNum); + + float u1 = newLayer.us[whichVert]; + float u2 = 1.0f; + if (whichVert < newLayer.us.Count - 1) + u2 = newLayer.us[whichVert + 1]; + + if (whichVert == cut1Vert || whichVert == cut2Vert) + { + u1 = 0.0f; + u2 = 1.0f; + } + else if (sides < 5) + { // boxes and prisms have one texture face per side of the prim, so the U values have to be scaled + // to reflect the entire texture width + u1 *= sides; + u2 *= sides; + u2 -= (int)u1; + u1 -= (int)u1; + if (u2 < 0.1f) + u2 = 1.0f; + + //newViewerFace2.primFaceNumber = newViewerFace1.primFaceNumber = whichVert + 1; + } + + newViewerFace1.uv1.U = u1; + newViewerFace1.uv2.U = u1; + newViewerFace1.uv3.U = u2; + + newViewerFace1.uv1.V = 1.0f - percentOfPath; + newViewerFace1.uv2.V = lastV; + newViewerFace1.uv3.V = lastV; + + newViewerFace2.uv1.U = u1; + newViewerFace2.uv2.U = u2; + newViewerFace2.uv3.U = u2; + + newViewerFace2.uv1.V = 1.0f - percentOfPath; + newViewerFace2.uv2.V = lastV; + newViewerFace2.uv3.V = 1.0f - percentOfPath; + + newViewerFace1.v1 = this.coords[i]; + newViewerFace1.v2 = this.coords[i - numVerts]; + newViewerFace1.v3 = this.coords[iNext - numVerts]; + + newViewerFace2.v1 = this.coords[i]; + newViewerFace2.v2 = this.coords[iNext - numVerts]; + newViewerFace2.v3 = this.coords[iNext]; + + // profile cut faces + if (whichVert == cut1Vert) + { + newViewerFace1.n1 = newLayer.cutNormal1; + newViewerFace1.n2 = newViewerFace1.n3 = lastCutNormal1; + + newViewerFace2.n1 = newViewerFace2.n3 = newLayer.cutNormal1; + newViewerFace2.n2 = lastCutNormal1; + } + else if (whichVert == cut2Vert) + { + newViewerFace1.n1 = newLayer.cutNormal2; + newViewerFace1.n2 = newViewerFace1.n3 = lastCutNormal2; + + newViewerFace2.n1 = newViewerFace2.n3 = newLayer.cutNormal2; + newViewerFace2.n2 = lastCutNormal2; + } + + else // outer and hollow faces + { + if ((sides < 5 && whichVert < newLayer.numOuterVerts) || (hollowSides < 5 && whichVert >= newLayer.numOuterVerts)) + { + newViewerFace1.CalcSurfaceNormal(); + newViewerFace2.CalcSurfaceNormal(); + } + else + { + newViewerFace1.n1 = this.normals[i]; + newViewerFace1.n2 = this.normals[i - numVerts]; + newViewerFace1.n3 = this.normals[iNext - numVerts]; + + newViewerFace2.n1 = this.normals[i]; + newViewerFace2.n2 = this.normals[iNext - numVerts]; + newViewerFace2.n3 = this.normals[iNext]; + } + } + + newViewerFace2.primFaceNumber = newViewerFace1.primFaceNumber = newLayer.faceNumbers[whichVert]; + + this.viewerFaces.Add(newViewerFace1); + this.viewerFaces.Add(newViewerFace2); + + } + } + } + + lastCutNormal1 = newLayer.cutNormal1; + lastCutNormal2 = newLayer.cutNormal2; + lastV = 1.0f - percentOfPath; + + // calc the step for the next iteration of the loop + + if (step < steps) + { + step += 1; + percentOfPath += percentOfPathMultiplier; + xOffset += xOffsetStepIncrement; + yOffset += yOffsetStepIncrement; + zOffset += stepSize; + if (percentOfPath > this.pathCutEnd) + done = true; + } + else done = true; + + if (done && viewerMode) + { + // add the top faces to the viewerFaces list here + Coord faceNormal = newLayer.faceNormal; + ViewerFace newViewerFace = new ViewerFace(); + newViewerFace.primFaceNumber = newLayer.bottomFaceNumber; + foreach (Face face in newLayer.faces) + { + newViewerFace.v1 = newLayer.coords[face.v1 - coordsLen]; + newViewerFace.v2 = newLayer.coords[face.v2 - coordsLen]; + newViewerFace.v3 = newLayer.coords[face.v3 - coordsLen]; + + newViewerFace.n1 = faceNormal; + newViewerFace.n2 = faceNormal; + newViewerFace.n3 = faceNormal; + + newViewerFace.uv1 = newLayer.faceUVs[face.v1 - coordsLen]; + newViewerFace.uv2 = newLayer.faceUVs[face.v2 - coordsLen]; + newViewerFace.uv3 = newLayer.faceUVs[face.v3 - coordsLen]; + + this.viewerFaces.Add(newViewerFace); + } + } + } + } + + /// + /// Extrude a profile into a circular path prim mesh. Used for prim types torus, tube, and ring. + /// + public void ExtrudeCircular() + { + this.coords = new List(); + this.faces = new List(); + + if (this.viewerMode) + { + this.viewerFaces = new List(); + this.calcVertexNormals = true; + } + + if (this.calcVertexNormals) + this.normals = new List(); + + int step = 0; + int steps = 24; + + normalsProcessed = false; + + float twistBegin = this.twistBegin / 360.0f * twoPi; + float twistEnd = this.twistEnd / 360.0f * twoPi; + float twistTotal = twistEnd - twistBegin; + + // if the profile has a lot of twist, add more layers otherwise the layers may overlap + // and the resulting mesh may be quite inaccurate. This method is arbitrary and doesn't + // accurately match the viewer + float twistTotalAbs = Math.Abs(twistTotal); + if (twistTotalAbs > 0.01f) + { + if (twistTotalAbs > Math.PI * 1.5f) + steps *= 2; + if (twistTotalAbs > Math.PI * 3.0f) + steps *= 2; + } + + float yPathScale = this.holeSizeY * 0.5f; + float pathLength = this.pathCutEnd - this.pathCutBegin; + float totalSkew = this.skew * 2.0f * pathLength; + float skewStart = this.pathCutBegin * 2.0f * this.skew - this.skew; + float xOffsetTopShearXFactor = this.topShearX * (0.25f + 0.5f * (0.5f - this.holeSizeY)); + float yShearCompensation = 1.0f + Math.Abs(this.topShearY) * 0.25f; + + // It's not quite clear what pushY (Y top shear) does, but subtracting it from the start and end + // angles appears to approximate it's effects on path cut. Likewise, adding it to the angle used + // to calculate the sine for generating the path radius appears to approximate it's effects there + // too, but there are some subtle differences in the radius which are noticeable as the prim size + // increases and it may affect megaprims quite a bit. The effect of the Y top shear parameter on + // the meshes generated with this technique appear nearly identical in shape to the same prims when + // displayed by the viewer. + + float startAngle = (twoPi * this.pathCutBegin * this.revolutions) - this.topShearY * 0.9f; + float endAngle = (twoPi * this.pathCutEnd * this.revolutions) - this.topShearY * 0.9f; + float stepSize = twoPi / this.stepsPerRevolution; + + step = (int)(startAngle / stepSize); + int firstStep = step; + float angle = startAngle; + float hollow = this.hollow; + + // sanity checks + float initialProfileRot = 0.0f; + if (this.sides == 3) + { + initialProfileRot = (float)Math.PI; + if (this.hollowSides == 4) + { + if (hollow > 0.7f) + hollow = 0.7f; + hollow *= 0.707f; + } + else hollow *= 0.5f; + } + else if (this.sides == 4) + { + initialProfileRot = 0.25f * (float)Math.PI; + if (this.hollowSides != 4) + hollow *= 0.707f; + } + else if (this.sides > 4) + { + initialProfileRot = (float)Math.PI; + if (this.hollowSides == 4) + { + if (hollow > 0.7f) + hollow = 0.7f; + hollow /= 0.7f; + } + } + + bool needEndFaces = false; + if (this.pathCutBegin != 0.0f || this.pathCutEnd != 1.0f) + needEndFaces = true; + else if (this.taperX != 0.0f || this.taperY != 0.0f) + needEndFaces = true; + else if (this.skew != 0.0f) + needEndFaces = true; + else if (twistTotal != 0.0f) + needEndFaces = true; + else if (this.radius != 0.0f) + needEndFaces = true; + + Profile profile = new Profile(this.sides, this.profileStart, this.profileEnd, hollow, this.hollowSides, needEndFaces, calcVertexNormals); + this.numPrimFaces = profile.numPrimFaces; + + int cut1Vert = -1; + int cut2Vert = -1; + if (hasProfileCut) + { + cut1Vert = hasHollow ? profile.coords.Count - 1 : 0; + cut2Vert = hasHollow ? profile.numOuterVerts - 1 : profile.numOuterVerts; + } + + if (initialProfileRot != 0.0f) + { + profile.AddRot(new Quat(new Coord(0.0f, 0.0f, 1.0f), initialProfileRot)); + if (viewerMode) + profile.MakeFaceUVs(); + } + + Coord lastCutNormal1 = new Coord(); + Coord lastCutNormal2 = new Coord(); + float lastV = 1.0f; + + bool done = false; + while (!done) // loop through the length of the path and add the layers + { + bool isEndLayer = false; + if (angle <= startAngle + .01f || angle >= endAngle - .01f) + isEndLayer = true; + + //Profile newLayer = profile.Clone(isEndLayer && needEndFaces); + Profile newLayer = profile.Clone(); + + float xProfileScale = (1.0f - Math.Abs(this.skew)) * this.holeSizeX; + float yProfileScale = this.holeSizeY; + + float percentOfPath = angle / (twoPi * this.revolutions); + float percentOfAngles = (angle - startAngle) / (endAngle - startAngle); + + if (this.taperX > 0.01f) + xProfileScale *= 1.0f - percentOfPath * this.taperX; + else if (this.taperX < -0.01f) + xProfileScale *= 1.0f + (1.0f - percentOfPath) * this.taperX; + + if (this.taperY > 0.01f) + yProfileScale *= 1.0f - percentOfPath * this.taperY; + else if (this.taperY < -0.01f) + yProfileScale *= 1.0f + (1.0f - percentOfPath) * this.taperY; + + if (xProfileScale != 1.0f || yProfileScale != 1.0f) + newLayer.Scale(xProfileScale, yProfileScale); + + float radiusScale = 1.0f; + if (this.radius > 0.001f) + radiusScale = 1.0f - this.radius * percentOfPath; + else if (this.radius < 0.001f) + radiusScale = 1.0f + this.radius * (1.0f - percentOfPath); + + float twist = twistBegin + twistTotal * percentOfPath; + + float xOffset = 0.5f * (skewStart + totalSkew * percentOfAngles); + xOffset += (float)Math.Sin(angle) * xOffsetTopShearXFactor; + + float yOffset = yShearCompensation * (float)Math.Cos(angle) * (0.5f - yPathScale) * radiusScale; + + float zOffset = (float)Math.Sin(angle + this.topShearY) * (0.5f - yPathScale) * radiusScale; + + // next apply twist rotation to the profile layer + if (twistTotal != 0.0f || twistBegin != 0.0f) + newLayer.AddRot(new Quat(new Coord(0.0f, 0.0f, 1.0f), twist)); + + // now orient the rotation of the profile layer relative to it's position on the path + // adding taperY to the angle used to generate the quat appears to approximate the viewer + newLayer.AddRot(new Quat(new Coord(1.0f, 0.0f, 0.0f), angle + this.topShearY)); + newLayer.AddPos(xOffset, yOffset, zOffset); + + if (isEndLayer && angle <= startAngle + .01f) + { + newLayer.FlipNormals(); + + // add the top faces to the viewerFaces list here + if (this.viewerMode && needEndFaces) + { + Coord faceNormal = newLayer.faceNormal; + ViewerFace newViewerFace = new ViewerFace(); + newViewerFace.primFaceNumber = 0; + foreach (Face face in newLayer.faces) + { + newViewerFace.v1 = newLayer.coords[face.v1]; + newViewerFace.v2 = newLayer.coords[face.v2]; + newViewerFace.v3 = newLayer.coords[face.v3]; + + newViewerFace.n1 = faceNormal; + newViewerFace.n2 = faceNormal; + newViewerFace.n3 = faceNormal; + + newViewerFace.uv1 = newLayer.faceUVs[face.v1]; + newViewerFace.uv2 = newLayer.faceUVs[face.v2]; + newViewerFace.uv3 = newLayer.faceUVs[face.v3]; + + this.viewerFaces.Add(newViewerFace); + } + } + } + + // append the layer and fill in the sides + + int coordsLen = this.coords.Count; + newLayer.AddValue2FaceVertexIndices(coordsLen); + + this.coords.AddRange(newLayer.coords); + + if (this.calcVertexNormals) + { + newLayer.AddValue2FaceNormalIndices(this.normals.Count); + this.normals.AddRange(newLayer.vertexNormals); + } + + if (isEndLayer) + this.faces.AddRange(newLayer.faces); + + // fill faces between layers + + int numVerts = newLayer.coords.Count; + Face newFace = new Face(); + if (step > firstStep) + { + int startVert = coordsLen + 1; + int endVert = this.coords.Count; + + if (sides < 5 || this.hasProfileCut || hollow > 0.0f) + startVert--; + + for (int i = startVert; i < endVert; i++) + { + int iNext = i + 1; + if (i == endVert - 1) + iNext = startVert; + + int whichVert = i - startVert; + + newFace.v1 = i; + newFace.v2 = i - numVerts; + newFace.v3 = iNext - numVerts; + this.faces.Add(newFace); + + newFace.v2 = iNext - numVerts; + newFace.v3 = iNext; + this.faces.Add(newFace); + + if (this.viewerMode) + { + // add the side faces to the list of viewerFaces here + ViewerFace newViewerFace1 = new ViewerFace(); + ViewerFace newViewerFace2 = new ViewerFace(); + float u1 = newLayer.us[whichVert]; + float u2 = 1.0f; + if (whichVert < newLayer.us.Count - 1) + u2 = newLayer.us[whichVert + 1]; + + if (whichVert == cut1Vert || whichVert == cut2Vert) + { + u1 = 0.0f; + u2 = 1.0f; + } + else if (sides < 5) + { // boxes and prisms have one texture face per side of the prim, so the U values have to be scaled + // to reflect the entire texture width + u1 *= sides; + u2 *= sides; + u2 -= (int)u1; + u1 -= (int)u1; + if (u2 < 0.1f) + u2 = 1.0f; + + //newViewerFace2.primFaceNumber = newViewerFace1.primFaceNumber = whichVert + 1; + } + + newViewerFace1.uv1.U = u1; + newViewerFace1.uv2.U = u1; + newViewerFace1.uv3.U = u2; + + newViewerFace1.uv1.V = 1.0f - percentOfPath; + newViewerFace1.uv2.V = lastV; + newViewerFace1.uv3.V = lastV; + + newViewerFace2.uv1.U = u1; + newViewerFace2.uv2.U = u2; + newViewerFace2.uv3.U = u2; + + newViewerFace2.uv1.V = 1.0f - percentOfPath; + newViewerFace2.uv2.V = lastV; + newViewerFace2.uv3.V = 1.0f - percentOfPath; + + newViewerFace1.v1 = this.coords[i]; + newViewerFace1.v2 = this.coords[i - numVerts]; + newViewerFace1.v3 = this.coords[iNext - numVerts]; + + newViewerFace2.v1 = this.coords[i]; + newViewerFace2.v2 = this.coords[iNext - numVerts]; + newViewerFace2.v3 = this.coords[iNext]; + + // profile cut faces + if (whichVert == cut1Vert) + { + newViewerFace1.n1 = newLayer.cutNormal1; + newViewerFace1.n2 = newViewerFace1.n3 = lastCutNormal1; + + newViewerFace2.n1 = newViewerFace2.n3 = newLayer.cutNormal1; + newViewerFace2.n2 = lastCutNormal1; + } + else if (whichVert == cut2Vert) + { + newViewerFace1.n1 = newLayer.cutNormal2; + newViewerFace1.n2 = newViewerFace1.n3 = lastCutNormal2; + + newViewerFace2.n1 = newViewerFace2.n3 = newLayer.cutNormal2; + newViewerFace2.n2 = lastCutNormal2; + } + else // periphery faces + { + if (sides < 5 && whichVert < newLayer.numOuterVerts) + { + newViewerFace1.n1 = this.normals[i]; + newViewerFace1.n2 = this.normals[i - numVerts]; + newViewerFace1.n3 = this.normals[i - numVerts]; + + newViewerFace2.n1 = this.normals[i]; + newViewerFace2.n2 = this.normals[i - numVerts]; + newViewerFace2.n3 = this.normals[i]; + } + else if (hollowSides < 5 && whichVert >= newLayer.numOuterVerts) + { + newViewerFace1.n1 = this.normals[iNext]; + newViewerFace1.n2 = this.normals[iNext - numVerts]; + newViewerFace1.n3 = this.normals[iNext - numVerts]; + + newViewerFace2.n1 = this.normals[iNext]; + newViewerFace2.n2 = this.normals[iNext - numVerts]; + newViewerFace2.n3 = this.normals[iNext]; + } + else + { + newViewerFace1.n1 = this.normals[i]; + newViewerFace1.n2 = this.normals[i - numVerts]; + newViewerFace1.n3 = this.normals[iNext - numVerts]; + + newViewerFace2.n1 = this.normals[i]; + newViewerFace2.n2 = this.normals[iNext - numVerts]; + newViewerFace2.n3 = this.normals[iNext]; + } + } + + newViewerFace1.primFaceNumber = newViewerFace2.primFaceNumber = newLayer.faceNumbers[whichVert]; + this.viewerFaces.Add(newViewerFace1); + this.viewerFaces.Add(newViewerFace2); + + } + } + } + + lastCutNormal1 = newLayer.cutNormal1; + lastCutNormal2 = newLayer.cutNormal2; + lastV = 1.0f - percentOfPath; + + // calculate terms for next iteration + // calculate the angle for the next iteration of the loop + + if (angle >= endAngle) + done = true; + else + { + step += 1; + angle = stepSize * step; + if (angle > endAngle) + angle = endAngle; + } + + if (done && viewerMode && needEndFaces) + { + // add the bottom faces to the viewerFaces list here + Coord faceNormal = newLayer.faceNormal; + ViewerFace newViewerFace = new ViewerFace(); + newViewerFace.primFaceNumber = newLayer.bottomFaceNumber; + foreach (Face face in newLayer.faces) + { + newViewerFace.v1 = newLayer.coords[face.v1 - coordsLen]; + newViewerFace.v2 = newLayer.coords[face.v2 - coordsLen]; + newViewerFace.v3 = newLayer.coords[face.v3 - coordsLen]; + + newViewerFace.n1 = faceNormal; + newViewerFace.n2 = faceNormal; + newViewerFace.n3 = faceNormal; + + newViewerFace.uv1 = newLayer.faceUVs[face.v1 - coordsLen]; + newViewerFace.uv2 = newLayer.faceUVs[face.v2 - coordsLen]; + newViewerFace.uv3 = newLayer.faceUVs[face.v3 - coordsLen]; + + this.viewerFaces.Add(newViewerFace); + } + } + } + } + + private Coord SurfaceNormal(Coord c1, Coord c2, Coord c3) + { + Coord edge1 = new Coord(c2.X - c1.X, c2.Y - c1.Y, c2.Z - c1.Z); + Coord edge2 = new Coord(c3.X - c1.X, c3.Y - c1.Y, c3.Z - c1.Z); + + Coord normal = Coord.Cross(edge1, edge2); + + normal.Normalize(); + + return normal; + } + + private Coord SurfaceNormal(Face face) + { + return SurfaceNormal(this.coords[face.v1], this.coords[face.v2], this.coords[face.v3]); + } + + /// + /// Calculate the surface normal for a face in the list of faces + /// + /// + /// + public Coord SurfaceNormal(int faceIndex) + { + int numFaces = this.faces.Count; + if (faceIndex < 0 || faceIndex >= numFaces) + throw new Exception("faceIndex out of range"); + + return SurfaceNormal(this.faces[faceIndex]); + } + + /// + /// Calculate surface normals for all of the faces in the list of faces in this mesh + /// + public void CalcNormals() + { + if (normalsProcessed) + return; + + normalsProcessed = true; + + int numFaces = faces.Count; + + if (!this.calcVertexNormals) + this.normals = new List(); + + for (int i = 0; i < numFaces; i++) + { + Face face = faces[i]; + + this.normals.Add(SurfaceNormal(i).Normalize()); + + int normIndex = normals.Count - 1; + face.n1 = normIndex; + face.n2 = normIndex; + face.n3 = normIndex; + + this.faces[i] = face; + } + } + + /// + /// Adds a value to each XYZ vertex coordinate in the mesh + /// + /// + /// + /// + public void AddPos(float x, float y, float z) + { + int i; + int numVerts = this.coords.Count; + Coord vert; + + for (i = 0; i < numVerts; i++) + { + vert = this.coords[i]; + vert.X += x; + vert.Y += y; + vert.Z += z; + this.coords[i] = vert; + } + } + + /// + /// Rotates the mesh + /// + /// + public void AddRot(Quat q) + { + int i; + int numVerts = this.coords.Count; + + for (i = 0; i < numVerts; i++) + this.coords[i] *= q; + + if (this.normals != null) + { + int numNormals = this.normals.Count; + for (i = 0; i < numNormals; i++) + this.normals[i] *= q; + } + + if (this.viewerFaces != null) + { + int numViewerFaces = this.viewerFaces.Count; + + for (i = 0; i < numViewerFaces; i++) + { + ViewerFace v = this.viewerFaces[i]; + v.v1 *= q; + v.v2 *= q; + v.v3 *= q; + + v.n1 *= q; + v.n2 *= q; + v.n3 *= q; + this.viewerFaces[i] = v; + } + } + + } + + /// + /// Scales the mesh + /// + /// + /// + /// + public void Scale(float x, float y, float z) + { + int i; + int numVerts = this.coords.Count; + //Coord vert; + + Coord m = new Coord(x, y, z); + for (i = 0; i < numVerts; i++) + this.coords[i] *= m; + + if (this.viewerFaces != null) + { + int numViewerFaces = this.viewerFaces.Count; + for (i = 0; i < numViewerFaces; i++) + { + ViewerFace v = this.viewerFaces[i]; + v.v1 *= m; + v.v2 *= m; + v.v3 *= m; + this.viewerFaces[i] = v; + } + + } + + } + + /// + /// Dumps the mesh to a Blender compatible "Raw" format file + /// + /// + /// + /// + public void DumpRaw(String path, String name, String title) + { + if (path == null) + return; + String fileName = name + "_" + title + ".raw"; + String completePath = Path.Combine(path, fileName); + StreamWriter sw = new StreamWriter(completePath); + + for (int i = 0; i < this.faces.Count; i++) + { + string s = this.coords[this.faces[i].v1].ToString(); + s += " " + this.coords[this.faces[i].v2].ToString(); + s += " " + this.coords[this.faces[i].v3].ToString(); + + sw.WriteLine(s); + } + + sw.Close(); + } + } +} diff --git a/OpenSim/Region/Physics/Meshing/SculptMesh.cs b/OpenSim/Region/Physics/Meshing/SculptMesh.cs new file mode 100644 index 0000000000..312f89aa47 --- /dev/null +++ b/OpenSim/Region/Physics/Meshing/SculptMesh.cs @@ -0,0 +1,343 @@ +/* + * Copyright (c) Contributors + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Drawing; +using System.Drawing.Imaging; + +namespace PrimMesher +{ + + public class SculptMesh + { + public List coords; + public List faces; + + public List viewerFaces; + public List normals; + public List uvs; + + public enum SculptType { sphere = 1, torus = 2, plane = 3, cylinder = 4 }; + private const float pixScale = 0.00390625f; // 1.0 / 256 + + private Bitmap ScaleImage(Bitmap srcImage, float scale) + { + int sourceWidth = srcImage.Width; + int sourceHeight = srcImage.Height; + int sourceX = 0; + int sourceY = 0; + + int destX = 0; + int destY = 0; + int destWidth = (int)(sourceWidth * scale); + int destHeight = (int)(sourceHeight * scale); + + Bitmap scaledImage = new Bitmap(destWidth, destHeight, + PixelFormat.Format24bppRgb); + scaledImage.SetResolution(srcImage.HorizontalResolution, + srcImage.VerticalResolution); + + Graphics grPhoto = Graphics.FromImage(scaledImage); + grPhoto.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear; + + grPhoto.DrawImage(srcImage, + new Rectangle(destX, destY, destWidth, destHeight), + new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight), + GraphicsUnit.Pixel); + + grPhoto.Dispose(); + return scaledImage; + } + + public SculptMesh SculptMeshFromFile(string fileName, SculptType sculptType, int lod, bool viewerMode) + { + Bitmap bitmap = (Bitmap)Bitmap.FromFile(fileName); + SculptMesh sculptMesh = new SculptMesh(bitmap, sculptType, lod, viewerMode); + bitmap.Dispose(); + return sculptMesh; + } + + public SculptMesh(Bitmap sculptBitmap, SculptType sculptType, int lod, bool viewerMode) + { + coords = new List(); + faces = new List(); + normals = new List(); + uvs = new List(); + + float sourceScaleFactor = (float)lod / (float)Math.Max(sculptBitmap.Width, sculptBitmap.Height); + bool scaleSourceImage = sourceScaleFactor < 1.0f ? true : false; + + Bitmap bitmap; + if (scaleSourceImage) + bitmap = ScaleImage(sculptBitmap, sourceScaleFactor); + else + bitmap = sculptBitmap; + + viewerFaces = new List(); + + int width = bitmap.Width; + int height = bitmap.Height; + + float widthUnit = 1.0f / width; + float heightUnit = 1.0f / (height - 1); + + int p1, p2, p3, p4; + Color color; + float x, y, z; + + int imageX, imageY; + + if (sculptType == SculptType.sphere) + { // average the top and bottom row pixel values so the resulting vertices appear to converge + int lastRow = height - 1; + int r1 = 0, g1 = 0, b1 = 0; + int r2 = 0, g2 = 0, b2 = 0; + for (imageX = 0; imageX < width; imageX++) + { + Color c1 = bitmap.GetPixel(imageX, 0); + Color c2 = bitmap.GetPixel(imageX, lastRow); + + r1 += c1.R; + g1 += c1.G; + b1 += c1.B; + + r2 += c2.R; + g2 += c2.G; + b2 += c2.B; + } + + Color newC1 = Color.FromArgb(r1 / width, g1 / width, b1 / width); + Color newC2 = Color.FromArgb(r2 / width, g2 / width, b2 / width); + + for (imageX = 0; imageX < width; imageX++) + { + bitmap.SetPixel(imageX, 0, newC1); + bitmap.SetPixel(imageX, lastRow, newC2); + } + } + + + int pixelsAcross = sculptType == SculptType.plane ? width : width + 1; + int pixelsDown = sculptType == SculptType.sphere || sculptType == SculptType.cylinder ? height + 1 : height; + + for (imageY = 0; imageY < pixelsDown; imageY++) + { + int rowOffset = imageY * width; + + for (imageX = 0; imageX < pixelsAcross; imageX++) + { + /* + * p1-----p2 + * | \ f2 | + * | \ | + * | f1 \| + * p3-----p4 + */ + + if (imageX < width) + { + p4 = rowOffset + imageX; + p3 = p4 - 1; + } + else + { + p4 = rowOffset; // wrap around to beginning + p3 = rowOffset + imageX - 1; + } + + p2 = p4 - width; + p1 = p3 - width; + + color = bitmap.GetPixel(imageX == width ? 0 : imageX, imageY == height ? height - 1 : imageY); + + x = (color.R - 128) * pixScale; + y = (color.G - 128) * pixScale; + z = (color.B - 128) * pixScale; + + Coord c = new Coord(x, y, z); + this.coords.Add(c); + if (viewerMode) + { + this.normals.Add(new Coord()); + this.uvs.Add(new UVCoord(widthUnit * imageX, heightUnit * imageY)); + } + + if (imageY > 0 && imageX > 0) + { + Face f1, f2; + + if (viewerMode) + { + f1 = new Face(p1, p3, p4, p1, p3, p4); + f1.uv1 = p1; + f1.uv2 = p3; + f1.uv3 = p4; + + f2 = new Face(p1, p4, p2, p1, p4, p2); + f2.uv1 = p1; + f2.uv2 = p4; + f2.uv3 = p2; + } + else + { + f1 = new Face(p1, p3, p4); + f2 = new Face(p1, p4, p2); + } + + this.faces.Add(f1); + this.faces.Add(f2); + } + } + } + + if (scaleSourceImage) + bitmap.Dispose(); + + if (viewerMode) + { // compute vertex normals by summing all the surface normals of all the triangles sharing + // each vertex and then normalizing + int numFaces = this.faces.Count; + for (int i = 0; i < numFaces; i++) + { + Face face = this.faces[i]; + Coord surfaceNormal = face.SurfaceNormal(this.coords); + this.normals[face.v1] += surfaceNormal; + this.normals[face.v2] += surfaceNormal; + this.normals[face.v3] += surfaceNormal; + } + + int numCoords = this.coords.Count; + for (int i = 0; i < numCoords; i++) + this.coords[i].Normalize(); + + if (sculptType != SculptType.plane) + { // blend the vertex normals at the cylinder seam + pixelsAcross = width + 1; + for (imageY = 0; imageY < height; imageY++) + { + int rowOffset = imageY * pixelsAcross; + + this.normals[rowOffset] = this.normals[rowOffset + width - 1] = (this.normals[rowOffset] + this.normals[rowOffset + width - 1]).Normalize(); + } + } + + foreach (Face face in this.faces) + { + ViewerFace vf = new ViewerFace(0); + vf.v1 = this.coords[face.v1]; + vf.v2 = this.coords[face.v2]; + vf.v3 = this.coords[face.v3]; + + vf.n1 = this.normals[face.n1]; + vf.n2 = this.normals[face.n2]; + vf.n3 = this.normals[face.n3]; + + vf.uv1 = this.uvs[face.uv1]; + vf.uv2 = this.uvs[face.uv2]; + vf.uv3 = this.uvs[face.uv3]; + + this.viewerFaces.Add(vf); + } + } + } + + public void AddRot(Quat q) + { + int i; + int numVerts = this.coords.Count; + + for (i = 0; i < numVerts; i++) + this.coords[i] *= q; + + if (this.viewerFaces != null) + { + int numViewerFaces = this.viewerFaces.Count; + + for (i = 0; i < numViewerFaces; i++) + { + ViewerFace v = this.viewerFaces[i]; + v.v1 *= q; + v.v2 *= q; + v.v3 *= q; + + v.n1 *= q; + v.n2 *= q; + v.n3 *= q; + + this.viewerFaces[i] = v; + } + } + } + + public void Scale(float x, float y, float z) + { + int i; + int numVerts = this.coords.Count; + //Coord vert; + + Coord m = new Coord(x, y, z); + for (i = 0; i < numVerts; i++) + this.coords[i] *= m; + + if (this.viewerFaces != null) + { + int numViewerFaces = this.viewerFaces.Count; + for (i = 0; i < numViewerFaces; i++) + { + ViewerFace v = this.viewerFaces[i]; + v.v1 *= m; + v.v2 *= m; + v.v3 *= m; + this.viewerFaces[i] = v; + } + } + } + + public void DumpRaw(String path, String name, String title) + { + if (path == null) + return; + String fileName = name + "_" + title + ".raw"; + String completePath = Path.Combine(path, fileName); + StreamWriter sw = new StreamWriter(completePath); + + for (int i = 0; i < this.faces.Count; i++) + { + string s = this.coords[this.faces[i].v1].ToString(); + s += " " + this.coords[this.faces[i].v2].ToString(); + s += " " + this.coords[this.faces[i].v3].ToString(); + + sw.WriteLine(s); + } + + sw.Close(); + } + } +} diff --git a/bin/Physics/PrimMesher.dll b/bin/Physics/PrimMesher.dll deleted file mode 100644 index 74117e05f4fa620787258894222d8ea0180b9644..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 -