feat(渲染器): 添加线条批处理功能以提高绘制效率
为OpenGL渲染器添加独立的线条批处理系统,包括: - 新增线条专用VAO和VBO - 实现线条顶点缓存和批处理刷新机制 - 修改所有线条绘制方法使用批处理 - 增加最大线条顶点数常量 - 优化线宽处理逻辑 通过批处理减少绘制调用次数,提升线条绘制性能。
This commit is contained in:
parent
ca41af0dbd
commit
f86f9b3966
|
|
@ -80,6 +80,7 @@ private:
|
||||||
// 形状批处理常量
|
// 形状批处理常量
|
||||||
static constexpr size_t MAX_CIRCLE_SEGMENTS = 128;
|
static constexpr size_t MAX_CIRCLE_SEGMENTS = 128;
|
||||||
static constexpr size_t MAX_SHAPE_VERTICES = 8192; // 最大形状顶点数
|
static constexpr size_t MAX_SHAPE_VERTICES = 8192; // 最大形状顶点数
|
||||||
|
static constexpr size_t MAX_LINE_VERTICES = 16384; // 最大线条顶点数
|
||||||
|
|
||||||
// 形状顶点结构(包含颜色)
|
// 形状顶点结构(包含颜色)
|
||||||
struct ShapeVertex {
|
struct ShapeVertex {
|
||||||
|
|
@ -93,6 +94,8 @@ private:
|
||||||
|
|
||||||
GLuint shapeVao_;
|
GLuint shapeVao_;
|
||||||
GLuint shapeVbo_;
|
GLuint shapeVbo_;
|
||||||
|
GLuint lineVao_; // 线条专用 VAO
|
||||||
|
GLuint lineVbo_; // 线条专用 VBO
|
||||||
|
|
||||||
glm::mat4 viewProjection_;
|
glm::mat4 viewProjection_;
|
||||||
std::vector<glm::mat4> transformStack_;
|
std::vector<glm::mat4> transformStack_;
|
||||||
|
|
@ -104,6 +107,11 @@ private:
|
||||||
size_t shapeVertexCount_ = 0;
|
size_t shapeVertexCount_ = 0;
|
||||||
GLenum currentShapeMode_ = GL_TRIANGLES;
|
GLenum currentShapeMode_ = GL_TRIANGLES;
|
||||||
|
|
||||||
|
// 线条批处理缓冲区
|
||||||
|
std::array<ShapeVertex, MAX_LINE_VERTICES> lineVertexCache_;
|
||||||
|
size_t lineVertexCount_ = 0;
|
||||||
|
float currentLineWidth_ = 1.0f;
|
||||||
|
|
||||||
// OpenGL 状态缓存
|
// OpenGL 状态缓存
|
||||||
BlendMode cachedBlendMode_ = BlendMode::None;
|
BlendMode cachedBlendMode_ = BlendMode::None;
|
||||||
bool blendEnabled_ = false;
|
bool blendEnabled_ = false;
|
||||||
|
|
@ -114,7 +122,9 @@ private:
|
||||||
|
|
||||||
void initShapeRendering();
|
void initShapeRendering();
|
||||||
void flushShapeBatch();
|
void flushShapeBatch();
|
||||||
|
void flushLineBatch();
|
||||||
void addShapeVertex(float x, float y, const Color &color);
|
void addShapeVertex(float x, float y, const Color &color);
|
||||||
|
void addLineVertex(float x, float y, const Color &color);
|
||||||
void submitShapeBatch(GLenum mode);
|
void submitShapeBatch(GLenum mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,16 @@ static constexpr size_t BLEND_STATE_COUNT =
|
||||||
sizeof(BLEND_STATES) / sizeof(BLEND_STATES[0]);
|
sizeof(BLEND_STATES) / sizeof(BLEND_STATES[0]);
|
||||||
|
|
||||||
GLRenderer::GLRenderer()
|
GLRenderer::GLRenderer()
|
||||||
: window_(nullptr), shapeVao_(0), shapeVbo_(0), vsync_(true),
|
: window_(nullptr), shapeVao_(0), shapeVbo_(0), lineVao_(0), lineVbo_(0),
|
||||||
shapeVertexCount_(0), currentShapeMode_(GL_TRIANGLES) {
|
vsync_(true), shapeVertexCount_(0), currentShapeMode_(GL_TRIANGLES),
|
||||||
|
lineVertexCount_(0), currentLineWidth_(1.0f) {
|
||||||
resetStats();
|
resetStats();
|
||||||
for (auto &v : shapeVertexCache_) {
|
for (auto &v : shapeVertexCache_) {
|
||||||
v = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
v = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
}
|
}
|
||||||
|
for (auto &v : lineVertexCache_) {
|
||||||
|
v = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLRenderer::~GLRenderer() { shutdown(); }
|
GLRenderer::~GLRenderer() { shutdown(); }
|
||||||
|
|
@ -106,9 +110,20 @@ void GLRenderer::shutdown() {
|
||||||
|
|
||||||
spriteBatch_.shutdown();
|
spriteBatch_.shutdown();
|
||||||
|
|
||||||
|
if (lineVbo_ != 0) {
|
||||||
|
glDeleteBuffers(1, &lineVbo_);
|
||||||
|
VRAMManager::getInstance().freeBuffer(MAX_LINE_VERTICES *
|
||||||
|
sizeof(ShapeVertex));
|
||||||
|
lineVbo_ = 0;
|
||||||
|
}
|
||||||
|
if (lineVao_ != 0) {
|
||||||
|
glDeleteVertexArrays(1, &lineVao_);
|
||||||
|
lineVao_ = 0;
|
||||||
|
}
|
||||||
if (shapeVbo_ != 0) {
|
if (shapeVbo_ != 0) {
|
||||||
glDeleteBuffers(1, &shapeVbo_);
|
glDeleteBuffers(1, &shapeVbo_);
|
||||||
VRAMManager::getInstance().freeBuffer(SHAPE_VBO_SIZE);
|
VRAMManager::getInstance().freeBuffer(MAX_SHAPE_VERTICES *
|
||||||
|
sizeof(ShapeVertex));
|
||||||
shapeVbo_ = 0;
|
shapeVbo_ = 0;
|
||||||
}
|
}
|
||||||
if (shapeVao_ != 0) {
|
if (shapeVao_ != 0) {
|
||||||
|
|
@ -126,6 +141,8 @@ void GLRenderer::beginFrame(const Color &clearColor) {
|
||||||
void GLRenderer::endFrame() {
|
void GLRenderer::endFrame() {
|
||||||
// 刷新所有待处理的形状批次
|
// 刷新所有待处理的形状批次
|
||||||
flushShapeBatch();
|
flushShapeBatch();
|
||||||
|
// 刷新所有待处理的线条批次
|
||||||
|
flushLineBatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::setViewport(int x, int y, int width, int height) {
|
void GLRenderer::setViewport(int x, int y, int width, int height) {
|
||||||
|
|
@ -246,59 +263,42 @@ void GLRenderer::endSpriteBatch() {
|
||||||
|
|
||||||
void GLRenderer::drawLine(const Vec2 &start, const Vec2 &end,
|
void GLRenderer::drawLine(const Vec2 &start, const Vec2 &end,
|
||||||
const Color &color, float width) {
|
const Color &color, float width) {
|
||||||
// 线条需要立即绘制,因为需要设置线宽
|
// 如果线宽改变,需要先刷新线条批次
|
||||||
// 先刷新之前的批处理
|
if (width != currentLineWidth_) {
|
||||||
flushShapeBatch();
|
flushLineBatch();
|
||||||
|
currentLineWidth_ = width;
|
||||||
|
}
|
||||||
|
|
||||||
glLineWidth(width);
|
// 添加两个顶点到线条缓冲区
|
||||||
|
addLineVertex(start.x, start.y, color);
|
||||||
shapeShader_.bind();
|
addLineVertex(end.x, end.y, color);
|
||||||
shapeShader_.setMat4("uViewProjection", viewProjection_);
|
|
||||||
|
|
||||||
ShapeVertex vertices[] = {
|
|
||||||
{start.x, start.y, color.r, color.g, color.b, color.a},
|
|
||||||
{end.x, end.y, color.r, color.g, color.b, color.a}};
|
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
|
|
||||||
|
|
||||||
glBindVertexArray(shapeVao_);
|
|
||||||
glDrawArrays(GL_LINES, 0, 2);
|
|
||||||
|
|
||||||
stats_.drawCalls++;
|
|
||||||
stats_.triangleCount += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::drawRect(const Rect &rect, const Color &color, float width) {
|
void GLRenderer::drawRect(const Rect &rect, const Color &color, float width) {
|
||||||
// 矩形边框需要立即绘制(因为有线宽参数)
|
// 如果线宽改变,需要先刷新线条批次
|
||||||
flushShapeBatch();
|
if (width != currentLineWidth_) {
|
||||||
|
flushLineBatch();
|
||||||
|
currentLineWidth_ = width;
|
||||||
|
}
|
||||||
|
|
||||||
float x1 = rect.origin.x;
|
float x1 = rect.origin.x;
|
||||||
float y1 = rect.origin.y;
|
float y1 = rect.origin.y;
|
||||||
float x2 = rect.origin.x + rect.size.width;
|
float x2 = rect.origin.x + rect.size.width;
|
||||||
float y2 = rect.origin.y + rect.size.height;
|
float y2 = rect.origin.y + rect.size.height;
|
||||||
|
|
||||||
glLineWidth(width);
|
|
||||||
shapeShader_.bind();
|
|
||||||
shapeShader_.setMat4("uViewProjection", viewProjection_);
|
|
||||||
|
|
||||||
// 4条线段 = 8个顶点
|
// 4条线段 = 8个顶点
|
||||||
ShapeVertex vertices[] = {{x1, y1, color.r, color.g, color.b, color.a},
|
// 上边
|
||||||
{x2, y1, color.r, color.g, color.b, color.a},
|
addLineVertex(x1, y1, color);
|
||||||
{x2, y1, color.r, color.g, color.b, color.a},
|
addLineVertex(x2, y1, color);
|
||||||
{x2, y2, color.r, color.g, color.b, color.a},
|
// 右边
|
||||||
{x2, y2, color.r, color.g, color.b, color.a},
|
addLineVertex(x2, y1, color);
|
||||||
{x1, y2, color.r, color.g, color.b, color.a},
|
addLineVertex(x2, y2, color);
|
||||||
{x1, y2, color.r, color.g, color.b, color.a},
|
// 下边
|
||||||
{x1, y1, color.r, color.g, color.b, color.a}};
|
addLineVertex(x2, y2, color);
|
||||||
|
addLineVertex(x1, y2, color);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
// 左边
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
|
addLineVertex(x1, y2, color);
|
||||||
|
addLineVertex(x1, y1, color);
|
||||||
glBindVertexArray(shapeVao_);
|
|
||||||
glDrawArrays(GL_LINES, 0, 8);
|
|
||||||
|
|
||||||
stats_.drawCalls++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::fillRect(const Rect &rect, const Color &color) {
|
void GLRenderer::fillRect(const Rect &rect, const Color &color) {
|
||||||
|
|
@ -329,36 +329,24 @@ void GLRenderer::drawCircle(const Vec2 ¢er, float radius,
|
||||||
segments = static_cast<int>(MAX_CIRCLE_SEGMENTS);
|
segments = static_cast<int>(MAX_CIRCLE_SEGMENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 圆形轮廓需要立即绘制(因为有线宽参数)
|
// 如果线宽改变,需要先刷新线条批次
|
||||||
flushShapeBatch();
|
if (width != currentLineWidth_) {
|
||||||
|
flushLineBatch();
|
||||||
glLineWidth(width);
|
currentLineWidth_ = width;
|
||||||
|
|
||||||
shapeShader_.bind();
|
|
||||||
shapeShader_.setMat4("uViewProjection", viewProjection_);
|
|
||||||
|
|
||||||
// 使用预分配缓冲区,避免每帧内存分配
|
|
||||||
const size_t vertexCount = static_cast<size_t>(segments + 1);
|
|
||||||
for (int i = 0; i <= segments; ++i) {
|
|
||||||
float angle =
|
|
||||||
2.0f * 3.14159f * static_cast<float>(i) / static_cast<float>(segments);
|
|
||||||
size_t idx = static_cast<size_t>(i);
|
|
||||||
shapeVertexCache_[idx].x = center.x + radius * cosf(angle);
|
|
||||||
shapeVertexCache_[idx].y = center.y + radius * sinf(angle);
|
|
||||||
shapeVertexCache_[idx].r = color.r;
|
|
||||||
shapeVertexCache_[idx].g = color.g;
|
|
||||||
shapeVertexCache_[idx].b = color.b;
|
|
||||||
shapeVertexCache_[idx].a = color.a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
// 使用线条批处理绘制圆形
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount * sizeof(ShapeVertex),
|
for (int i = 0; i < segments; ++i) {
|
||||||
shapeVertexCache_.data());
|
float angle1 =
|
||||||
|
2.0f * 3.14159f * static_cast<float>(i) / static_cast<float>(segments);
|
||||||
|
float angle2 = 2.0f * 3.14159f * static_cast<float>(i + 1) /
|
||||||
|
static_cast<float>(segments);
|
||||||
|
|
||||||
glBindVertexArray(shapeVao_);
|
addLineVertex(center.x + radius * cosf(angle1),
|
||||||
glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(vertexCount));
|
center.y + radius * sinf(angle1), color);
|
||||||
|
addLineVertex(center.x + radius * cosf(angle2),
|
||||||
stats_.drawCalls++;
|
center.y + radius * sinf(angle2), color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::fillCircle(const Vec2 ¢er, float radius,
|
void GLRenderer::fillCircle(const Vec2 ¢er, float radius,
|
||||||
|
|
@ -406,41 +394,22 @@ void GLRenderer::fillTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3,
|
||||||
|
|
||||||
void GLRenderer::drawPolygon(const std::vector<Vec2> &points,
|
void GLRenderer::drawPolygon(const std::vector<Vec2> &points,
|
||||||
const Color &color, float width) {
|
const Color &color, float width) {
|
||||||
flushShapeBatch();
|
if (points.size() < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
glLineWidth(width);
|
// 如果线宽改变,需要先刷新线条批次
|
||||||
shapeShader_.bind();
|
if (width != currentLineWidth_) {
|
||||||
shapeShader_.setMat4("uViewProjection", viewProjection_);
|
flushLineBatch();
|
||||||
|
currentLineWidth_ = width;
|
||||||
size_t pointCount = std::min(points.size(), MAX_CIRCLE_SEGMENTS);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < pointCount; ++i) {
|
|
||||||
size_t idx = i;
|
|
||||||
shapeVertexCache_[idx].x = points[i].x;
|
|
||||||
shapeVertexCache_[idx].y = points[i].y;
|
|
||||||
shapeVertexCache_[idx].r = color.r;
|
|
||||||
shapeVertexCache_[idx].g = color.g;
|
|
||||||
shapeVertexCache_[idx].b = color.b;
|
|
||||||
shapeVertexCache_[idx].a = color.a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 闭合多边形
|
// 绘制所有边
|
||||||
shapeVertexCache_[pointCount].x = points[0].x;
|
for (size_t i = 0; i < points.size(); ++i) {
|
||||||
shapeVertexCache_[pointCount].y = points[0].y;
|
const Vec2 &p1 = points[i];
|
||||||
shapeVertexCache_[pointCount].r = color.r;
|
const Vec2 &p2 = points[(i + 1) % points.size()];
|
||||||
shapeVertexCache_[pointCount].g = color.g;
|
addLineVertex(p1.x, p1.y, color);
|
||||||
shapeVertexCache_[pointCount].b = color.b;
|
addLineVertex(p2.x, p2.y, color);
|
||||||
shapeVertexCache_[pointCount].a = color.a;
|
}
|
||||||
pointCount++;
|
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, pointCount * sizeof(ShapeVertex),
|
|
||||||
shapeVertexCache_.data());
|
|
||||||
|
|
||||||
glBindVertexArray(shapeVao_);
|
|
||||||
glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(pointCount));
|
|
||||||
|
|
||||||
stats_.drawCalls++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::fillPolygon(const std::vector<Vec2> &points,
|
void GLRenderer::fillPolygon(const std::vector<Vec2> &points,
|
||||||
|
|
@ -517,13 +486,12 @@ void GLRenderer::initShapeRendering() {
|
||||||
// 编译形状着色器
|
// 编译形状着色器
|
||||||
shapeShader_.compileFromSource(SHAPE_VERTEX_SHADER, SHAPE_FRAGMENT_SHADER);
|
shapeShader_.compileFromSource(SHAPE_VERTEX_SHADER, SHAPE_FRAGMENT_SHADER);
|
||||||
|
|
||||||
// 创建 VAO 和 VBO
|
// 创建形状 VAO 和 VBO
|
||||||
glGenVertexArrays(1, &shapeVao_);
|
glGenVertexArrays(1, &shapeVao_);
|
||||||
glGenBuffers(1, &shapeVbo_);
|
glGenBuffers(1, &shapeVbo_);
|
||||||
|
|
||||||
glBindVertexArray(shapeVao_);
|
glBindVertexArray(shapeVao_);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
glBindBuffer(GL_ARRAY_BUFFER, shapeVbo_);
|
||||||
// 使用更大的缓冲区以支持形状批处理
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, MAX_SHAPE_VERTICES * sizeof(ShapeVertex),
|
glBufferData(GL_ARRAY_BUFFER, MAX_SHAPE_VERTICES * sizeof(ShapeVertex),
|
||||||
nullptr, GL_DYNAMIC_DRAW);
|
nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
|
@ -539,9 +507,32 @@ void GLRenderer::initShapeRendering() {
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
// 创建线条专用 VAO 和 VBO
|
||||||
|
glGenVertexArrays(1, &lineVao_);
|
||||||
|
glGenBuffers(1, &lineVbo_);
|
||||||
|
|
||||||
|
glBindVertexArray(lineVao_);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, lineVbo_);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, MAX_LINE_VERTICES * sizeof(ShapeVertex),
|
||||||
|
nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
// 位置属性 (location = 0)
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex),
|
||||||
|
reinterpret_cast<void *>(offsetof(ShapeVertex, x)));
|
||||||
|
|
||||||
|
// 颜色属性 (location = 1)
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex),
|
||||||
|
reinterpret_cast<void *>(offsetof(ShapeVertex, r)));
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
// VRAM 跟踪
|
// VRAM 跟踪
|
||||||
VRAMManager::getInstance().allocBuffer(MAX_SHAPE_VERTICES *
|
VRAMManager::getInstance().allocBuffer(MAX_SHAPE_VERTICES *
|
||||||
sizeof(ShapeVertex));
|
sizeof(ShapeVertex));
|
||||||
|
VRAMManager::getInstance().allocBuffer(MAX_LINE_VERTICES *
|
||||||
|
sizeof(ShapeVertex));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRenderer::addShapeVertex(float x, float y, const Color &color) {
|
void GLRenderer::addShapeVertex(float x, float y, const Color &color) {
|
||||||
|
|
@ -557,6 +548,19 @@ void GLRenderer::addShapeVertex(float x, float y, const Color &color) {
|
||||||
v.a = color.a;
|
v.a = color.a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLRenderer::addLineVertex(float x, float y, const Color &color) {
|
||||||
|
if (lineVertexCount_ >= MAX_LINE_VERTICES) {
|
||||||
|
flushLineBatch();
|
||||||
|
}
|
||||||
|
ShapeVertex &v = lineVertexCache_[lineVertexCount_++];
|
||||||
|
v.x = x;
|
||||||
|
v.y = y;
|
||||||
|
v.r = color.r;
|
||||||
|
v.g = color.g;
|
||||||
|
v.b = color.b;
|
||||||
|
v.a = color.a;
|
||||||
|
}
|
||||||
|
|
||||||
void GLRenderer::submitShapeBatch(GLenum mode) {
|
void GLRenderer::submitShapeBatch(GLenum mode) {
|
||||||
if (shapeVertexCount_ == 0)
|
if (shapeVertexCount_ == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -588,4 +592,27 @@ void GLRenderer::flushShapeBatch() {
|
||||||
shapeVertexCount_ = 0;
|
shapeVertexCount_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLRenderer::flushLineBatch() {
|
||||||
|
if (lineVertexCount_ == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 先刷新形状批次
|
||||||
|
flushShapeBatch();
|
||||||
|
|
||||||
|
glLineWidth(currentLineWidth_);
|
||||||
|
shapeShader_.bind();
|
||||||
|
shapeShader_.setMat4("uViewProjection", viewProjection_);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, lineVbo_);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, lineVertexCount_ * sizeof(ShapeVertex),
|
||||||
|
lineVertexCache_.data());
|
||||||
|
|
||||||
|
glBindVertexArray(lineVao_);
|
||||||
|
glDrawArrays(GL_LINES, 0, static_cast<GLsizei>(lineVertexCount_));
|
||||||
|
|
||||||
|
stats_.drawCalls++;
|
||||||
|
|
||||||
|
lineVertexCount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace extra2d
|
} // namespace extra2d
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue