committed miter hair segment code from Steve
parent
906f5910fd
commit
8c0e6e3d18
|
@ -491,7 +491,7 @@ plugins += env.SharedLibrary('plugins/obj', ['src/shapes/obj.cpp'])
|
|||
plugins += env.SharedLibrary('plugins/serialized', ['src/shapes/serialized.cpp'])
|
||||
plugins += env.SharedLibrary('plugins/sphere', ['src/shapes/sphere.cpp'])
|
||||
plugins += env.SharedLibrary('plugins/cylinder', ['src/shapes/cylinder.cpp'])
|
||||
plugins += env.SharedLibrary('plugins/hair', ['src/shapes/hair.cpp'])
|
||||
plugins += env.SharedLibrary('plugins/hair', ['src/shapes/hair.cpp', 'src/shapes/miterseg.cpp'])
|
||||
#plugins += env.SharedLibrary('plugins/group', ['src/shapes/group.cpp'])
|
||||
|
||||
# Samplers
|
||||
|
|
|
@ -1,132 +1,98 @@
|
|||
#include <fstream>
|
||||
|
||||
#include <mitsuba/render/shape.h>
|
||||
#include <mitsuba/core/plugin.h>
|
||||
#include <mitsuba/core/fresolver.h>
|
||||
#include <fstream>
|
||||
|
||||
#include "hair.h"
|
||||
#include "miterseg.h"
|
||||
|
||||
MTS_NAMESPACE_BEGIN
|
||||
|
||||
/**
|
||||
* The 'Hair' primitive consists of a list of hair segments, which are
|
||||
* rasterized into cylinders and spheres. The file format is simply a
|
||||
* list of lines of the form "x y z" where an empty line indicates the beginning
|
||||
* of a new hair.
|
||||
*/
|
||||
class Hair : public Shape {
|
||||
struct HairSegment {
|
||||
Point start, end;
|
||||
Hair::Hair(const Properties &props) : Shape(props) {
|
||||
std::string filename = props.getString("filename");
|
||||
m_radius = (Float) props.getFloat("radius", 0.05f);
|
||||
m_name = FileResolver::getInstance()->resolve(filename);
|
||||
|
||||
inline HairSegment(Point start, Point end)
|
||||
: start(start), end(end) {
|
||||
}
|
||||
};
|
||||
Log(EInfo, "Loading hair geometry from \"%s\" ..", m_name.c_str());
|
||||
|
||||
Float m_radius;
|
||||
std::vector<HairSegment> m_segments;
|
||||
public:
|
||||
Hair(const Properties &props) : Shape(props) {
|
||||
std::string filename = props.getString("filename");
|
||||
m_radius = (Float) props.getFloat("radius", 0.05f);
|
||||
m_name = FileResolver::getInstance()->resolve(filename);
|
||||
std::ifstream is(m_name.c_str());
|
||||
if (is.fail())
|
||||
Log(EError, "Could not open \"%s\"!", m_name.c_str());
|
||||
|
||||
Log(EInfo, "Loading hair geometry from \"%s\" ..", m_name.c_str());
|
||||
|
||||
std::ifstream is(m_name.c_str());
|
||||
if (is.fail())
|
||||
Log(EError, "Could not open \"%s\"!", m_name.c_str());
|
||||
|
||||
std::string line;
|
||||
int segments = 0;
|
||||
Point p, prev;
|
||||
while (is.good()) {
|
||||
std::getline(is, line);
|
||||
if (line.length() > 0 && line[0] == '#')
|
||||
continue;
|
||||
if (line.length() == 0) {
|
||||
segments = 0;
|
||||
} else {
|
||||
std::istringstream iss(line);
|
||||
iss >> p.x >> p.y >> p.z;
|
||||
m_aabb.expandBy(m_objectToWorld(p));
|
||||
|
||||
if (segments++ > 0)
|
||||
m_segments.push_back(HairSegment(prev, p));
|
||||
|
||||
prev = p;
|
||||
}
|
||||
}
|
||||
m_aabb.min -= Vector(m_radius, m_radius, m_radius);
|
||||
m_aabb.max += Vector(m_radius, m_radius, m_radius);
|
||||
|
||||
Log(EDebug, "Read %i hair segments.", m_segments.size());
|
||||
}
|
||||
|
||||
Hair(Stream *stream, InstanceManager *manager) : Shape(stream, manager) {
|
||||
m_radius = stream->readFloat();
|
||||
|
||||
size_t segmentCount = stream->readUInt();
|
||||
m_segments.reserve(segmentCount);
|
||||
for (size_t i=0; i<segmentCount; ++i)
|
||||
m_segments.push_back(HairSegment(Point(stream), Point(stream)));
|
||||
}
|
||||
|
||||
void serialize(Stream *stream, InstanceManager *manager) const {
|
||||
Shape::serialize(stream, manager);
|
||||
|
||||
stream->writeFloat(m_radius);
|
||||
stream->writeUInt(m_segments.size());
|
||||
for (size_t i=0; i<m_segments.size(); ++i) {
|
||||
m_segments[i].start.serialize(stream);
|
||||
m_segments[i].end.serialize(stream);
|
||||
}
|
||||
}
|
||||
|
||||
bool isCompound() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
Shape *getElement(int _index) {
|
||||
unsigned int index = _index / 2;
|
||||
if (index >= m_segments.size())
|
||||
return NULL;
|
||||
|
||||
Point start = m_segments[index].start;
|
||||
Point end = m_segments[index].end;
|
||||
Float length = (end-start).length();
|
||||
|
||||
Vector axis = normalize(end-start);
|
||||
Vector rotAxis = normalize(cross(Vector(0,0,1), axis));
|
||||
Float rotAngle = radToDeg(std::acos(axis.z));
|
||||
|
||||
Transform trafo =
|
||||
m_objectToWorld
|
||||
* Transform::translate(start)
|
||||
* Transform::rotate(rotAxis, rotAngle);
|
||||
|
||||
if ((_index % 2) == 0) {
|
||||
Properties sphereProperties("sphere");
|
||||
sphereProperties.setFloat("radius", m_radius);
|
||||
sphereProperties.setTransform("toWorld", trafo);
|
||||
Shape *sphere = static_cast<Shape *> (PluginManager::getInstance()->
|
||||
createObject(Shape::m_theClass, sphereProperties));
|
||||
sphere->addChild("bsdf", m_bsdf);
|
||||
sphere->configure();
|
||||
return sphere;
|
||||
std::string line;
|
||||
bool newFiber = true;
|
||||
Point p;
|
||||
while (is.good()) {
|
||||
std::getline(is, line);
|
||||
if (line.length() > 0 && line[0] == '#')
|
||||
continue;
|
||||
if (line.length() == 0) {
|
||||
newFiber = true;
|
||||
} else {
|
||||
Properties cylinderProperties("cylinder");
|
||||
cylinderProperties.setFloat("radius", m_radius);
|
||||
cylinderProperties.setFloat("length", length);
|
||||
cylinderProperties.setTransform("toWorld", trafo);
|
||||
Shape *cylinder = static_cast<Shape *> (PluginManager::getInstance()->
|
||||
createObject(Shape::m_theClass, cylinderProperties));
|
||||
cylinder->addChild("bsdf", m_bsdf);
|
||||
cylinder->configure();
|
||||
|
||||
return cylinder;
|
||||
std::istringstream iss(line);
|
||||
iss >> p.x >> p.y >> p.z;
|
||||
m_vertices.push_back(p);
|
||||
m_startFiber.push_back(newFiber);
|
||||
newFiber = false;
|
||||
}
|
||||
}
|
||||
m_startFiber.push_back(true);
|
||||
|
||||
buildSegIndex();
|
||||
|
||||
Log(EDebug, "Read %i hair vertices, %i segments,", m_vertices.size(), m_segIndex.size());
|
||||
}
|
||||
|
||||
Hair::Hair(Stream *stream, InstanceManager *manager) : Shape(stream, manager) {
|
||||
m_radius = stream->readFloat();
|
||||
size_t segmentCount = stream->readUInt();
|
||||
|
||||
m_vertices.reserve(segmentCount);
|
||||
for (size_t i=0; i<segmentCount; ++i)
|
||||
m_vertices.push_back(Point(stream));
|
||||
|
||||
m_startFiber.reserve(segmentCount);
|
||||
for (size_t i=0; i<segmentCount; ++i)
|
||||
m_startFiber.push_back(stream->readBool());
|
||||
|
||||
buildSegIndex();
|
||||
}
|
||||
|
||||
void Hair::serialize(Stream *stream, InstanceManager *manager) const {
|
||||
Shape::serialize(stream, manager);
|
||||
|
||||
stream->writeFloat(m_radius);
|
||||
size_t segmentCount = m_vertices.size();
|
||||
stream->writeUInt(segmentCount);
|
||||
|
||||
for (size_t i=0; i<segmentCount; ++i)
|
||||
m_vertices[i].serialize(stream);
|
||||
|
||||
for (size_t i=0; i<segmentCount; ++i)
|
||||
stream->writeBool(m_startFiber[i]);
|
||||
}
|
||||
|
||||
Shape *Hair::getElement(int index) {
|
||||
if ((size_t) index >= m_segIndex.size())
|
||||
return NULL;
|
||||
|
||||
MiterHairSegment *segment = new MiterHairSegment(this, m_segIndex[index]);
|
||||
segment->addChild("bsdf", m_bsdf);
|
||||
segment->configure();
|
||||
return segment;
|
||||
}
|
||||
|
||||
|
||||
void Hair::buildSegIndex() {
|
||||
// Compute the index of the first vertex in each segment.
|
||||
m_segIndex.clear();
|
||||
for (size_t i=0; i<m_vertices.size(); i++)
|
||||
if (!m_startFiber[i+1])
|
||||
m_segIndex.push_back(i);
|
||||
}
|
||||
|
||||
|
||||
MTS_DECLARE_CLASS()
|
||||
};
|
||||
|
||||
MTS_IMPLEMENT_CLASS_S(Hair, false, Shape)
|
||||
MTS_EXPORT_PLUGIN(Hair, "Hair geometry");
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
#ifndef HAIR_H_
|
||||
#define HAIR_H_
|
||||
|
||||
#include <mitsuba/render/shape.h>
|
||||
|
||||
MTS_NAMESPACE_BEGIN
|
||||
|
||||
|
||||
class Hair : public Shape {
|
||||
|
||||
// The radius of the hair fibers (all fibers have constant radius)
|
||||
Float m_radius;
|
||||
|
||||
// The vertices of the hair: [0...nVertices)
|
||||
std::vector<Point> m_vertices;
|
||||
|
||||
// An indication of which vertices start a new fiber: [0...nVertices+1)
|
||||
std::vector<bool> m_startFiber;
|
||||
|
||||
// A mapping of segment indices to vertex indices (needed only for construction): [0...nSegments)
|
||||
std::vector<int> m_segIndex;
|
||||
|
||||
public:
|
||||
|
||||
Hair(const Properties &props);
|
||||
|
||||
Hair(Stream *stream, InstanceManager *manager);
|
||||
|
||||
void serialize(Stream *stream, InstanceManager *manager) const;
|
||||
|
||||
bool isCompound() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
Shape *getElement(int index);
|
||||
|
||||
Float radius() const { return m_radius; }
|
||||
|
||||
Point vertex(int iv) const { return m_vertices[iv]; }
|
||||
void vertex(int iv, Point &p) const { p = m_vertices[iv]; }
|
||||
|
||||
bool vertexStartsFiber(int iv) const { return m_startFiber[iv]; }
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
void buildSegIndex();
|
||||
|
||||
MTS_DECLARE_CLASS()
|
||||
};
|
||||
|
||||
|
||||
MTS_NAMESPACE_END
|
||||
|
||||
|
||||
#endif /* HAIR_H_ */
|
|
@ -0,0 +1,223 @@
|
|||
#include <mitsuba/render/shape.h>
|
||||
#include "miterseg.h"
|
||||
|
||||
MTS_NAMESPACE_BEGIN
|
||||
|
||||
MiterHairSegment::MiterHairSegment(ref<Hair> parent, int iv) : Shape(Properties()) {
|
||||
m_hair = parent;
|
||||
m_iv = iv;
|
||||
configure();
|
||||
}
|
||||
|
||||
|
||||
void MiterHairSegment::configure() {
|
||||
Shape::configure();
|
||||
|
||||
m_aabb.reset(); m_bsphere.radius = 0;
|
||||
|
||||
const Point start = firstVertex();
|
||||
const Point end = secondVertex();
|
||||
const Vector segment = end - start;
|
||||
|
||||
// Caution this is false for collapsing segments (where the intersection of the miter planes intersects the cylinder)
|
||||
m_surfaceArea = 2*M_PI * m_hair->radius() * segment.length();
|
||||
m_invSurfaceArea = 1.0f / m_surfaceArea;
|
||||
|
||||
m_aabb = getWorldAABB(0, 1);
|
||||
m_bsphere.center = m_aabb.getCenter();
|
||||
for (int i=0; i<8; ++i)
|
||||
m_bsphere.expandBy(m_aabb.getCorner(i));
|
||||
}
|
||||
|
||||
|
||||
AABB MiterHairSegment::getWorldAABB(Float t0, Float t1) const {
|
||||
AABB result;
|
||||
|
||||
// The bounding box is conservatively the bbox of two spheres at the
|
||||
// endpoints of the segment. Each sphere's radius is the hair
|
||||
// radius divided by the cosine of the steepest miter angle, making
|
||||
// it a bounding sphere for the ellipsoidal boundary for that end
|
||||
// of the segment.
|
||||
// Side note: There is a possible problem here, that the miter angle
|
||||
// can get arbitrarily steep if two segments form a very sharp angle.
|
||||
// This may need to be addressed at some point.
|
||||
|
||||
const Point start = firstVertex();
|
||||
const Point end = secondVertex();
|
||||
const Vector segment = end - start;
|
||||
|
||||
const Point a = start + t0 * segment;
|
||||
const Point b = start + t1 * segment;
|
||||
|
||||
// cosine of steepest miter angle
|
||||
const Float cos0 = dot(firstMiterNormal(), tangent());
|
||||
const Float cos1 = dot(secondMiterNormal(), tangent());
|
||||
const Float maxInvCos = 1.0 / std::min(cos0, cos1);
|
||||
|
||||
const Float expandRadius = m_hair->radius() * maxInvCos;
|
||||
const Vector expandVec(expandRadius, expandRadius, expandRadius);
|
||||
|
||||
result.expandBy(a - expandVec);
|
||||
result.expandBy(a + expandVec);
|
||||
result.expandBy(b - expandVec);
|
||||
result.expandBy(b + expandVec);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AABB MiterHairSegment::getClippedAABB(const AABB &aabb) const {
|
||||
AABB result(m_aabb);
|
||||
result.clip(aabb);
|
||||
return result;
|
||||
/* The following is broken, I believe...
|
||||
Float nearT, farT;
|
||||
AABB result(m_aabb);
|
||||
result.clip(aabb);
|
||||
|
||||
Point a = firstVertex();
|
||||
Point b = secondVertex();
|
||||
|
||||
if (!result.rayIntersect(Ray(a, normalize(b-a)), nearT, farT))
|
||||
return result; // that could be improved
|
||||
|
||||
nearT = std::max(nearT, (Float) 0);
|
||||
farT = std::min(farT, m_length);
|
||||
result = getWorldAABB(nearT, farT);
|
||||
result.clip(aabb);
|
||||
|
||||
return result;*/
|
||||
}
|
||||
|
||||
bool MiterHairSegment::rayIntersect(const Ray &ray, Float start, Float end, Float &t) const {
|
||||
Float nearT, farT;
|
||||
|
||||
/* First compute the intersection with the infinite cylinder */
|
||||
|
||||
// Projection of ray onto subspace normal to axis
|
||||
Vector axis = tangent();
|
||||
Vector relOrigin = ray.o - firstVertex();
|
||||
Vector projOrigin = relOrigin - dot(axis, relOrigin) * axis;
|
||||
Vector projDirection = ray.d - dot(axis, ray.d) * axis;
|
||||
|
||||
// Quadratic to intersect circle in projection
|
||||
const Float A = projDirection.lengthSquared();
|
||||
const Float B = 2 * dot(projOrigin, projDirection);
|
||||
const Float radius = m_hair->radius();
|
||||
const Float C = projOrigin.lengthSquared() - radius*radius;
|
||||
|
||||
if (!solveQuadratic(A, B, C, nearT, farT))
|
||||
return false;
|
||||
|
||||
if (nearT > end || farT < start)
|
||||
return false;
|
||||
|
||||
/* Next check the intersection points against the miter planes */
|
||||
|
||||
Point pointNear = ray(nearT);
|
||||
Point pointFar = ray(farT);
|
||||
if (dot(pointNear - firstVertex(), firstMiterNormal()) >= 0 &&
|
||||
dot(pointNear - secondVertex(), secondMiterNormal()) <= 0 &&
|
||||
nearT >= start) {
|
||||
t = nearT;
|
||||
} else if (dot(pointFar - firstVertex(), firstMiterNormal()) >= 0 &&
|
||||
dot(pointFar - secondVertex(), secondMiterNormal()) <= 0) {
|
||||
if (farT > end)
|
||||
return false;
|
||||
t = farT;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MiterHairSegment::rayIntersect(const Ray &ray, Intersection &its) const {
|
||||
if (!rayIntersect(ray, ray.mint, ray.maxt, its.t))
|
||||
return false;
|
||||
its.p = ray(its.t);
|
||||
|
||||
|
||||
/* For now I don't compute texture coordinates at all. */
|
||||
its.uv = Point2(0,0);
|
||||
its.dpdu = Vector(0,0,0);
|
||||
its.dpdv = Vector(0,0,0);
|
||||
|
||||
its.geoFrame.s = tangent();
|
||||
const Vector relHitPoint = its.p - firstVertex();
|
||||
const Vector axis = tangent();
|
||||
its.geoFrame.n = Normal(relHitPoint - dot(axis, relHitPoint) * axis);
|
||||
its.geoFrame.t = cross(its.geoFrame.n, its.geoFrame.s);
|
||||
its.shFrame = its.geoFrame;
|
||||
its.wi = its.toLocal(-ray.d);
|
||||
its.hasUVPartials = false;
|
||||
its.shape = this;
|
||||
|
||||
/* Intersection refinement step */
|
||||
// Do I need this?
|
||||
/*
|
||||
Vector2 localDir(normalize(Vector2(local.x, local.y)));
|
||||
Vector rel = its.p - m_objectToWorld(Point(m_radius * localDir.x,
|
||||
m_radius * localDir.y, local.z));
|
||||
Float correction = -dot(rel, its.geoFrame.n)/dot(ray.d, its.geoFrame.n);
|
||||
|
||||
its.t += correction;
|
||||
if (its.t < ray.mint || its.t > ray.maxt) {
|
||||
its.t = std::numeric_limits<Float>::infinity();
|
||||
return false;
|
||||
}
|
||||
|
||||
its.p += ray.d * correction;
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(MTS_SSE)
|
||||
/* SSE-accelerated packet tracing is not supported for cylinders at the moment */
|
||||
__m128 MiterHairSegment::rayIntersectPacket(const RayPacket4 &packet, const
|
||||
__m128 start, __m128 end, __m128 inactive, Intersection4 &its) const {
|
||||
SSEVector result(_mm_setzero_ps()), mint(start), maxt(end), mask(inactive);
|
||||
Float t;
|
||||
|
||||
for (int i=0; i<4; i++) {
|
||||
Ray ray;
|
||||
for (int axis=0; axis<3; axis++) {
|
||||
ray.o[axis] = packet.o[axis].f[i];
|
||||
ray.d[axis] = packet.d[axis].f[i];
|
||||
}
|
||||
if (mask.i[i] != 0)
|
||||
continue;
|
||||
if (rayIntersect(ray, mint.f[i], maxt.f[i], t)) {
|
||||
result.i[i] = 0xFFFFFFFF;
|
||||
its.t.f[i] = t;
|
||||
}
|
||||
}
|
||||
return result.ps;
|
||||
}
|
||||
#endif
|
||||
|
||||
Float MiterHairSegment::sampleArea(ShapeSamplingRecord &sRec, const Point2 &sample) const {
|
||||
/* Luminaire sampling not supported */
|
||||
Log(EError, "Area sampling not supported by MiterHairSegment");
|
||||
return 0;
|
||||
/*
|
||||
Point p = Point(m_radius * std::cos(sample.y),
|
||||
m_radius * std::sin(sample.y),
|
||||
sample.x * m_length);
|
||||
sRec.p = m_objectToWorld(p);
|
||||
sRec.n = normalize(m_objectToWorld(Normal(p.x, p.y, 0.0f)));
|
||||
return m_invSurfaceArea;
|
||||
*/
|
||||
}
|
||||
|
||||
std::string MiterHairSegment::toString() const {
|
||||
std::ostringstream oss;
|
||||
oss << "MiterHairSegment [" << endl
|
||||
<< " index = " << m_iv << endl
|
||||
<< "]";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
|
||||
MTS_IMPLEMENT_CLASS_S(MiterHairSegment, false, Shape)
|
||||
MTS_NAMESPACE_END
|
|
@ -0,0 +1,87 @@
|
|||
#ifndef MITERSEG_H_
|
||||
#define MITERSEG_H_
|
||||
|
||||
#include <mitsuba/render/shape.h>
|
||||
#include <mitsuba/core/ref.h>
|
||||
|
||||
#include "hair.h"
|
||||
|
||||
MTS_NAMESPACE_BEGIN
|
||||
|
||||
class MiterHairSegment : public Shape {
|
||||
private:
|
||||
ref<Hair> m_hair; // the hair array to which this segment belongs
|
||||
int m_iv; // the index of this hair segment within the parent hair array
|
||||
|
||||
public:
|
||||
|
||||
MiterHairSegment(ref<Hair> parent, int index);
|
||||
|
||||
MiterHairSegment(const Properties &props) : Shape(props) {
|
||||
Log(EError, "Miter Hair Segments cannot be created directly; they are created by Hair instances.");
|
||||
}
|
||||
|
||||
MiterHairSegment(Stream *stream, InstanceManager *manager)
|
||||
: Shape(stream, manager) {
|
||||
AssertEx(false, "Hair Segments do not support serialization.");
|
||||
}
|
||||
|
||||
bool isClippable() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
void configure();
|
||||
|
||||
AABB getWorldAABB(Float start, Float end) const;
|
||||
|
||||
AABB getClippedAABB(const AABB &aabb) const;
|
||||
|
||||
bool rayIntersect(const Ray &_ray, Float start, Float end, Float &t) const;
|
||||
|
||||
bool rayIntersect(const Ray &ray, Intersection &its) const;
|
||||
|
||||
#if defined(MTS_SSE)
|
||||
__m128 rayIntersectPacket(const RayPacket4 &, const __m128, __m128, __m128, Intersection4 &) const;
|
||||
#endif
|
||||
|
||||
Float sampleArea(ShapeSamplingRecord &sRec, const Point2 &sample) const;
|
||||
|
||||
void serialize(Stream *stream, InstanceManager *manager) const {
|
||||
AssertEx(false, "Hair Segments do not support serialization.");
|
||||
}
|
||||
|
||||
std::string toString() const;
|
||||
|
||||
inline Point firstVertex() const { return m_hair->vertex(m_iv); }
|
||||
inline Point secondVertex() const { return m_hair->vertex(m_iv+1); }
|
||||
inline Vector tangent() const { return normalize(secondVertex() - firstVertex()); }
|
||||
|
||||
inline bool prevSegmentExists() const { return !m_hair->vertexStartsFiber(m_iv); }
|
||||
inline Point prevVertex() const { return m_hair->vertex(m_iv-1); }
|
||||
inline Vector prevTangent() const { return normalize(firstVertex() - prevVertex()); }
|
||||
|
||||
inline bool nextSegmentExists() const { return !m_hair->vertexStartsFiber(m_iv+2); }
|
||||
inline Point nextVertex() const { return m_hair->vertex(m_iv+2); }
|
||||
inline Vector nextTangent() const { return normalize(nextVertex() - secondVertex()); }
|
||||
|
||||
inline Vector firstMiterNormal() const {
|
||||
if (prevSegmentExists())
|
||||
return normalize(prevTangent() + tangent());
|
||||
else
|
||||
return tangent();
|
||||
}
|
||||
|
||||
inline Vector secondMiterNormal() const {
|
||||
if (nextSegmentExists())
|
||||
return normalize(tangent() + nextTangent());
|
||||
else
|
||||
return tangent();
|
||||
}
|
||||
|
||||
MTS_DECLARE_CLASS()
|
||||
};
|
||||
|
||||
MTS_NAMESPACE_END
|
||||
|
||||
|
||||
#endif /* MITERSEG_H_ */
|
Loading…
Reference in New Issue