patch TinyRenderer so it software-renders in an OpenGL texture, for testing
fix texture support: flip texture to make OpenGL happy (lower-left is origin) add path prefix to .obj loader, so materials/textures are loaded ok.
This commit is contained in:
@@ -181,6 +181,7 @@
|
|||||||
include "../examples/OpenGLWindow"
|
include "../examples/OpenGLWindow"
|
||||||
include "../examples/ThirdPartyLibs/Gwen"
|
include "../examples/ThirdPartyLibs/Gwen"
|
||||||
include "../examples/SimpleOpenGL3"
|
include "../examples/SimpleOpenGL3"
|
||||||
|
include "../examples/TinyRenderer"
|
||||||
|
|
||||||
include "../examples/HelloWorld"
|
include "../examples/HelloWorld"
|
||||||
include "../examples/BasicDemo"
|
include "../examples/BasicDemo"
|
||||||
|
|||||||
13
data/floor.mtl
Normal file
13
data/floor.mtl
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
newmtl floor
|
||||||
|
Ns 10.0000
|
||||||
|
Ni 1.5000
|
||||||
|
d 1.0000
|
||||||
|
Tr 0.0000
|
||||||
|
Tf 1.0000 1.0000 1.0000
|
||||||
|
illum 2
|
||||||
|
Ka 0.0000 0.0000 0.0000
|
||||||
|
Kd 0.5880 0.5880 0.5880
|
||||||
|
Ks 0.0000 0.0000 0.0000
|
||||||
|
Ke 0.0000 0.0000 0.0000
|
||||||
|
map_Ka floor_diffuse.jpg
|
||||||
|
map_Kd cube.tga
|
||||||
18
data/floor.obj
Normal file
18
data/floor.obj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
o
|
||||||
|
mtllib floor.mtl
|
||||||
|
v -1 -1 -1
|
||||||
|
v 1 -1 -1
|
||||||
|
v 1 -1 1
|
||||||
|
v -1 -1 1
|
||||||
|
|
||||||
|
vt 0 0
|
||||||
|
vt .25 0
|
||||||
|
vt .25 .25
|
||||||
|
vt 0 .25
|
||||||
|
|
||||||
|
vn 0 1 0
|
||||||
|
usemtl floor
|
||||||
|
|
||||||
|
f 3/3/1 2/2/1 1/1/1
|
||||||
|
f 4/4/1 3/3/1 1/1/1
|
||||||
|
|
||||||
BIN
data/floor_diffuse.jpg
Normal file
BIN
data/floor_diffuse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
data/floor_diffuse.tga
Normal file
BIN
data/floor_diffuse.tga
Normal file
Binary file not shown.
BIN
data/floor_nm_tangent.tga
Normal file
BIN
data/floor_nm_tangent.tga
Normal file
Binary file not shown.
@@ -101,7 +101,7 @@ void ImportObjSetup::initPhysics()
|
|||||||
const char* filename = shape.material.diffuse_texname.c_str();
|
const char* filename = shape.material.diffuse_texname.c_str();
|
||||||
const unsigned char* image=0;
|
const unsigned char* image=0;
|
||||||
|
|
||||||
const char* prefix[]={"./","./data/","../data/","../../data/","../../../data/","../../../../data/"};
|
const char* prefix[]={ pathPrefix,"./","./data/","../data/","../../data/","../../../data/","../../../../data/"};
|
||||||
int numprefix = sizeof(prefix)/sizeof(const char*);
|
int numprefix = sizeof(prefix)/sizeof(const char*);
|
||||||
|
|
||||||
for (int i=0;!image && i<numprefix;i++)
|
for (int i=0;!image && i<numprefix;i++)
|
||||||
|
|||||||
@@ -532,18 +532,13 @@ int GLInstancingRenderer::registerTexture(const unsigned char* texels, int width
|
|||||||
|
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
b3Assert(glGetError() ==GL_NO_ERROR);
|
||||||
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width,height,0,GL_RGB,GL_UNSIGNED_BYTE,image);
|
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
|
||||||
|
|
||||||
glGenerateMipmap(GL_TEXTURE_2D);
|
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
|
||||||
|
|
||||||
InternalTextureHandle h;
|
InternalTextureHandle h;
|
||||||
h.m_glTexture = textureHandle;
|
h.m_glTexture = textureHandle;
|
||||||
h.m_width = width;
|
h.m_width = width;
|
||||||
h.m_height = height;
|
h.m_height = height;
|
||||||
|
|
||||||
m_data->m_textureHandles.push_back(h);
|
m_data->m_textureHandles.push_back(h);
|
||||||
|
updateTexture(textureIndex, texels);
|
||||||
return textureIndex;
|
return textureIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,13 +547,31 @@ void GLInstancingRenderer::updateTexture(int textureIndex, const unsigned cha
|
|||||||
{
|
{
|
||||||
if (textureIndex>=0)
|
if (textureIndex>=0)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
b3Assert(glGetError() ==GL_NO_ERROR);
|
||||||
InternalTextureHandle& h = m_data->m_textureHandles[textureIndex];
|
InternalTextureHandle& h = m_data->m_textureHandles[textureIndex];
|
||||||
|
|
||||||
|
//textures need to be flipped for OpenGL...
|
||||||
|
b3AlignedObjectArray<unsigned char> flippedTexels;
|
||||||
|
flippedTexels.resize(h.m_width* h.m_height * 3);
|
||||||
|
for (int i = 0; i < h.m_width; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < h.m_height; j++)
|
||||||
|
{
|
||||||
|
flippedTexels[(i + j*h.m_width) * 3] = texels[(i + (h.m_height-1-j)*h.m_width) * 3];
|
||||||
|
flippedTexels[(i + j*h.m_width) * 3+1] = texels[(i + (h.m_height - 1 - j)*h.m_width) * 3+1];
|
||||||
|
flippedTexels[(i + j*h.m_width) * 3+2] = texels[(i + (h.m_height - 1 - j)*h.m_width) * 3+1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D,h.m_glTexture);
|
glBindTexture(GL_TEXTURE_2D,h.m_glTexture);
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
b3Assert(glGetError() ==GL_NO_ERROR);
|
||||||
const GLubyte* image= (const GLubyte*)texels;
|
const GLubyte* image= (const GLubyte*)texels;
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, h.m_width,h.m_height,0,GL_RGB,GL_UNSIGNED_BYTE,image);
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, h.m_width,h.m_height,0,GL_RGB,GL_UNSIGNED_BYTE,&flippedTexels[0]);
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
b3Assert(glGetError() ==GL_NO_ERROR);
|
||||||
glGenerateMipmap(GL_TEXTURE_2D);
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
b3Assert(glGetError() ==GL_NO_ERROR);
|
b3Assert(glGetError() ==GL_NO_ERROR);
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
SYSCONF_LINK = g++
|
|
||||||
CPPFLAGS = -Wall -Wextra -Weffc++ -pedantic -std=c++98
|
|
||||||
LDFLAGS = -O3
|
|
||||||
LIBS = -lm
|
|
||||||
|
|
||||||
DESTDIR = ./
|
|
||||||
TARGET = main
|
|
||||||
|
|
||||||
OBJECTS := $(patsubst %.cpp,%.o,$(wildcard *.cpp))
|
|
||||||
|
|
||||||
all: $(DESTDIR)$(TARGET)
|
|
||||||
|
|
||||||
$(DESTDIR)$(TARGET): $(OBJECTS)
|
|
||||||
$(SYSCONF_LINK) -Wall $(LDFLAGS) -o $(DESTDIR)$(TARGET) $(OBJECTS) $(LIBS)
|
|
||||||
|
|
||||||
$(OBJECTS): %.o: %.cpp
|
|
||||||
$(SYSCONF_LINK) -Wall $(CPPFLAGS) -c $(CFLAGS) $< -o $@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm -f $(OBJECTS)
|
|
||||||
-rm -f $(TARGET)
|
|
||||||
-rm -f *.tga
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Tiny Renderer or how OpenGL works: software renderer in 500 lines of code
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
**Check [the wiki](https://github.com/ssloy/tinyrenderer/wiki/Lesson-1:-Bresenham%E2%80%99s-Line-Drawing-Algorithm) for the detailed lessons. My source code is irrelevant. Read the wiki and implement your own renderer. Only when you suffer through all the tiny details you will learn what is going on.**
|
|
||||||
|
|
||||||
**I do want to get emails for feedback (dmitry.sokolov@univ-lorraine.fr); do not hesitate to contact me if you have any questions.**
|
|
||||||
|
|
||||||
**If you are a teacher and willing to adopt this material for teaching your class your are very welcome, no authorization is needed, simply inform me by mail, it will help me to improve the course.**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
In this series of articles, I want to show the way OpenGL works by writing its clone (a much simplified one). Surprisingly enough, I often meet people who cannot overcome the initial hurdle of learning OpenGL / DirectX. Thus, I have prepared a short series of lectures, after which my students show quite good renderers.
|
|
||||||
|
|
||||||
So, the task is formulated as follows: using no third-party libraries (especially graphic ones), get something like this picture:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_Warning: this is a training material that will loosely repeat the structure of the OpenGL library. It will be a software renderer. **I do not want to show how to write applications for OpenGL. I want to show how OpenGL works.** I am deeply convinced that it is impossible to write efficient applications using 3D libraries without understanding this._
|
|
||||||
|
|
||||||
I will try to make the final code about 500 lines. My students need 10 to 20 programming hours to begin making such renderers. At the input, we get a test file with a polygonal wire + pictures with textures. At the output, we’ll get a rendered model. No graphical interface, the program simply generates an image.
|
|
||||||
|
|
||||||
|
|
||||||
Since the goal is to minimize external dependencies, I give my students just one class that allows working with [TGA](http://en.wikipedia.org/wiki/Truevision_TGA) files. It’s one of the simplest formats that supports images in RGB/RGBA/black and white formats. So, as a starting point, we’ll obtain a simple way to work with pictures. You should note that the only functionality available at the very beginning (in addition to loading and saving images) is the capability to set the color of one pixel.
|
|
||||||
|
|
||||||
There are no functions for drawing line segments and triangles. We’ll have to do all of this by hand. I provide my source code that I write in parallel with students. But I would not recommend using it, as this doesn’t make sense. The entire code is available on github, and [here](https://github.com/ssloy/tinyrenderer/tree/909fe20934ba5334144d2c748805690a1fa4c89f) you will find the source code I give to my students.
|
|
||||||
|
|
||||||
```C++
|
|
||||||
#include "tgaimage.h"
|
|
||||||
const TGAColor white = TGAColor(255, 255, 255, 255);
|
|
||||||
const TGAColor red = TGAColor(255, 0, 0, 255);
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
TGAImage image(100, 100, TGAImage::RGB);
|
|
||||||
image.set(52, 41, red);
|
|
||||||
image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
|
|
||||||
image.write_tga_file("output.tga");`
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
output.tga should look something like this:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Teaser: few examples made with the renderer
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
172
examples/TinyRenderer/TinyRenderer.cpp
Normal file
172
examples/TinyRenderer/TinyRenderer.cpp
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#include "TinyRenderer.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <limits>
|
||||||
|
#include <iostream>
|
||||||
|
#include "TinyRenderer/tgaimage.h"
|
||||||
|
#include "TinyRenderer/model.h"
|
||||||
|
#include "TinyRenderer/geometry.h"
|
||||||
|
#include "TinyRenderer/our_gl.h"
|
||||||
|
#include "../../Utils/b3ResourcePath.h"
|
||||||
|
#include "Bullet3Common/b3MinMax.h"
|
||||||
|
|
||||||
|
Vec3f light_dir_world(1,1,1);
|
||||||
|
|
||||||
|
|
||||||
|
struct Shader : public IShader {
|
||||||
|
|
||||||
|
Model* m_model;
|
||||||
|
Vec3f m_light_dir_local;
|
||||||
|
Matrix& m_modelView;
|
||||||
|
Matrix& m_projectionMatrix;
|
||||||
|
|
||||||
|
mat<2,3,float> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
|
||||||
|
mat<4,3,float> varying_tri; // triangle coordinates (clip coordinates), written by VS, read by FS
|
||||||
|
mat<3,3,float> varying_nrm; // normal per vertex to be interpolated by FS
|
||||||
|
mat<3,3,float> ndc_tri; // triangle in normalized device coordinates
|
||||||
|
|
||||||
|
Shader(Model* model, Vec3f light_dir_local, Matrix& modelView, Matrix& projectionMatrix)
|
||||||
|
:m_model(model),
|
||||||
|
m_light_dir_local(light_dir_local),
|
||||||
|
m_modelView(modelView),
|
||||||
|
m_projectionMatrix(projectionMatrix)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual Vec4f vertex(int iface, int nthvert) {
|
||||||
|
varying_uv.set_col(nthvert, m_model->uv(iface, nthvert));
|
||||||
|
varying_nrm.set_col(nthvert, proj<3>((m_projectionMatrix*m_modelView).invert_transpose()*embed<4>(m_model->normal(iface, nthvert), 0.f)));
|
||||||
|
Vec4f gl_Vertex = m_projectionMatrix*m_modelView*embed<4>(m_model->vert(iface, nthvert));
|
||||||
|
varying_tri.set_col(nthvert, gl_Vertex);
|
||||||
|
ndc_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3]));
|
||||||
|
return gl_Vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool fragment(Vec3f bar, TGAColor &color) {
|
||||||
|
Vec3f bn = (varying_nrm*bar).normalize();
|
||||||
|
Vec2f uv = varying_uv*bar;
|
||||||
|
|
||||||
|
mat<3,3,float> A;
|
||||||
|
A[0] = ndc_tri.col(1) - ndc_tri.col(0);
|
||||||
|
A[1] = ndc_tri.col(2) - ndc_tri.col(0);
|
||||||
|
A[2] = bn;
|
||||||
|
|
||||||
|
mat<3,3,float> AI = A.invert();
|
||||||
|
|
||||||
|
Vec3f i = AI * Vec3f(varying_uv[0][1] - varying_uv[0][0], varying_uv[0][2] - varying_uv[0][0], 0);
|
||||||
|
Vec3f j = AI * Vec3f(varying_uv[1][1] - varying_uv[1][0], varying_uv[1][2] - varying_uv[1][0], 0);
|
||||||
|
|
||||||
|
mat<3,3,float> B;
|
||||||
|
B.set_col(0, i.normalize());
|
||||||
|
B.set_col(1, j.normalize());
|
||||||
|
B.set_col(2, bn);
|
||||||
|
|
||||||
|
Vec3f n = (B*m_model->normal(uv)).normalize();
|
||||||
|
|
||||||
|
float diff = b3Min(b3Max(0.f, n*m_light_dir_local+0.6f),1.f);
|
||||||
|
color = m_model->diffuse(uv)*diff;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct TinyRenderObjectData
|
||||||
|
{
|
||||||
|
//Camera
|
||||||
|
Matrix m_viewMatrix;
|
||||||
|
Matrix m_projectionMatrix;
|
||||||
|
Matrix m_viewPortMatrix;
|
||||||
|
|
||||||
|
//Model (vertices, indices, textures, shader)
|
||||||
|
Matrix m_modelMatrix;
|
||||||
|
class Model* m_model;
|
||||||
|
class IShader* m_shader;
|
||||||
|
|
||||||
|
|
||||||
|
//Output
|
||||||
|
TGAImage m_rgbColorBuffer;
|
||||||
|
b3AlignedObjectArray<float> m_depthBuffer;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
TinyRenderObjectData::TinyRenderObjectData(int width, int height, const char* fileName)
|
||||||
|
:m_width(width),
|
||||||
|
m_height(height),
|
||||||
|
m_rgbColorBuffer(width,height,TGAImage::RGB),
|
||||||
|
m_model(0)
|
||||||
|
{
|
||||||
|
Vec3f eye(1,1,3);
|
||||||
|
Vec3f center(0,0,0);
|
||||||
|
Vec3f up(0,1,0);
|
||||||
|
|
||||||
|
m_viewMatrix = lookat(eye, center, up);
|
||||||
|
m_viewportMatrix = viewport(width/8, height/8, width*3/4, height*3/4);
|
||||||
|
m_projectionMatrix = projection(-1.f/(eye-center).norm());
|
||||||
|
|
||||||
|
|
||||||
|
m_depthBuffer.resize(width*height);
|
||||||
|
//todo(erwincoumans) move the file loading out of here
|
||||||
|
char relativeFileName[1024];
|
||||||
|
if (!b3ResourcePath::findResourcePath(fileName, relativeFileName, 1024))
|
||||||
|
{
|
||||||
|
printf("Cannot find file %s\n", fileName);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
m_model = new Model(relativeFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TinyRenderObjectData::~TinyRenderObjectData()
|
||||||
|
{
|
||||||
|
delete m_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TinyRenderer::renderObject(TinyRenderObjectData& renderData)
|
||||||
|
{
|
||||||
|
const char* fileName = "obj/floor.obj";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//new Model(relativeFileName);//argv[m]);
|
||||||
|
Model* model = renderData.m_model;
|
||||||
|
if (0==model)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int width = renderData.m_width;
|
||||||
|
const int height = renderData.m_height;
|
||||||
|
b3AlignedObjectArray<float>& zbuffer = renderData.m_depthBuffer;
|
||||||
|
|
||||||
|
//todo(erwincoumans) make this a separate call
|
||||||
|
for (int i=width*height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
|
||||||
|
|
||||||
|
TGAImage& frame = renderData.m_rgbColorBuffer;
|
||||||
|
|
||||||
|
//lookat(eye, center, up);
|
||||||
|
//viewport(width/8, height/8, width*3/4, height*3/4);
|
||||||
|
//projection(-1.f/(eye-center).norm());
|
||||||
|
|
||||||
|
Vec3f light_dir_local = proj<3>((renderData.m_projectionMatrix*renderData.m_viewMatrix*embed<4>(light_dir_world, 0.f))).normalize();
|
||||||
|
|
||||||
|
{
|
||||||
|
//for (int m=1; m<argc; m++) {
|
||||||
|
|
||||||
|
Shader shader(model, light_dir_local, renderData.m_viewMatrix, renderData.m_projectionMatrix);
|
||||||
|
for (int i=0; i<model->nfaces(); i++) {
|
||||||
|
for (int j=0; j<3; j++) {
|
||||||
|
shader.vertex(i, j);
|
||||||
|
}
|
||||||
|
triangle(shader.varying_tri, shader, frame, &zbuffer[0], renderData.m_viewportMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//frame.flip_vertically(); // to place the origin in the bottom left corner of the image
|
||||||
|
//frame.write_tga_file("framebuffer.tga");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
39
examples/TinyRenderer/TinyRenderer.h
Normal file
39
examples/TinyRenderer/TinyRenderer.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef TINY_RENDERER_H
|
||||||
|
#define TINY_RENDERER_H
|
||||||
|
|
||||||
|
//#include "TinyRenderer/our_gl.h"
|
||||||
|
#include "TinyRenderer/geometry.h"
|
||||||
|
#include "Bullet3Common/b3AlignedObjectArray.h"
|
||||||
|
#include "TinyRenderer/tgaimage.h"
|
||||||
|
|
||||||
|
struct TinyRenderObjectData
|
||||||
|
{
|
||||||
|
//Camera
|
||||||
|
Matrix m_viewMatrix;
|
||||||
|
Matrix m_projectionMatrix;
|
||||||
|
Matrix m_viewportMatrix;
|
||||||
|
|
||||||
|
//Model (vertices, indices, textures, shader)
|
||||||
|
Matrix m_modelMatrix;
|
||||||
|
class Model* m_model;
|
||||||
|
//class IShader* m_shader; todo(erwincoumans) expose the shader, for now we use a default shader
|
||||||
|
|
||||||
|
//Output
|
||||||
|
int m_width;
|
||||||
|
int m_height;
|
||||||
|
TGAImage m_rgbColorBuffer;
|
||||||
|
b3AlignedObjectArray<float> m_depthBuffer;
|
||||||
|
|
||||||
|
TinyRenderObjectData(int width, int height, const char* objFileName);
|
||||||
|
virtual ~TinyRenderObjectData();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class TinyRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void renderObject(TinyRenderObjectData& renderData);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TINY_RENDERER_Hbla
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#ifndef __GEOMETRY_H__
|
#ifndef __GEOMETRY_H__
|
||||||
#define __GEOMETRY_H__
|
#define __GEOMETRY_H__
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vector>
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <iostream>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
|
||||||
template<size_t DimCols,size_t DimRows,typename T> class mat;
|
template<size_t DimCols,size_t DimRows,typename T> class mat;
|
||||||
|
|
||||||
@@ -85,14 +85,14 @@ template<size_t LEN,size_t DIM, typename T> vec<LEN,T> proj(const vec<DIM,T> &v)
|
|||||||
template <typename T> vec<3,T> cross(vec<3,T> v1, vec<3,T> v2) {
|
template <typename T> vec<3,T> cross(vec<3,T> v1, vec<3,T> v2) {
|
||||||
return vec<3,T>(v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x);
|
return vec<3,T>(v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x);
|
||||||
}
|
}
|
||||||
|
#if 0
|
||||||
template <size_t DIM, typename T> std::ostream& operator<<(std::ostream& out, vec<DIM,T>& v) {
|
template <size_t DIM, typename T> std::ostream& operator<<(std::ostream& out, vec<DIM,T>& v) {
|
||||||
for(unsigned int i=0; i<DIM; i++) {
|
for(unsigned int i=0; i<DIM; i++) {
|
||||||
out << v[i] << " " ;
|
out << v[i] << " " ;
|
||||||
}
|
}
|
||||||
return out ;
|
return out ;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
template<size_t DIM,typename T> struct dt {
|
template<size_t DIM,typename T> struct dt {
|
||||||
@@ -204,11 +204,12 @@ template<size_t DimRows,size_t DimCols,typename T>mat<DimCols,DimRows,T> operato
|
|||||||
return lhs;
|
return lhs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
template <size_t DimRows,size_t DimCols,class T> std::ostream& operator<<(std::ostream& out, mat<DimRows,DimCols,T>& m) {
|
template <size_t DimRows,size_t DimCols,class T> std::ostream& operator<<(std::ostream& out, mat<DimRows,DimCols,T>& m) {
|
||||||
for (size_t i=0; i<DimRows; i++) out << m[i] << std::endl;
|
for (size_t i=0; i<DimRows; i++) out << m[i] << std::endl;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
typedef vec<2, float> Vec2f;
|
typedef vec<2, float> Vec2f;
|
||||||
|
|||||||
@@ -1,94 +1,209 @@
|
|||||||
#include <vector>
|
#include "OpenGLWindow/SimpleOpenGL3App.h"
|
||||||
#include <limits>
|
#include "Bullet3Common/b3Quaternion.h"
|
||||||
#include <iostream>
|
#include "Bullet3Common/b3CommandLineArgs.h"
|
||||||
#include "tgaimage.h"
|
#include "assert.h"
|
||||||
#include "model.h"
|
#include <stdio.h>
|
||||||
#include "geometry.h"
|
|
||||||
#include "our_gl.h"
|
|
||||||
|
|
||||||
Model *model = NULL;
|
char* gVideoFileName = 0;
|
||||||
|
char* gPngFileName = 0;
|
||||||
|
|
||||||
const int width = 800;
|
static b3WheelCallback sOldWheelCB = 0;
|
||||||
const int height = 800;
|
static b3ResizeCallback sOldResizeCB = 0;
|
||||||
|
static b3MouseMoveCallback sOldMouseMoveCB = 0;
|
||||||
|
static b3MouseButtonCallback sOldMouseButtonCB = 0;
|
||||||
|
static b3KeyboardCallback sOldKeyboardCB = 0;
|
||||||
|
//static b3RenderCallback sOldRenderCB = 0;
|
||||||
|
|
||||||
Vec3f light_dir(1,1,1);
|
float gWidth = 0 ;
|
||||||
Vec3f eye(1,1,3);
|
float gHeight = 0;
|
||||||
Vec3f center(0,0,0);
|
|
||||||
Vec3f up(0,1,0);
|
|
||||||
|
|
||||||
struct Shader : public IShader {
|
void MyWheelCallback(float deltax, float deltay)
|
||||||
mat<2,3,float> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
|
{
|
||||||
mat<4,3,float> varying_tri; // triangle coordinates (clip coordinates), written by VS, read by FS
|
if (sOldWheelCB)
|
||||||
mat<3,3,float> varying_nrm; // normal per vertex to be interpolated by FS
|
sOldWheelCB(deltax,deltay);
|
||||||
mat<3,3,float> ndc_tri; // triangle in normalized device coordinates
|
}
|
||||||
|
void MyResizeCallback( float width, float height)
|
||||||
|
{
|
||||||
|
gWidth = width;
|
||||||
|
gHeight = height;
|
||||||
|
|
||||||
virtual Vec4f vertex(int iface, int nthvert) {
|
if (sOldResizeCB)
|
||||||
varying_uv.set_col(nthvert, model->uv(iface, nthvert));
|
sOldResizeCB(width,height);
|
||||||
varying_nrm.set_col(nthvert, proj<3>((Projection*ModelView).invert_transpose()*embed<4>(model->normal(iface, nthvert), 0.f)));
|
}
|
||||||
Vec4f gl_Vertex = Projection*ModelView*embed<4>(model->vert(iface, nthvert));
|
void MyMouseMoveCallback( float x, float y)
|
||||||
varying_tri.set_col(nthvert, gl_Vertex);
|
{
|
||||||
ndc_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3]));
|
printf("Mouse Move: %f, %f\n", x,y);
|
||||||
return gl_Vertex;
|
|
||||||
|
if (sOldMouseMoveCB)
|
||||||
|
sOldMouseMoveCB(x,y);
|
||||||
|
}
|
||||||
|
void MyMouseButtonCallback(int button, int state, float x, float y)
|
||||||
|
{
|
||||||
|
if (sOldMouseButtonCB)
|
||||||
|
sOldMouseButtonCB(button,state,x,y);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool fragment(Vec3f bar, TGAColor &color) {
|
|
||||||
Vec3f bn = (varying_nrm*bar).normalize();
|
|
||||||
Vec2f uv = varying_uv*bar;
|
|
||||||
|
|
||||||
mat<3,3,float> A;
|
void MyKeyboardCallback(int keycode, int state)
|
||||||
A[0] = ndc_tri.col(1) - ndc_tri.col(0);
|
{
|
||||||
A[1] = ndc_tri.col(2) - ndc_tri.col(0);
|
//keycodes are in examples/CommonInterfaces/CommonWindowInterface.h
|
||||||
A[2] = bn;
|
//for example B3G_ESCAPE for escape key
|
||||||
|
//state == 1 for pressed, state == 0 for released.
|
||||||
mat<3,3,float> AI = A.invert();
|
// use app->m_window->isModifiedPressed(...) to check for shift, escape and alt keys
|
||||||
|
printf("MyKeyboardCallback received key:%c in state %d\n",keycode,state);
|
||||||
Vec3f i = AI * Vec3f(varying_uv[0][1] - varying_uv[0][0], varying_uv[0][2] - varying_uv[0][0], 0);
|
if (sOldKeyboardCB)
|
||||||
Vec3f j = AI * Vec3f(varying_uv[1][1] - varying_uv[1][0], varying_uv[1][2] - varying_uv[1][0], 0);
|
sOldKeyboardCB(keycode,state);
|
||||||
|
|
||||||
mat<3,3,float> B;
|
|
||||||
B.set_col(0, i.normalize());
|
|
||||||
B.set_col(1, j.normalize());
|
|
||||||
B.set_col(2, bn);
|
|
||||||
|
|
||||||
Vec3f n = (B*model->normal(uv)).normalize();
|
|
||||||
|
|
||||||
float diff = std::max(0.f, n*light_dir);
|
|
||||||
color = model->diffuse(uv)*diff;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
#include "TinyRenderer.h"
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char* argv[])
|
||||||
if (2>argc) {
|
{
|
||||||
std::cerr << "Usage: " << argv[0] << " obj/model.obj" << std::endl;
|
b3CommandLineArgs myArgs(argc,argv);
|
||||||
return 1;
|
|
||||||
|
|
||||||
|
|
||||||
|
SimpleOpenGL3App* app = new SimpleOpenGL3App("SimpleOpenGL3App",640,480,true);
|
||||||
|
|
||||||
|
app->m_instancingRenderer->getActiveCamera()->setCameraDistance(13);
|
||||||
|
app->m_instancingRenderer->getActiveCamera()->setCameraPitch(0);
|
||||||
|
app->m_instancingRenderer->getActiveCamera()->setCameraTargetPosition(0,0,0);
|
||||||
|
sOldKeyboardCB = app->m_window->getKeyboardCallback();
|
||||||
|
app->m_window->setKeyboardCallback(MyKeyboardCallback);
|
||||||
|
sOldMouseMoveCB = app->m_window->getMouseMoveCallback();
|
||||||
|
app->m_window->setMouseMoveCallback(MyMouseMoveCallback);
|
||||||
|
sOldMouseButtonCB = app->m_window->getMouseButtonCallback();
|
||||||
|
app->m_window->setMouseButtonCallback(MyMouseButtonCallback);
|
||||||
|
sOldWheelCB = app->m_window->getWheelCallback();
|
||||||
|
app->m_window->setWheelCallback(MyWheelCallback);
|
||||||
|
sOldResizeCB = app->m_window->getResizeCallback();
|
||||||
|
app->m_window->setResizeCallback(MyResizeCallback);
|
||||||
|
|
||||||
|
int textureWidth = gWidth;
|
||||||
|
int textureHeight = gHeight;
|
||||||
|
|
||||||
|
TinyRenderObjectData renderData(textureWidth, textureHeight, "african_head/african_head.obj");//floor.obj");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
myArgs.GetCmdLineArgument("mp4_file",gVideoFileName);
|
||||||
|
if (gVideoFileName)
|
||||||
|
app->dumpFramesToVideo(gVideoFileName);
|
||||||
|
|
||||||
|
myArgs.GetCmdLineArgument("png_file",gPngFileName);
|
||||||
|
char fileName[1024];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
unsigned char* image=new unsigned char[textureWidth*textureHeight*4];
|
||||||
|
|
||||||
|
|
||||||
|
int textureHandle = app->m_renderer->registerTexture(image,textureWidth,textureHeight);
|
||||||
|
|
||||||
|
int cubeIndex = app->registerCubeShape(1,1,1);
|
||||||
|
|
||||||
|
b3Vector3 pos = b3MakeVector3(0,0,0);
|
||||||
|
b3Quaternion orn(0,0,0,1);
|
||||||
|
b3Vector3 color=b3MakeVector3(1,0,0);
|
||||||
|
b3Vector3 scaling=b3MakeVector3 (1,1,1);
|
||||||
|
app->m_renderer->registerGraphicsInstance(cubeIndex,pos,orn,color,scaling);
|
||||||
|
app->m_renderer->writeTransforms();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
static int frameCount = 0;
|
||||||
|
frameCount++;
|
||||||
|
if (gPngFileName)
|
||||||
|
{
|
||||||
|
printf("gPngFileName=%s\n",gPngFileName);
|
||||||
|
|
||||||
|
sprintf(fileName,"%s%d.png",gPngFileName,frameCount++);
|
||||||
|
app->dumpNextFrameToPng(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
float *zbuffer = new float[width*height];
|
app->m_instancingRenderer->init();
|
||||||
for (int i=width*height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
|
app->m_instancingRenderer->updateCamera();
|
||||||
|
|
||||||
TGAImage frame(width, height, TGAImage::RGB);
|
float projMat[16];
|
||||||
lookat(eye, center, up);
|
app->m_instancingRenderer->getActiveCamera()->getCameraProjectionMatrix(projMat);
|
||||||
viewport(width/8, height/8, width*3/4, height*3/4);
|
float viewMat[16];
|
||||||
projection(-1.f/(eye-center).norm());
|
app->m_instancingRenderer->getActiveCamera()->getCameraViewMatrix(viewMat);
|
||||||
light_dir = proj<3>((Projection*ModelView*embed<4>(light_dir, 0.f))).normalize();
|
for (int i=0;i<4;i++)
|
||||||
|
{
|
||||||
|
for (int j=0;j<4;j++)
|
||||||
|
{
|
||||||
|
renderData.m_viewMatrix[i][j] = viewMat[i+4*j];
|
||||||
|
//renderData.m_projectionMatrix[i][j] = projMat[i+4*j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int m=1; m<argc; m++) {
|
for(int y=0;y<textureHeight;++y)
|
||||||
model = new Model(argv[m]);
|
{
|
||||||
Shader shader;
|
unsigned char* pi=image+(y)*textureWidth*3;
|
||||||
for (int i=0; i<model->nfaces(); i++) {
|
for(int x=0;x<textureWidth;++x)
|
||||||
for (int j=0; j<3; j++) {
|
{
|
||||||
shader.vertex(i, j);
|
|
||||||
}
|
|
||||||
triangle(shader.varying_tri, shader, frame, zbuffer);
|
|
||||||
}
|
|
||||||
delete model;
|
|
||||||
}
|
|
||||||
frame.flip_vertically(); // to place the origin in the bottom left corner of the image
|
|
||||||
frame.write_tga_file("framebuffer.tga");
|
|
||||||
|
|
||||||
delete [] zbuffer;
|
TGAColor color;
|
||||||
|
color.bgra[0] = 255;
|
||||||
|
color.bgra[1] = 255;
|
||||||
|
color.bgra[2] = 255;
|
||||||
|
color.bgra[3] = 255;
|
||||||
|
|
||||||
|
renderData.m_rgbColorBuffer.set(x,y,color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TinyRenderer::renderObject(renderData);
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
//update the texels of the texture using a simple pattern, animated using frame index
|
||||||
|
for(int y=0;y<textureHeight;++y)
|
||||||
|
{
|
||||||
|
unsigned char* pi=image+(y)*textureWidth*3;
|
||||||
|
for(int x=0;x<textureWidth;++x)
|
||||||
|
{
|
||||||
|
|
||||||
|
TGAColor color = renderData.m_rgbColorBuffer.get(x,y);
|
||||||
|
pi[0] = color.bgra[2];
|
||||||
|
pi[1] = color.bgra[1];
|
||||||
|
pi[2] = color.bgra[0];
|
||||||
|
pi+=3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
|
||||||
|
//update the texels of the texture using a simple pattern, animated using frame index
|
||||||
|
for(int y=0;y<textureHeight;++y)
|
||||||
|
{
|
||||||
|
const int t=(y+frameCount)>>4;
|
||||||
|
unsigned char* pi=image+y*textureWidth*3;
|
||||||
|
for(int x=0;x<textureWidth;++x)
|
||||||
|
{
|
||||||
|
const int s=x>>4;
|
||||||
|
const unsigned char b=180;
|
||||||
|
unsigned char c=b+((s+(t&1))&1)*(255-b);
|
||||||
|
pi[0]=pi[1]=pi[2]=pi[3]=c;pi+=3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
app->m_renderer->activateTexture(textureHandle);
|
||||||
|
app->m_renderer->updateTexture(textureHandle,image);
|
||||||
|
|
||||||
|
float color[4] = {1,1,1,1};
|
||||||
|
app->m_primRenderer->drawTexturedRect(0,0,gWidth/3,gHeight/3,color,0,0,1,1,true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app->m_renderer->renderScene();
|
||||||
|
app->drawGrid();
|
||||||
|
char bla[1024];
|
||||||
|
sprintf(bla,"Simple test frame %d", frameCount);
|
||||||
|
|
||||||
|
app->drawText(bla,10,10);
|
||||||
|
app->swapBuffer();
|
||||||
|
} while (!app->m_window->requestedExit());
|
||||||
|
|
||||||
|
|
||||||
|
delete app;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include "our_gl.h"
|
#include "our_gl.h"
|
||||||
|
#include "Bullet3Common/b3MinMax.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Matrix ModelView;
|
|
||||||
Matrix Viewport;
|
|
||||||
Matrix Projection;
|
|
||||||
|
|
||||||
IShader::~IShader() {}
|
IShader::~IShader() {}
|
||||||
|
|
||||||
void viewport(int x, int y, int w, int h) {
|
Matrix viewport(int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
Matrix Viewport;
|
||||||
Viewport = Matrix::identity();
|
Viewport = Matrix::identity();
|
||||||
Viewport[0][3] = x+w/2.f;
|
Viewport[0][3] = x+w/2.f;
|
||||||
Viewport[1][3] = y+h/2.f;
|
Viewport[1][3] = y+h/2.f;
|
||||||
@@ -17,14 +19,17 @@ void viewport(int x, int y, int w, int h) {
|
|||||||
Viewport[0][0] = w/2.f;
|
Viewport[0][0] = w/2.f;
|
||||||
Viewport[1][1] = h/2.f;
|
Viewport[1][1] = h/2.f;
|
||||||
Viewport[2][2] = 0;
|
Viewport[2][2] = 0;
|
||||||
|
return Viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
void projection(float coeff) {
|
Matrix projection(float coeff) {
|
||||||
|
Matrix Projection;
|
||||||
Projection = Matrix::identity();
|
Projection = Matrix::identity();
|
||||||
Projection[3][2] = coeff;
|
Projection[3][2] = coeff;
|
||||||
|
return Projection;
|
||||||
}
|
}
|
||||||
|
|
||||||
void lookat(Vec3f eye, Vec3f center, Vec3f up) {
|
Matrix lookat(Vec3f eye, Vec3f center, Vec3f up) {
|
||||||
Vec3f z = (eye-center).normalize();
|
Vec3f z = (eye-center).normalize();
|
||||||
Vec3f x = cross(up,z).normalize();
|
Vec3f x = cross(up,z).normalize();
|
||||||
Vec3f y = cross(z,x).normalize();
|
Vec3f y = cross(z,x).normalize();
|
||||||
@@ -36,7 +41,9 @@ void lookat(Vec3f eye, Vec3f center, Vec3f up) {
|
|||||||
Minv[2][i] = z[i];
|
Minv[2][i] = z[i];
|
||||||
Tr[i][3] = -center[i];
|
Tr[i][3] = -center[i];
|
||||||
}
|
}
|
||||||
|
Matrix ModelView;
|
||||||
ModelView = Minv*Tr;
|
ModelView = Minv*Tr;
|
||||||
|
return ModelView;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec3f barycentric(Vec2f A, Vec2f B, Vec2f C, Vec2f P) {
|
Vec3f barycentric(Vec2f A, Vec2f B, Vec2f C, Vec2f P) {
|
||||||
@@ -52,8 +59,8 @@ Vec3f barycentric(Vec2f A, Vec2f B, Vec2f C, Vec2f P) {
|
|||||||
return Vec3f(-1,1,1); // in this case generate negative coordinates, it will be thrown away by the rasterizator
|
return Vec3f(-1,1,1); // in this case generate negative coordinates, it will be thrown away by the rasterizator
|
||||||
}
|
}
|
||||||
|
|
||||||
void triangle(mat<4,3,float> &clipc, IShader &shader, TGAImage &image, float *zbuffer) {
|
void triangle(mat<4,3,float> &clipc, IShader &shader, TGAImage &image, float *zbuffer, const Matrix& viewPortMatrix) {
|
||||||
mat<3,4,float> pts = (Viewport*clipc).transpose(); // transposed to ease access to each of the points
|
mat<3,4,float> pts = (viewPortMatrix*clipc).transpose(); // transposed to ease access to each of the points
|
||||||
mat<3,2,float> pts2;
|
mat<3,2,float> pts2;
|
||||||
for (int i=0; i<3; i++) pts2[i] = proj<2>(pts[i]/pts[i][3]);
|
for (int i=0; i<3; i++) pts2[i] = proj<2>(pts[i]/pts[i][3]);
|
||||||
|
|
||||||
@@ -62,8 +69,8 @@ void triangle(mat<4,3,float> &clipc, IShader &shader, TGAImage &image, float *zb
|
|||||||
Vec2f clamp(image.get_width()-1, image.get_height()-1);
|
Vec2f clamp(image.get_width()-1, image.get_height()-1);
|
||||||
for (int i=0; i<3; i++) {
|
for (int i=0; i<3; i++) {
|
||||||
for (int j=0; j<2; j++) {
|
for (int j=0; j<2; j++) {
|
||||||
bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts2[i][j]));
|
bboxmin[j] = b3Max(0.f, b3Min(bboxmin[j], pts2[i][j]));
|
||||||
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts2[i][j]));
|
bboxmax[j] = b3Min(clamp[j], b3Max(bboxmax[j], pts2[i][j]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vec2i P;
|
Vec2i P;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
#include "tgaimage.h"
|
#include "tgaimage.h"
|
||||||
#include "geometry.h"
|
#include "geometry.h"
|
||||||
|
|
||||||
extern Matrix ModelView;
|
|
||||||
extern Matrix Projection;
|
|
||||||
|
|
||||||
void viewport(int x, int y, int w, int h);
|
|
||||||
void projection(float coeff=0.f); // coeff = -1/c
|
|
||||||
void lookat(Vec3f eye, Vec3f center, Vec3f up);
|
Matrix viewport(int x, int y, int w, int h);
|
||||||
|
Matrix projection(float coeff=0.f); // coeff = -1/c
|
||||||
|
Matrix lookat(Vec3f eye, Vec3f center, Vec3f up);
|
||||||
|
|
||||||
struct IShader {
|
struct IShader {
|
||||||
virtual ~IShader();
|
virtual ~IShader();
|
||||||
@@ -17,6 +17,6 @@ struct IShader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//void triangle(Vec4f *pts, IShader &shader, TGAImage &image, float *zbuffer);
|
//void triangle(Vec4f *pts, IShader &shader, TGAImage &image, float *zbuffer);
|
||||||
void triangle(mat<4,3,float> &pts, IShader &shader, TGAImage &image, float *zbuffer);
|
void triangle(mat<4,3,float> &pts, IShader &shader, TGAImage &image, float *zbuffer, const Matrix& viewPortMatrix);
|
||||||
#endif //__OUR_GL_H__
|
#endif //__OUR_GL_H__
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user