2019-12-27 21:56:38 +00:00
|
|
|
/*
|
|
|
|
* Copyright 2019 Google Inc.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
|
|
* found in the LICENSE file.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "gm/gm.h"
|
|
|
|
|
|
|
|
#include "src/gpu/GrCaps.h"
|
|
|
|
#include "src/gpu/GrContextPriv.h"
|
|
|
|
#include "src/gpu/GrMemoryPool.h"
|
|
|
|
#include "src/gpu/GrMesh.h"
|
|
|
|
#include "src/gpu/GrOpFlushState.h"
|
|
|
|
#include "src/gpu/GrOpsRenderPass.h"
|
|
|
|
#include "src/gpu/GrPipeline.h"
|
|
|
|
#include "src/gpu/GrPrimitiveProcessor.h"
|
|
|
|
#include "src/gpu/GrProgramInfo.h"
|
|
|
|
#include "src/gpu/GrRecordingContextPriv.h"
|
|
|
|
#include "src/gpu/GrRenderTargetContext.h"
|
|
|
|
#include "src/gpu/GrRenderTargetContextPriv.h"
|
|
|
|
#include "src/gpu/GrShaderCaps.h"
|
|
|
|
#include "src/gpu/GrShaderVar.h"
|
|
|
|
#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
|
|
|
|
#include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
|
|
|
|
#include "src/gpu/glsl/GrGLSLPrimitiveProcessor.h"
|
|
|
|
#include "src/gpu/glsl/GrGLSLVarying.h"
|
|
|
|
#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
|
|
|
|
#include "src/gpu/ops/GrDrawOp.h"
|
|
|
|
|
|
|
|
namespace skiagm {
|
|
|
|
|
|
|
|
constexpr static GrGeometryProcessor::Attribute kPositionAttrib =
|
|
|
|
{"position", kFloat3_GrVertexAttribType, kFloat3_GrSLType};
|
|
|
|
|
|
|
|
constexpr static std::array<float, 3> kTri1[3] = {
|
|
|
|
{20.5f,20.5f,1}, {170.5f,280.5f,4}, {320.5f,20.5f,1}};
|
|
|
|
constexpr static std::array<float, 3> kTri2[3] = {
|
|
|
|
{640.5f,280.5f,3}, {490.5f,20.5f,1}, {340.5f,280.5f,6}};
|
|
|
|
constexpr static SkRect kRect = {20.5f, 340.5f, 640.5f, 480.5f};
|
|
|
|
|
|
|
|
constexpr static int kWidth = (int)kRect.fRight + 21;
|
|
|
|
constexpr static int kHeight = (int)kRect.fBottom + 21;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is a GPU-backend specific test. It ensures that tessellation works as expected by drawing
|
|
|
|
* several triangles. The test passes as long as the triangle tessellations match the reference
|
|
|
|
* images on gold.
|
|
|
|
*/
|
|
|
|
class TessellationGM : public GpuGM {
|
|
|
|
SkString onShortName() override { return SkString("tessellation"); }
|
|
|
|
SkISize onISize() override { return {kWidth, kHeight}; }
|
|
|
|
DrawResult onDraw(GrContext*, GrRenderTargetContext*, SkCanvas*, SkString*) override;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
class TessellationTestTriShader : public GrGeometryProcessor {
|
|
|
|
public:
|
|
|
|
TessellationTestTriShader(const SkMatrix& viewMatrix)
|
|
|
|
: GrGeometryProcessor(kTessellationTestTriShader_ClassID), fViewMatrix(viewMatrix) {
|
|
|
|
this->setVertexAttributes(&kPositionAttrib, 1);
|
|
|
|
this->setWillUseTessellationShaders();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const char* name() const final { return "TessellationTestTriShader"; }
|
|
|
|
void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const final {}
|
|
|
|
|
|
|
|
class Impl : public GrGLSLGeometryProcessor {
|
|
|
|
void onEmitCode(EmitArgs& args, GrGPArgs*) override {
|
|
|
|
args.fVaryingHandler->emitAttributes(args.fGP.cast<TessellationTestTriShader>());
|
|
|
|
const char* viewMatrix;
|
|
|
|
fViewMatrixUniform = args.fUniformHandler->addUniform(
|
|
|
|
kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix);
|
|
|
|
args.fVertBuilder->declareGlobal(
|
|
|
|
GrShaderVar("P_", kFloat3_GrSLType, GrShaderVar::kOut_TypeModifier));
|
|
|
|
args.fVertBuilder->codeAppendf(R"(
|
|
|
|
P_.xy = (%s * float3(position.xy, 1)).xy;
|
|
|
|
P_.z = position.z;)", viewMatrix);
|
|
|
|
// GrGLProgramBuilder will call writeTess*ShaderGLSL when it is compiling.
|
|
|
|
this->writeFragmentShader(args.fFragBuilder, args.fOutputColor, args.fOutputCoverage);
|
|
|
|
}
|
|
|
|
void writeFragmentShader(GrGLSLFPFragmentBuilder*, const char* color, const char* coverage);
|
|
|
|
void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& proc,
|
|
|
|
const CoordTransformRange&) override {
|
|
|
|
pdman.setSkMatrix(fViewMatrixUniform,
|
|
|
|
proc.cast<TessellationTestTriShader>().fViewMatrix);
|
|
|
|
}
|
|
|
|
GrGLSLUniformHandler::UniformHandle fViewMatrixUniform;
|
|
|
|
};
|
|
|
|
|
|
|
|
GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const override {
|
|
|
|
return new Impl;
|
|
|
|
}
|
|
|
|
|
|
|
|
SkString getTessControlShaderGLSL(const char* versionAndExtensionDecls,
|
|
|
|
const GrShaderCaps&) const override;
|
|
|
|
SkString getTessEvaluationShaderGLSL(const char* versionAndExtensionDecls,
|
|
|
|
const GrShaderCaps&) const override;
|
|
|
|
|
|
|
|
const SkMatrix fViewMatrix;
|
|
|
|
};
|
|
|
|
|
|
|
|
SkString TessellationTestTriShader::getTessControlShaderGLSL(
|
|
|
|
const char* versionAndExtensionDecls, const GrShaderCaps&) const {
|
|
|
|
SkString code(versionAndExtensionDecls);
|
|
|
|
code.append(R"(
|
|
|
|
layout(vertices = 3) out;
|
|
|
|
|
|
|
|
in vec3 P_[];
|
|
|
|
out vec3 P[];
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
P[gl_InvocationID] = P_[gl_InvocationID];
|
|
|
|
gl_TessLevelOuter[gl_InvocationID] = P_[gl_InvocationID].z;
|
|
|
|
gl_TessLevelInner[0] = 2.0;
|
|
|
|
})");
|
|
|
|
|
|
|
|
return code;
|
|
|
|
}
|
|
|
|
|
|
|
|
SkString TessellationTestTriShader::getTessEvaluationShaderGLSL(
|
|
|
|
const char* versionAndExtensionDecls, const GrShaderCaps&) const {
|
|
|
|
SkString code(versionAndExtensionDecls);
|
|
|
|
code.append(R"(
|
|
|
|
layout(triangles, equal_spacing, cw) in;
|
|
|
|
|
|
|
|
uniform vec4 sk_RTAdjust;
|
|
|
|
|
|
|
|
in vec3 P[];
|
|
|
|
out vec3 barycentric_coord;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
vec2 devcoord = mat3x2(P[0].xy, P[1].xy, P[2].xy) * gl_TessCoord.xyz;
|
|
|
|
devcoord = round(devcoord - .5) + .5; // Make horz and vert lines on px bounds.
|
|
|
|
gl_Position = vec4(devcoord.xy * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
|
|
|
|
|
|
|
|
float i = 0.0;
|
|
|
|
if (gl_TessCoord.y == 0.0) {
|
|
|
|
i += gl_TessCoord.z * P[1].z;
|
|
|
|
} else {
|
|
|
|
i += P[1].z;
|
|
|
|
if (gl_TessCoord.x == 0.0) {
|
|
|
|
i += gl_TessCoord.y * P[0].z;
|
|
|
|
} else {
|
|
|
|
i += P[0].z;
|
2019-12-28 05:20:55 +00:00
|
|
|
if (gl_TessCoord.z == 0.0) {
|
|
|
|
i += gl_TessCoord.x * P[2].z;
|
|
|
|
} else {
|
|
|
|
barycentric_coord = vec3(0, 1, 0);
|
|
|
|
return;
|
|
|
|
}
|
2019-12-27 21:56:38 +00:00
|
|
|
}
|
|
|
|
}
|
2019-12-28 05:20:55 +00:00
|
|
|
i = abs(mod(i, 2.0) - 1.0);
|
|
|
|
barycentric_coord = vec3(i, 0, 1.0 - i);
|
|
|
|
})");
|
2019-12-27 21:56:38 +00:00
|
|
|
|
|
|
|
return code;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TessellationTestTriShader::Impl::writeFragmentShader(
|
|
|
|
GrGLSLFPFragmentBuilder* f, const char* color, const char* coverage) {
|
|
|
|
f->declareGlobal(
|
|
|
|
GrShaderVar("barycentric_coord", kFloat3_GrSLType, GrShaderVar::kIn_TypeModifier));
|
|
|
|
f->codeAppendf(R"(
|
|
|
|
half3 d = half3(1 - barycentric_coord/fwidth(barycentric_coord));
|
|
|
|
half coverage = max(max(d.x, d.y), d.z);
|
|
|
|
%s = half4(0, coverage, coverage, 1);
|
|
|
|
%s = half4(1);)", color, coverage);
|
|
|
|
}
|
|
|
|
|
|
|
|
class TessellationTestRectShader : public GrGeometryProcessor {
|
|
|
|
public:
|
|
|
|
TessellationTestRectShader(const SkMatrix& viewMatrix)
|
|
|
|
: GrGeometryProcessor(kTessellationTestTriShader_ClassID), fViewMatrix(viewMatrix) {
|
|
|
|
this->setWillUseTessellationShaders();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const char* name() const final { return "TessellationTestRectShader"; }
|
|
|
|
void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const final {}
|
|
|
|
|
|
|
|
class Impl : public GrGLSLGeometryProcessor {
|
|
|
|
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
|
|
|
|
const char* viewMatrix;
|
|
|
|
fViewMatrixUniform = args.fUniformHandler->addUniform(
|
|
|
|
kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix);
|
|
|
|
args.fVertBuilder->declareGlobal(
|
|
|
|
GrShaderVar("M_", kFloat3x3_GrSLType, GrShaderVar::kOut_TypeModifier));
|
|
|
|
args.fVertBuilder->codeAppendf("M_ = %s;", viewMatrix);
|
|
|
|
// GrGLProgramBuilder will call writeTess*ShaderGLSL when it is compiling.
|
|
|
|
this->writeFragmentShader(args.fFragBuilder, args.fOutputColor, args.fOutputCoverage);
|
|
|
|
}
|
|
|
|
void writeFragmentShader(GrGLSLFPFragmentBuilder*, const char* color, const char* coverage);
|
|
|
|
void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& proc,
|
|
|
|
const CoordTransformRange&) override {
|
|
|
|
pdman.setSkMatrix(fViewMatrixUniform,
|
|
|
|
proc.cast<TessellationTestRectShader>().fViewMatrix);
|
|
|
|
}
|
|
|
|
GrGLSLUniformHandler::UniformHandle fViewMatrixUniform;
|
|
|
|
};
|
|
|
|
|
|
|
|
GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const override {
|
|
|
|
return new Impl;
|
|
|
|
}
|
|
|
|
|
|
|
|
SkString getTessControlShaderGLSL(const char* versionAndExtensionDecls,
|
|
|
|
const GrShaderCaps&) const override;
|
|
|
|
SkString getTessEvaluationShaderGLSL(const char* versionAndExtensionDecls,
|
|
|
|
const GrShaderCaps&) const override;
|
|
|
|
|
|
|
|
const SkMatrix fViewMatrix;
|
|
|
|
};
|
|
|
|
|
|
|
|
SkString TessellationTestRectShader::getTessControlShaderGLSL(
|
|
|
|
const char* versionAndExtensionDecls, const GrShaderCaps& caps) const {
|
|
|
|
SkString code(versionAndExtensionDecls);
|
|
|
|
code.append(R"(
|
|
|
|
layout(vertices = 1) out;
|
|
|
|
|
|
|
|
in mat3 M_[];
|
|
|
|
out mat3 M[];
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
M[gl_InvocationID] = M_[gl_InvocationID];
|
|
|
|
gl_TessLevelInner[0] = 8.0;
|
|
|
|
gl_TessLevelInner[1] = 2.0;
|
|
|
|
gl_TessLevelOuter[0] = 2.0;
|
|
|
|
gl_TessLevelOuter[1] = 8.0;
|
|
|
|
gl_TessLevelOuter[2] = 2.0;
|
|
|
|
gl_TessLevelOuter[3] = 8.0;
|
|
|
|
})");
|
|
|
|
|
|
|
|
return code;
|
|
|
|
}
|
|
|
|
|
|
|
|
SkString TessellationTestRectShader::getTessEvaluationShaderGLSL(
|
|
|
|
const char* versionAndExtensionDecls, const GrShaderCaps& caps) const {
|
|
|
|
SkString code(versionAndExtensionDecls);
|
|
|
|
code.appendf(R"(
|
|
|
|
layout(quads, equal_spacing, cw) in;
|
|
|
|
|
|
|
|
uniform vec4 sk_RTAdjust;
|
|
|
|
|
|
|
|
in mat3 M[];
|
|
|
|
out vec4 barycentric_coord;
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
vec4 R = vec4(%f, %f, %f, %f);
|
|
|
|
vec2 localcoord = mix(R.xy, R.zw, gl_TessCoord.xy);
|
|
|
|
vec2 devcoord = (M[0] * vec3(localcoord, 1)).xy;
|
|
|
|
devcoord = round(devcoord - .5) + .5; // Make horz and vert lines on px bounds.
|
|
|
|
gl_Position = vec4(devcoord.xy * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
|
|
|
|
|
|
|
|
float i = gl_TessCoord.x * 8.0;
|
|
|
|
i = abs(mod(i, 2.0) - 1.0);
|
|
|
|
if (gl_TessCoord.y == 0.0 || gl_TessCoord.y == 1.0) {
|
|
|
|
barycentric_coord = vec4(i, 1.0 - i, 0, 0);
|
|
|
|
} else {
|
|
|
|
barycentric_coord = vec4(0, 0, i, 1.0 - i);
|
|
|
|
}
|
|
|
|
})", kRect.left(), kRect.top(), kRect.right(), kRect.bottom());
|
|
|
|
|
|
|
|
return code;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TessellationTestRectShader::Impl::writeFragmentShader(
|
|
|
|
GrGLSLFPFragmentBuilder* f, const char* color, const char* coverage) {
|
|
|
|
f->declareGlobal(GrShaderVar("barycentric_coord", kFloat4_GrSLType,
|
|
|
|
GrShaderVar::kIn_TypeModifier));
|
|
|
|
f->codeAppendf(R"(
|
|
|
|
float4 fwidths = fwidth(barycentric_coord);
|
|
|
|
half coverage = 0;
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
if (fwidths[i] != 0) {
|
|
|
|
coverage = half(max(coverage, 1 - barycentric_coord[i]/fwidths[i]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
%s = half4(coverage, 0, coverage, 1);
|
|
|
|
%s = half4(1);)", color, coverage);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class TessellationTestOp : public GrDrawOp {
|
|
|
|
DEFINE_OP_CLASS_ID
|
|
|
|
|
|
|
|
public:
|
|
|
|
TessellationTestOp(const SkMatrix& viewMatrix, const std::array<float, 3>* triPositions)
|
|
|
|
: GrDrawOp(ClassID()), fViewMatrix(viewMatrix), fTriPositions(triPositions) {
|
|
|
|
this->setBounds(SkRect::MakeIWH(kWidth, kHeight), HasAABloat::kNo, IsHairline::kNo);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const char* name() const override { return "TessellationTestOp"; }
|
|
|
|
FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
|
|
|
|
GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*,
|
|
|
|
bool hasMixedSampledCoverage, GrClampType) override {
|
|
|
|
return GrProcessorSet::EmptySetAnalysis();
|
|
|
|
}
|
|
|
|
|
|
|
|
void onPrepare(GrOpFlushState* flushState) override {
|
|
|
|
if (fTriPositions) {
|
|
|
|
if (void* vertexData = flushState->makeVertexSpace(sizeof(float) * 3, 3, &fVertexBuffer,
|
|
|
|
&fBaseVertex)) {
|
|
|
|
memcpy(vertexData, fTriPositions, sizeof(float) * 3 * 3);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void onExecute(GrOpFlushState* state, const SkRect& chainBounds) override {
|
|
|
|
GrPipeline pipeline(GrScissorTest::kDisabled, SkBlendMode::kSrc,
|
|
|
|
state->drawOpArgs().outputSwizzle());
|
|
|
|
GrPipeline::FixedDynamicState fixedDynamicState;
|
|
|
|
|
|
|
|
GrMesh mesh(GrPrimitiveType::kPatches);
|
|
|
|
std::unique_ptr<GrGeometryProcessor> shader;
|
|
|
|
if (fTriPositions) {
|
|
|
|
if (!fVertexBuffer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
mesh.setTessellationPatchVertexCount(3);
|
|
|
|
mesh.setNonIndexedNonInstanced(3);
|
|
|
|
mesh.setVertexData(fVertexBuffer, fBaseVertex);
|
|
|
|
shader = std::make_unique<TessellationTestTriShader>(fViewMatrix);
|
|
|
|
} else {
|
|
|
|
// Use a mismatched number of vertices in the input patch vs output.
|
|
|
|
// (The tessellation control shader will output one vertex per patch.)
|
|
|
|
mesh.setTessellationPatchVertexCount(5);
|
|
|
|
mesh.setNonIndexedNonInstanced(5);
|
|
|
|
shader = std::make_unique<TessellationTestRectShader>(fViewMatrix);
|
|
|
|
}
|
|
|
|
|
|
|
|
GrProgramInfo programInfo(state->proxy()->numSamples(), state->proxy()->numStencilSamples(),
|
|
|
|
state->proxy()->backendFormat(), state->view()->origin(),
|
|
|
|
&pipeline, shader.get(), &fixedDynamicState, nullptr, 0,
|
|
|
|
GrPrimitiveType::kPatches, mesh.tessellationPatchVertexCount());
|
|
|
|
|
|
|
|
state->opsRenderPass()->draw(programInfo, &mesh, 1, SkRect::MakeIWH(kWidth, kHeight));
|
|
|
|
}
|
|
|
|
|
|
|
|
const SkMatrix fViewMatrix;
|
|
|
|
const std::array<float, 3>* const fTriPositions;
|
|
|
|
sk_sp<const GrBuffer> fVertexBuffer;
|
|
|
|
int fBaseVertex = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
static SkPath build_outset_triangle(const std::array<float, 3>* tri) {
|
|
|
|
SkPath outset;
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
SkPoint p = {tri[i][0], tri[i][1]};
|
|
|
|
SkPoint left = {tri[(i + 2) % 3][0], tri[(i + 2) % 3][1]};
|
|
|
|
SkPoint right = {tri[(i + 1) % 3][0], tri[(i + 1) % 3][1]};
|
|
|
|
SkPoint n0, n1;
|
|
|
|
n0.setNormalize(left.y() - p.y(), p.x() - left.x());
|
|
|
|
n1.setNormalize(p.y() - right.y(), right.x() - p.x());
|
|
|
|
p += (n0 + n1) * 3;
|
|
|
|
if (0 == i) {
|
|
|
|
outset.moveTo(p);
|
|
|
|
} else {
|
|
|
|
outset.lineTo(p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return outset;
|
|
|
|
}
|
|
|
|
|
|
|
|
DrawResult TessellationGM::onDraw(GrContext* ctx, GrRenderTargetContext* rtc, SkCanvas* canvas,
|
|
|
|
SkString* errorMsg) {
|
|
|
|
if (!ctx->priv().caps()->shaderCaps()->tessellationSupport()) {
|
|
|
|
*errorMsg = "Requires GPU tessellation support.";
|
|
|
|
return DrawResult::kSkip;
|
|
|
|
}
|
|
|
|
if (!ctx->priv().caps()->shaderCaps()->shaderDerivativeSupport()) {
|
|
|
|
*errorMsg = "Requires shader derivatives."
|
|
|
|
"(These are expected to always be present when there is tessellation!!)";
|
|
|
|
return DrawResult::kFail;
|
|
|
|
}
|
|
|
|
|
|
|
|
canvas->clear(SK_ColorBLACK);
|
|
|
|
SkPaint borderPaint;
|
|
|
|
borderPaint.setColor4f({0,1,1,1});
|
|
|
|
borderPaint.setAntiAlias(true);
|
|
|
|
canvas->drawPath(build_outset_triangle(kTri1), borderPaint);
|
|
|
|
canvas->drawPath(build_outset_triangle(kTri2), borderPaint);
|
|
|
|
|
|
|
|
borderPaint.setColor4f({1,0,1,1});
|
|
|
|
canvas->drawRect(kRect.makeOutset(1.5f, 1.5f), borderPaint);
|
|
|
|
|
|
|
|
GrOpMemoryPool* pool = ctx->priv().opMemoryPool();
|
|
|
|
rtc->priv().testingOnly_addDrawOp(
|
|
|
|
pool->allocate<TessellationTestOp>(canvas->getTotalMatrix(), kTri1));
|
|
|
|
rtc->priv().testingOnly_addDrawOp(
|
|
|
|
pool->allocate<TessellationTestOp>(canvas->getTotalMatrix(), kTri2));
|
|
|
|
rtc->priv().testingOnly_addDrawOp(
|
|
|
|
pool->allocate<TessellationTestOp>(canvas->getTotalMatrix(), nullptr));
|
|
|
|
|
|
|
|
return skiagm::DrawResult::kOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
DEF_GM( return new TessellationGM(); )
|
|
|
|
|
|
|
|
}
|