544 lines
18 KiB
C++
544 lines
18 KiB
C++
/*
|
|
src/glutil.cpp -- Convenience classes for accessing OpenGL >= 3.x
|
|
|
|
NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
|
|
The widget drawing code is based on the NanoVG demo application
|
|
by Mikko Mononen.
|
|
|
|
All rights reserved. Use of this source code is governed by a
|
|
BSD-style license that can be found in the LICENSE.txt file.
|
|
*/
|
|
|
|
#include <nanogui/glutil.h>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <Eigen/Geometry>
|
|
|
|
NAMESPACE_BEGIN(nanogui)
|
|
|
|
static GLuint createShader_helper(GLint type, const std::string &name,
|
|
const std::string &defines,
|
|
std::string shader_string) {
|
|
if (shader_string.empty())
|
|
return (GLuint) 0;
|
|
|
|
if (!defines.empty()) {
|
|
if (shader_string.length() > 8 && shader_string.substr(0, 8) == "#version") {
|
|
std::istringstream iss(shader_string);
|
|
std::ostringstream oss;
|
|
std::string line;
|
|
std::getline(iss, line);
|
|
oss << line << std::endl;
|
|
oss << defines;
|
|
while (std::getline(iss, line))
|
|
oss << line << std::endl;
|
|
shader_string = oss.str();
|
|
} else {
|
|
shader_string = defines + shader_string;
|
|
}
|
|
}
|
|
|
|
GLuint id = glCreateShader(type);
|
|
const char *shader_string_const = shader_string.c_str();
|
|
glShaderSource(id, 1, &shader_string_const, nullptr);
|
|
glCompileShader(id);
|
|
|
|
GLint status;
|
|
glGetShaderiv(id, GL_COMPILE_STATUS, &status);
|
|
|
|
if (status != GL_TRUE) {
|
|
char buffer[512];
|
|
std::cerr << "Error while compiling ";
|
|
if (type == GL_VERTEX_SHADER)
|
|
std::cerr << "vertex shader";
|
|
else if (type == GL_FRAGMENT_SHADER)
|
|
std::cerr << "fragment shader";
|
|
else if (type == GL_GEOMETRY_SHADER)
|
|
std::cerr << "geometry shader";
|
|
std::cerr << " \"" << name << "\":" << std::endl;
|
|
std::cerr << shader_string << std::endl << std::endl;
|
|
glGetShaderInfoLog(id, 512, nullptr, buffer);
|
|
std::cerr << "Error: " << std::endl << buffer << std::endl;
|
|
throw std::runtime_error("Shader compilation failed!");
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
bool GLShader::initFromFiles(
|
|
const std::string &name,
|
|
const std::string &vertex_fname,
|
|
const std::string &fragment_fname,
|
|
const std::string &geometry_fname) {
|
|
auto file_to_string = [](const std::string &filename) -> std::string {
|
|
if (filename.empty())
|
|
return "";
|
|
std::ifstream t(filename);
|
|
return std::string((std::istreambuf_iterator<char>(t)),
|
|
std::istreambuf_iterator<char>());
|
|
};
|
|
|
|
return init(name,
|
|
file_to_string(vertex_fname),
|
|
file_to_string(fragment_fname),
|
|
file_to_string(geometry_fname));
|
|
}
|
|
|
|
bool GLShader::init(const std::string &name,
|
|
const std::string &vertex_str,
|
|
const std::string &fragment_str,
|
|
const std::string &geometry_str) {
|
|
std::string defines;
|
|
for (auto def : mDefinitions)
|
|
defines += std::string("#define ") + def.first + std::string(" ") + def.second + "\n";
|
|
|
|
glGenVertexArrays(1, &mVertexArrayObject);
|
|
mName = name;
|
|
mVertexShader =
|
|
createShader_helper(GL_VERTEX_SHADER, name, defines, vertex_str);
|
|
mGeometryShader =
|
|
createShader_helper(GL_GEOMETRY_SHADER, name, defines, geometry_str);
|
|
mFragmentShader =
|
|
createShader_helper(GL_FRAGMENT_SHADER, name, defines, fragment_str);
|
|
|
|
if (!mVertexShader || !mFragmentShader)
|
|
return false;
|
|
if (!geometry_str.empty() && !mGeometryShader)
|
|
return false;
|
|
|
|
mProgramShader = glCreateProgram();
|
|
|
|
glAttachShader(mProgramShader, mVertexShader);
|
|
glAttachShader(mProgramShader, mFragmentShader);
|
|
|
|
if (mGeometryShader)
|
|
glAttachShader(mProgramShader, mGeometryShader);
|
|
|
|
glLinkProgram(mProgramShader);
|
|
|
|
GLint status;
|
|
glGetProgramiv(mProgramShader, GL_LINK_STATUS, &status);
|
|
|
|
if (status != GL_TRUE) {
|
|
char buffer[512];
|
|
glGetProgramInfoLog(mProgramShader, 512, nullptr, buffer);
|
|
std::cerr << "Linker error (" << mName << "): " << std::endl << buffer << std::endl;
|
|
mProgramShader = 0;
|
|
throw std::runtime_error("Shader linking failed!");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void GLShader::bind() {
|
|
glUseProgram(mProgramShader);
|
|
glBindVertexArray(mVertexArrayObject);
|
|
}
|
|
|
|
GLint GLShader::attrib(const std::string &name, bool warn) const {
|
|
GLint id = glGetAttribLocation(mProgramShader, name.c_str());
|
|
if (id == -1 && warn)
|
|
std::cerr << mName << ": warning: did not find attrib " << name << std::endl;
|
|
return id;
|
|
}
|
|
|
|
void GLShader::setUniform(const std::string &name, const GLUniformBuffer &buf, bool warn) {
|
|
GLuint blockIndex = glGetUniformBlockIndex(mProgramShader, name.c_str());
|
|
if (blockIndex == GL_INVALID_INDEX) {
|
|
if (warn)
|
|
std::cerr << mName << ": warning: did not find uniform buffer " << name << std::endl;
|
|
return;
|
|
}
|
|
glUniformBlockBinding(mProgramShader, blockIndex, buf.getBindingPoint());
|
|
}
|
|
|
|
GLint GLShader::uniform(const std::string &name, bool warn) const {
|
|
GLint id = glGetUniformLocation(mProgramShader, name.c_str());
|
|
if (id == -1 && warn)
|
|
std::cerr << mName << ": warning: did not find uniform " << name << std::endl;
|
|
return id;
|
|
}
|
|
|
|
void GLShader::uploadAttrib(const std::string &name, size_t size, int dim,
|
|
uint32_t compSize, GLuint glType, bool integral,
|
|
const void *data, int version) {
|
|
int attribID = 0;
|
|
if (name != "indices") {
|
|
attribID = attrib(name);
|
|
if (attribID < 0)
|
|
return;
|
|
}
|
|
|
|
GLuint bufferID;
|
|
auto it = mBufferObjects.find(name);
|
|
if (it != mBufferObjects.end()) {
|
|
Buffer &buffer = it->second;
|
|
bufferID = it->second.id;
|
|
buffer.version = version;
|
|
buffer.size = (GLuint) size;
|
|
buffer.compSize = compSize;
|
|
} else {
|
|
glGenBuffers(1, &bufferID);
|
|
Buffer buffer;
|
|
buffer.id = bufferID;
|
|
buffer.glType = glType;
|
|
buffer.dim = dim;
|
|
buffer.compSize = compSize;
|
|
buffer.size = (GLuint) size;
|
|
buffer.version = version;
|
|
mBufferObjects[name] = buffer;
|
|
}
|
|
size_t totalSize = size * (size_t) compSize;
|
|
|
|
if (name == "indices") {
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferID);
|
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, totalSize, data, GL_DYNAMIC_DRAW);
|
|
} else {
|
|
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
|
|
glBufferData(GL_ARRAY_BUFFER, totalSize, data, GL_DYNAMIC_DRAW);
|
|
if (size == 0) {
|
|
glDisableVertexAttribArray(attribID);
|
|
} else {
|
|
glEnableVertexAttribArray(attribID);
|
|
glVertexAttribPointer(attribID, dim, glType, integral, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLShader::downloadAttrib(const std::string &name, size_t size, int /* dim */,
|
|
uint32_t compSize, GLuint /* glType */, void *data) {
|
|
auto it = mBufferObjects.find(name);
|
|
if (it == mBufferObjects.end())
|
|
throw std::runtime_error("downloadAttrib(" + mName + ", " + name + ") : buffer not found!");
|
|
|
|
const Buffer &buf = it->second;
|
|
if (buf.size != size || buf.compSize != compSize)
|
|
throw std::runtime_error(mName + ": downloadAttrib: size mismatch!");
|
|
|
|
size_t totalSize = size * (size_t) compSize;
|
|
|
|
if (name == "indices") {
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf.id);
|
|
glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, totalSize, data);
|
|
} else {
|
|
glBindBuffer(GL_ARRAY_BUFFER, buf.id);
|
|
glGetBufferSubData(GL_ARRAY_BUFFER, 0, totalSize, data);
|
|
}
|
|
}
|
|
|
|
void GLShader::shareAttrib(const GLShader &otherShader, const std::string &name, const std::string &_as) {
|
|
std::string as = _as.length() == 0 ? name : _as;
|
|
auto it = otherShader.mBufferObjects.find(name);
|
|
if (it == otherShader.mBufferObjects.end())
|
|
throw std::runtime_error("shareAttribute(" + otherShader.mName + ", " + name + "): attribute not found!");
|
|
const Buffer &buffer = it->second;
|
|
|
|
if (name != "indices") {
|
|
int attribID = attrib(as);
|
|
if (attribID < 0)
|
|
return;
|
|
glEnableVertexAttribArray(attribID);
|
|
glBindBuffer(GL_ARRAY_BUFFER, buffer.id);
|
|
glVertexAttribPointer(attribID, buffer.dim, buffer.glType, buffer.compSize == 1 ? GL_TRUE : GL_FALSE, 0, 0);
|
|
} else {
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer.id);
|
|
}
|
|
}
|
|
|
|
void GLShader::invalidateAttribs() {
|
|
for (auto &buffer : mBufferObjects)
|
|
buffer.second.version = -1;
|
|
}
|
|
|
|
void GLShader::freeAttrib(const std::string &name) {
|
|
auto it = mBufferObjects.find(name);
|
|
if (it != mBufferObjects.end()) {
|
|
glDeleteBuffers(1, &it->second.id);
|
|
mBufferObjects.erase(it);
|
|
}
|
|
}
|
|
|
|
void GLShader::drawIndexed(int type, uint32_t offset_, uint32_t count_) {
|
|
if (count_ == 0)
|
|
return;
|
|
size_t offset = offset_;
|
|
size_t count = count_;
|
|
|
|
switch (type) {
|
|
case GL_TRIANGLES: offset *= 3; count *= 3; break;
|
|
case GL_LINES: offset *= 2; count *= 2; break;
|
|
}
|
|
|
|
glDrawElements(type, (GLsizei) count, GL_UNSIGNED_INT,
|
|
(const void *)(offset * sizeof(uint32_t)));
|
|
}
|
|
|
|
void GLShader::drawArray(int type, uint32_t offset, uint32_t count) {
|
|
if (count == 0)
|
|
return;
|
|
|
|
glDrawArrays(type, offset, count);
|
|
}
|
|
|
|
void GLShader::free() {
|
|
for (auto &buf: mBufferObjects)
|
|
glDeleteBuffers(1, &buf.second.id);
|
|
mBufferObjects.clear();
|
|
|
|
if (mVertexArrayObject) {
|
|
glDeleteVertexArrays(1, &mVertexArrayObject);
|
|
mVertexArrayObject = 0;
|
|
}
|
|
|
|
glDeleteProgram(mProgramShader); mProgramShader = 0;
|
|
glDeleteShader(mVertexShader); mVertexShader = 0;
|
|
glDeleteShader(mFragmentShader); mFragmentShader = 0;
|
|
glDeleteShader(mGeometryShader); mGeometryShader = 0;
|
|
}
|
|
|
|
const GLShader::Buffer &GLShader::attribBuffer(const std::string &name) {
|
|
for (auto &pair : mBufferObjects) {
|
|
if (pair.first == name)
|
|
return pair.second;
|
|
}
|
|
|
|
throw std::runtime_error(mName + ": attribBuffer: " + name + " not found!");
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
|
|
void GLUniformBuffer::init() {
|
|
glGenBuffers(1, &mID);
|
|
}
|
|
|
|
void GLUniformBuffer::bind(int bindingPoint) {
|
|
mBindingPoint = bindingPoint;
|
|
glBindBufferBase(GL_UNIFORM_BUFFER, mBindingPoint, mID);
|
|
}
|
|
|
|
void GLUniformBuffer::release() {
|
|
glBindBufferBase(GL_UNIFORM_BUFFER, mBindingPoint, 0);
|
|
}
|
|
|
|
void GLUniformBuffer::free() {
|
|
glDeleteBuffers(1, &mID);
|
|
mID = 0;
|
|
}
|
|
|
|
void GLUniformBuffer::update(const std::vector<uint8_t> &data) {
|
|
glBindBuffer(GL_UNIFORM_BUFFER, mID);
|
|
glBufferData(GL_UNIFORM_BUFFER, data.size(), data.data(), GL_DYNAMIC_DRAW);
|
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
|
|
void GLFramebuffer::init(const Vector2i &size, int nSamples) {
|
|
mSize = size;
|
|
mSamples = nSamples;
|
|
|
|
glGenRenderbuffers(1, &mColor);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, mColor);
|
|
|
|
if (nSamples <= 1)
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, size.x(), size.y());
|
|
else
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, nSamples, GL_RGBA8, size.x(), size.y());
|
|
|
|
glGenRenderbuffers(1, &mDepth);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, mDepth);
|
|
|
|
if (nSamples <= 1)
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x(), size.y());
|
|
else
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, nSamples, GL_DEPTH24_STENCIL8, size.x(), size.y());
|
|
|
|
glGenFramebuffers(1, &mFramebuffer);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
|
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, mColor);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, mDepth);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, mDepth);
|
|
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
|
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
if (status != GL_FRAMEBUFFER_COMPLETE)
|
|
throw std::runtime_error("Could not create framebuffer object!");
|
|
|
|
release();
|
|
}
|
|
|
|
void GLFramebuffer::free() {
|
|
glDeleteRenderbuffers(1, &mColor);
|
|
glDeleteRenderbuffers(1, &mDepth);
|
|
mColor = mDepth = 0;
|
|
}
|
|
|
|
void GLFramebuffer::bind() {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
|
if (mSamples > 1)
|
|
glEnable(GL_MULTISAMPLE);
|
|
}
|
|
|
|
void GLFramebuffer::release() {
|
|
if (mSamples > 1)
|
|
glDisable(GL_MULTISAMPLE);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
void GLFramebuffer::blit() {
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mFramebuffer);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
glDrawBuffer(GL_BACK);
|
|
|
|
glBlitFramebuffer(0, 0, mSize.x(), mSize.y(), 0, 0, mSize.x(), mSize.y(),
|
|
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
void GLFramebuffer::downloadTGA(const std::string &filename) {
|
|
uint8_t *temp = new uint8_t[mSize.prod() * 4];
|
|
|
|
std::cout << "Writing \"" << filename << "\" (" << mSize.x() << "x" << mSize.y() << ") .. ";
|
|
std::cout.flush();
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mFramebuffer);
|
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
glReadPixels(0, 0, mSize.x(), mSize.y(), GL_BGRA, GL_UNSIGNED_BYTE, temp);
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
|
|
|
uint32_t rowSize = mSize.x() * 4;
|
|
uint32_t halfHeight = mSize.y() / 2;
|
|
uint8_t *line = (uint8_t *) alloca(rowSize);
|
|
for (uint32_t i=0, j=mSize.y()-1; i<halfHeight; ++i) {
|
|
memcpy(line, temp + i * rowSize, rowSize);
|
|
memcpy(temp + i * rowSize, temp + j * rowSize, rowSize);
|
|
memcpy(temp + j * rowSize, line, rowSize);
|
|
j--;
|
|
}
|
|
|
|
FILE *tga = fopen(filename.c_str(), "wb");
|
|
if (tga == nullptr)
|
|
throw std::runtime_error("GLFramebuffer::downloadTGA(): Could not open output file");
|
|
fputc(0, tga); /* ID */
|
|
fputc(0, tga); /* Color map */
|
|
fputc(2, tga); /* Image type */
|
|
fputc(0, tga); fputc(0, tga); /* First entry of color map (unused) */
|
|
fputc(0, tga); fputc(0, tga); /* Length of color map (unused) */
|
|
fputc(0, tga); /* Color map entry size (unused) */
|
|
fputc(0, tga); fputc(0, tga); /* X offset */
|
|
fputc(0, tga); fputc(0, tga); /* Y offset */
|
|
fputc(mSize.x() % 256, tga); /* Width */
|
|
fputc(mSize.x() / 256, tga); /* continued */
|
|
fputc(mSize.y() % 256, tga); /* Height */
|
|
fputc(mSize.y() / 256, tga); /* continued */
|
|
fputc(32, tga); /* Bits per pixel */
|
|
fputc(0x20, tga); /* Scan from top left */
|
|
fwrite(temp, mSize.prod() * 4, 1, tga);
|
|
fclose(tga);
|
|
|
|
delete[] temp;
|
|
std::cout << "done." << std::endl;
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
|
|
Eigen::Vector3f project(const Eigen::Vector3f &obj,
|
|
const Eigen::Matrix4f &model,
|
|
const Eigen::Matrix4f &proj,
|
|
const Vector2i &viewportSize) {
|
|
Eigen::Vector4f tmp;
|
|
tmp << obj, 1;
|
|
|
|
tmp = model * tmp;
|
|
|
|
tmp = proj * tmp;
|
|
|
|
tmp = tmp.array() / tmp(3);
|
|
tmp = tmp.array() * 0.5f + 0.5f;
|
|
tmp(0) = tmp(0) * viewportSize.x();
|
|
tmp(1) = tmp(1) * viewportSize.y();
|
|
|
|
return tmp.head(3);
|
|
}
|
|
|
|
Eigen::Vector3f unproject(const Eigen::Vector3f &win,
|
|
const Eigen::Matrix4f &model,
|
|
const Eigen::Matrix4f &proj,
|
|
const Vector2i &viewportSize) {
|
|
Eigen::Matrix4f Inverse = (proj * model).inverse();
|
|
|
|
Eigen::Vector4f tmp;
|
|
tmp << win, 1;
|
|
tmp(0) = tmp(0) / viewportSize.x();
|
|
tmp(1) = tmp(1) / viewportSize.y();
|
|
tmp = tmp.array() * 2.0f - 1.0f;
|
|
|
|
Eigen::Vector4f obj = Inverse * tmp;
|
|
obj /= obj(3);
|
|
|
|
return obj.head(3);
|
|
}
|
|
|
|
Eigen::Matrix4f lookAt(const Eigen::Vector3f &origin,
|
|
const Eigen::Vector3f &target,
|
|
const Eigen::Vector3f &up) {
|
|
Eigen::Vector3f f = (target - origin).normalized();
|
|
Eigen::Vector3f s = f.cross(up).normalized();
|
|
Eigen::Vector3f u = s.cross(f);
|
|
|
|
Eigen::Matrix4f result = Eigen::Matrix4f::Identity();
|
|
result(0, 0) = s(0);
|
|
result(0, 1) = s(1);
|
|
result(0, 2) = s(2);
|
|
result(1, 0) = u(0);
|
|
result(1, 1) = u(1);
|
|
result(1, 2) = u(2);
|
|
result(2, 0) = -f(0);
|
|
result(2, 1) = -f(1);
|
|
result(2, 2) = -f(2);
|
|
result(0, 3) = -s.transpose() * origin;
|
|
result(1, 3) = -u.transpose() * origin;
|
|
result(2, 3) = f.transpose() * origin;
|
|
return result;
|
|
}
|
|
|
|
Eigen::Matrix4f ortho(float left, float right, float bottom,
|
|
float top, float nearVal, float farVal) {
|
|
Eigen::Matrix4f result = Eigen::Matrix4f::Identity();
|
|
result(0, 0) = 2.0f / (right - left);
|
|
result(1, 1) = 2.0f / (top - bottom);
|
|
result(2, 2) = -2.0f / (farVal - nearVal);
|
|
result(0, 3) = -(right + left) / (right - left);
|
|
result(1, 3) = -(top + bottom) / (top - bottom);
|
|
result(2, 3) = -(farVal + nearVal) / (farVal - nearVal);
|
|
return result;
|
|
}
|
|
|
|
Eigen::Matrix4f frustum(float left, float right, float bottom,
|
|
float top, float nearVal,
|
|
float farVal) {
|
|
Eigen::Matrix4f result = Eigen::Matrix4f::Zero();
|
|
result(0, 0) = (2.0f * nearVal) / (right - left);
|
|
result(1, 1) = (2.0f * nearVal) / (top - bottom);
|
|
result(0, 2) = (right + left) / (right - left);
|
|
result(1, 2) = (top + bottom) / (top - bottom);
|
|
result(2, 2) = -(farVal + nearVal) / (farVal - nearVal);
|
|
result(3, 2) = -1.0f;
|
|
result(2, 3) = -(2.0f * farVal * nearVal) / (farVal - nearVal);
|
|
return result;
|
|
}
|
|
|
|
Eigen::Matrix4f scale(const Eigen::Vector3f &v) {
|
|
return Eigen::Affine3f(Eigen::Scaling(v)).matrix();
|
|
}
|
|
|
|
Eigen::Matrix4f translate(const Eigen::Vector3f &v) {
|
|
return Eigen::Affine3f(Eigen::Translation<float, 3>(v)).matrix();
|
|
}
|
|
|
|
NAMESPACE_END(nanogui)
|