diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index c6b757a8..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,9 +0,0 @@ -image: - file: Dockerfile -tasks: -- command: > - cmake -Bbuild && - cmake --build build --parallel && - build/tinyrenderer obj/diablo3_pose/diablo3_pose.obj obj/floor.obj && - convert framebuffer.tga framebuffer.png && - open framebuffer.png diff --git a/CMakeLists.txt b/CMakeLists.txt index c3c3372a..f4a0d117 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ endif() find_package(OpenMP COMPONENTS CXX) -set(SOURCES main.cpp model.cpp our_gl.cpp tgaimage.cpp) +set(SOURCES main.cpp graphics.cpp model.cpp tgaimage.cpp) add_executable(${PROJECT_NAME} ${SOURCES}) target_link_libraries(${PROJECT_NAME} PRIVATE $<$:OpenMP::OpenMP_CXX>) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bfd2bfb1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM gitpod/workspace-full - -USER root -# add your tools here -RUN apt-get update && apt-get install -y \ - imagemagick diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 95dbe5b4..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,13 +0,0 @@ -Tiny Renderer, https://github.com/ssloy/tinyrenderer -Copyright Dmitry V. Sokolov - -This software is provided 'as-is', without any express or implied warranty. -In no event will the authors be held liable for any damages arising from the use of this software. -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it freely, -subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - diff --git a/README.md b/README.md deleted file mode 100644 index 069b0fe3..00000000 --- a/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Tiny Renderer or how OpenGL works: software rendering in 500 lines of code - -# Check [the wiki](https://github.com/ssloy/tinyrenderer/wiki) for the detailed lessons. - -## compilation - -```sh -git clone https://github.com/ssloy/tinyrenderer.git && -cd tinyrenderer && -cmake -Bbuild && -cmake --build build -j && -build/tinyrenderer obj/diablo3_pose/diablo3_pose.obj obj/floor.obj -``` -The rendered image is saved to `framebuffer.tga`. - -You can open the project in Gitpod, a free online dev environment for GitHub: -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ssloy/tinyrenderer) - -On open, the editor will compile & run the program as well as open the resulting image in the editor's preview. -Just change the code in the editor and rerun the script (use the terminal's history) to see updated images. - -## The main idea - -**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.** - -In [this series of articles](https://github.com/ssloy/tinyrenderer/wiki), I want to show how 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: - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/africanhead.png) - -_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, and 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 ability to set one pixel's color. - -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.write_tga_file("output.tga");` - return 0; -} -``` - -output.tga should look something like this: - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/reddot.png) - - -# Teaser: few examples made with the renderer - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/demon.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/diablo-glow.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/boggie.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/diablo-ssao.png) diff --git a/graphics.cpp b/graphics.cpp new file mode 100644 index 00000000..7876810b --- /dev/null +++ b/graphics.cpp @@ -0,0 +1,45 @@ +#include +#include "graphics.h" + +mat<4,4> ModelView, Viewport, Perspective; + +void lookat(const vec3 eye, const vec3 center, const vec3 up) { + vec3 n = normalized(eye-center); + vec3 l = normalized(cross(up,n)); + vec3 m = normalized(cross(n, l)); + ModelView = mat<4,4>{{{l.x,l.y,l.z,0}, {m.x,m.y,m.z,0}, {n.x,n.y,n.z,0}, {0,0,0,1}}} * + mat<4,4>{{{1,0,0,-center.x}, {0,1,0,-center.y}, {0,0,1,-center.z}, {0,0,0,1}}}; +} + +void perspective(const double f) { + Perspective = {{{1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0, -1/f,1}}}; +} + +void viewport(const int x, const int y, const int w, const int h) { + Viewport = {{{w/2., 0, 0, x+w/2.}, {0, h/2., 0, y+h/2.}, {0,0,1,0}, {0,0,0,1}}}; +} + +void rasterize(const vec4 clip[3], const IShader &shader, std::vector &zbuffer, TGAImage &framebuffer) { + vec4 ndc[3] = { clip[0]/clip[0].w, clip[1]/clip[1].w, clip[2]/clip[2].w }; // normalized device coordinates + vec2 screen[3] = { (Viewport*ndc[0]).xy(), (Viewport*ndc[1]).xy(), (Viewport*ndc[2]).xy() }; // screen coordinates + + mat<3,3> ABC = {{ {screen[0].x, screen[0].y, 1.}, {screen[1].x, screen[1].y, 1.}, {screen[2].x, screen[2].y, 1.} }}; + if (ABC.det()<1) return; // backface culling + discarding triangles that cover less than a pixel + + auto [bbminx,bbmaxx] = std::minmax({screen[0].x, screen[1].x, screen[2].x}); // bounding box for the triangle + auto [bbminy,bbmaxy] = std::minmax({screen[0].y, screen[1].y, screen[2].y}); // defined by its top left and bottom right corners +#pragma omp parallel for + for (int x=std::max(bbminx, 0); x<=std::min(bbmaxx, framebuffer.width()-1); x++) { // clip the bounding box by the screen + for (int y=std::max(bbminy, 0); y<=std::min(bbmaxy, framebuffer.height()-1); y++) { + vec3 bc = ABC.invert_transpose() * vec3{static_cast(x), static_cast(y), 1.}; // barycentric coordinates of {x,y} w.r.t the triangle + if (bc.x<0 || bc.y<0 || bc.z<0) continue; // negative barycentric coordinate => the pixel is outside the triangle + double z = bc * vec3{ ndc[0].z, ndc[1].z, ndc[2].z }; + if (z <= zbuffer[x+y*framebuffer.width()]) continue; + auto [discard, color] = shader.fragment(bc); + if (discard) continue; // fragment shader can discard current fragment + zbuffer[x+y*framebuffer.width()] = z; + framebuffer.set(x, y, color); + } + } +} + diff --git a/our_gl.h b/graphics.h similarity index 59% rename from our_gl.h rename to graphics.h index 44433433..d4251803 100644 --- a/our_gl.h +++ b/graphics.h @@ -1,16 +1,16 @@ #include "tgaimage.h" #include "geometry.h" -void viewport(const int x, const int y, const int w, const int h); -void projection(const double coeff=0); // coeff = -1/c void lookat(const vec3 eye, const vec3 center, const vec3 up); +void perspective(const double f); +void viewport(const int x, const int y, const int w, const int h); struct IShader { static TGAColor sample2D(const TGAImage &img, const vec2 &uvf) { return img.get(uvf[0] * img.width(), uvf[1] * img.height()); } - virtual bool fragment(const vec3 bar, TGAColor &color) const = 0; + virtual std::pair fragment(const vec3 bar) const = 0; }; -void rasterize(const vec4 clip_verts[3], const IShader &shader, TGAImage &image, std::vector &zbuffer); +void rasterize(const vec4 clip[3], const IShader &shader, std::vector &zbuffer, TGAImage &framebuffer); diff --git a/main.cpp b/main.cpp index eda62883..015b361a 100644 --- a/main.cpp +++ b/main.cpp @@ -1,54 +1,42 @@ #include +#include "graphics.h" #include "model.h" -#include "our_gl.h" -extern mat<4,4> ModelView; // "OpenGL" state matrices -extern mat<4,4> Projection; +extern mat<4,4> ModelView, Perspective; // "OpenGL" state matrices -struct Shader : IShader { +struct FlatShader : IShader { const Model &model; - vec3 uniform_l; // light direction in view coordinates - mat<3,2> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader - mat<3,3> varying_nrm; // normal per vertex to be interpolated by FS - mat<3,3> view_tri; // triangle in view coordinates + vec3 uniform_l; // light direction in clip coordinates + vec3 tri_eye[3]; - Shader(const vec3 l, const Model &m) : model(m) { + FlatShader(const vec3 l, const Model &m) : model(m) { uniform_l = normalized((ModelView*vec4{l.x, l.y, l.z, 0.}).xyz()); // transform the light vector to view coordinates } - virtual void vertex(const int iface, const int nthvert, vec4& gl_Position) { - vec3 n = model.normal(iface, nthvert); - vec3 v = model.vert(iface, nthvert); - gl_Position = ModelView * vec4{v.x, v.y, v.z, 1.}; - varying_uv[nthvert] = model.uv(iface, nthvert); - varying_nrm[nthvert] = (ModelView.invert_transpose() * vec4{n.x, n.y, n.z, 0.}).xyz(); - view_tri[nthvert] = gl_Position.xyz(); - gl_Position = Projection * gl_Position; + virtual vec4 vertex(const int face, const int vert) { + vec3 v = model.vert(face, vert); // current vertex in object coordinates + vec4 gl_Position = ModelView * vec4{v.x, v.y, v.z, 1.}; + tri_eye[vert] = gl_Position.xyz(); // in eye coordinates + return Perspective * gl_Position; // in clip coordinates } - virtual bool fragment(const vec3 bar, TGAColor &gl_FragColor) const { - vec3 bn = normalized(bar * varying_nrm); // per-vertex normal interpolation - vec2 uv = bar * varying_uv; // tex coord interpolation + virtual std::pair fragment(const vec3 bar) const { + TGAColor gl_FragColor; - mat<3,3> AI = mat<3,3>{ {view_tri[1] - view_tri[0], view_tri[2] - view_tri[0], bn} }.invert(); // for the math refer to the tangent space normal mapping lecture - vec3 i = AI * vec3{varying_uv[1].x - varying_uv[0].x, varying_uv[2].x - varying_uv[0].x, 0}; // https://github.com/ssloy/tinyrenderer/wiki/Lesson-6bis-tangent-space-normal-mapping - vec3 j = AI * vec3{varying_uv[1].y - varying_uv[0].y, varying_uv[2].y - varying_uv[0].y, 0}; - mat<3,3> B = mat<3,3>{ { normalized(i), normalized(j), bn } }.transpose(); + vec3 n = normalized(cross(tri_eye[1]-tri_eye[0], tri_eye[2]-tri_eye[0])); // triangle normal in eye coordinates + vec3 r = normalized(n * (n * uniform_l)*2 - uniform_l); // reflected light direction - vec3 n = normalized(B * model.normal(uv)); // transform the normal from the texture to the tangent space - vec3 r = normalized(n * (n * uniform_l)*2 - uniform_l); // reflected light direction, specular mapping is described here: https://github.com/ssloy/tinyrenderer/wiki/Lesson-6-Shaders-for-the-software-renderer - double diff = std::max(0., n * uniform_l); // diffuse light intensity - double spec = std::pow(std::max(-r.z, 0.), 5+sample2D(model.specular(), uv)[0]); // specular intensity, note that the camera lies on the z-axis (in view), therefore simple -r.z + double diff = std::max(0., n * uniform_l); // diffuse light intensity + double spec = std::pow(std::max(r.z, 0.), 35); // specular intensity, note that the camera lies on the z-axis (in eye coordinates), therefore simple r.z - TGAColor c = sample2D(model.diffuse(), uv); for (int i : {0,1,2}) - gl_FragColor[i] = std::min(10 + c[i]*(diff + spec), 255); // (a bit of ambient light, diff + spec), clamp the result - return false; // do not discard the pixel + gl_FragColor[i] = std::min(30 + 255*(diff + spec), 255); // a bit of ambient light + diffuse light + return {false, gl_FragColor}; // do not discard the pixel } }; int main(int argc, char** argv) { - if (2>argc) { + if (argc < 2) { std::cerr << "Usage: " << argv[0] << " obj/model.obj" << std::endl; return 1; } @@ -56,26 +44,29 @@ int main(int argc, char** argv) { constexpr int width = 800; // output image size constexpr int height = 800; constexpr vec3 light_dir{1,1,1}; // light source - constexpr vec3 eye{1,1,3}; // camera position - constexpr vec3 center{0,0,0}; // camera direction - constexpr vec3 up{0,1,0}; // camera up vector + constexpr vec3 eye{-1,0,2}; // camera position + constexpr vec3 center{0,0,0}; // camera direction + constexpr vec3 up{0,1,0}; // camera up vector - lookat(eye, center, up); // build the ModelView matrix - viewport(width/8, height/8, width*3/4, height*3/4); // build the Viewport matrix - projection(norm(eye-center)); // build the Projection matrix - std::vector zbuffer(width*height, std::numeric_limits::max()); + lookat(eye, center, up); // build the ModelView matrix + perspective(norm(eye-center)); // build the Perspective matrix + viewport(width/16, height/16, width*7/8, height*7/8); // build the Viewport matrix + + TGAImage framebuffer(width, height, TGAImage::RGB); + std::vector zbuffer(width*height, -std::numeric_limits::max()); - TGAImage framebuffer(width, height, TGAImage::RGB); // the output image for (int m=1; m #include #include "model.h" @@ -15,23 +16,11 @@ Model::Model(const std::string filename) { vec3 v; for (int i : {0,1,2}) iss >> v[i]; verts.push_back(v); - } else if (!line.compare(0, 3, "vn ")) { - iss >> trash >> trash; - vec3 n; - for (int i : {0,1,2}) iss >> n[i]; - norms.push_back(normalized(n)); - } else if (!line.compare(0, 3, "vt ")) { - iss >> trash >> trash; - vec2 uv; - for (int i : {0,1}) iss >> uv[i]; - tex.push_back({uv.x, 1-uv.y}); - } else if (!line.compare(0, 2, "f ")) { + } else if (!line.compare(0, 2, "f ")) { int f,t,n, cnt = 0; iss >> trash; while (iss >> f >> trash >> t >> trash >> n) { facet_vrt.push_back(--f); - facet_tex.push_back(--t); - facet_nrm.push_back(--n); cnt++; } if (3!=cnt) { @@ -40,20 +29,9 @@ Model::Model(const std::string filename) { } } } - std::cerr << "# v# " << nverts() << " f# " << nfaces() << " vt# " << tex.size() << " vn# " << norms.size() << std::endl; - auto load_texture = [&filename](const std::string suffix, TGAImage &img) { - size_t dot = filename.find_last_of("."); - if (dot==std::string::npos) return; - std::string texfile = filename.substr(0,dot) + suffix; - std::cerr << "texture file " << texfile << " loading " << (img.read_tga_file(texfile.c_str()) ? "ok" : "failed") << std::endl; - }; - load_texture("_diffuse.tga", diffusemap ); - load_texture("_nm_tangent.tga", normalmap ); - load_texture("_spec.tga", specularmap); + std::cerr << "# v# " << nverts() << " f# " << nfaces() << std::endl; } -const TGAImage& Model::diffuse() const { return diffusemap; } -const TGAImage& Model::specular() const { return specularmap; } int Model::nverts() const { return verts.size(); } int Model::nfaces() const { return facet_vrt.size()/3; } @@ -65,16 +43,3 @@ vec3 Model::vert(const int iface, const int nthvert) const { return verts[facet_vrt[iface*3+nthvert]]; } -vec3 Model::normal(const vec2 &uvf) const { - TGAColor c = normalmap.get(uvf[0]*normalmap.width(), uvf[1]*normalmap.height()); - return vec3{(double)c[2],(double)c[1],(double)c[0]}*2./255. - vec3{1,1,1}; -} - -vec2 Model::uv(const int iface, const int nthvert) const { - return tex[facet_tex[iface*3+nthvert]]; -} - -vec3 Model::normal(const int iface, const int nthvert) const { - return norms[facet_nrm[iface*3+nthvert]]; -} - diff --git a/model.h b/model.h index ce5786cc..e6f25548 100644 --- a/model.h +++ b/model.h @@ -1,26 +1,14 @@ +#include #include "geometry.h" -#include "tgaimage.h" class Model { - std::vector verts = {}; // array of vertices ┐ generally speaking, these arrays - std::vector norms = {}; // array of normal vectors │ do not have the same size - std::vector tex = {}; // array of tex coords ┘ check the logs of the Model() constructor - std::vector facet_vrt = {}; // ┐ per-triangle indices in the above arrays, - std::vector facet_nrm = {}; // │ the size is supposed to be - std::vector facet_tex = {}; // ┘ nfaces()*3 - TGAImage diffusemap = {}; // diffuse color texture - TGAImage normalmap = {}; // normal map texture - TGAImage specularmap = {}; // specular texture + std::vector verts = {}; // array of vertices + std::vector facet_vrt = {}; // per-triangle index in the above array public: Model(const std::string filename); int nverts() const; // number of vertices int nfaces() const; // number of triangles vec3 vert(const int i) const; // 0 <= i < nverts() vec3 vert(const int iface, const int nthvert) const; // 0 <= iface <= nfaces(), 0 <= nthvert < 3 - vec3 normal(const int iface, const int nthvert) const; // normal coming from the "vn x y z" entries in the .obj file - vec3 normal(const vec2 &uv) const; // normal vector from the normal map texture - vec2 uv(const int iface, const int nthvert) const; // uv coordinates of triangle corners - const TGAImage& diffuse() const; - const TGAImage& specular() const; }; diff --git a/our_gl.cpp b/our_gl.cpp deleted file mode 100644 index 0e0c4284..00000000 --- a/our_gl.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "our_gl.h" - -mat<4,4> ModelView; -mat<4,4> Viewport; -mat<4,4> Projection; - -void viewport(const int x, const int y, const int w, const int h) { - Viewport = {{{w/2., 0, 0, x+w/2.}, {0, h/2., 0, y+h/2.}, {0,0,1,0}, {0,0,0,1}}}; -} - -void projection(const double f) { // check https://en.wikipedia.org/wiki/Camera_matrix - Projection = {{{1,0,0,0}, {0,-1,0,0}, {0,0,1,0}, {0,0,-1/f,0}}}; -} - -void lookat(const vec3 eye, const vec3 center, const vec3 up) { // check https://github.com/ssloy/tinyrenderer/wiki/Lesson-5-Moving-the-camera - vec3 z = normalized(center-eye); - vec3 x = normalized(cross(up,z)); - vec3 y = normalized(cross(z, x)); - ModelView = mat<4,4>{{{x.x,x.y,x.z,0}, {y.x,y.y,y.z,0}, {z.x,z.y,z.z,0}, {0,0,0,1}}} * - mat<4,4>{{{1,0,0,-eye.x}, {0,1,0,-eye.y}, {0,0,1,-eye.z}, {0,0,0,1}}}; -} - -vec3 barycentric(const vec2 tri[3], const vec2 P) { - mat<3,3> ABC = {{ {tri[0].x, tri[0].y, 1.}, {tri[1].x, tri[1].y, 1.}, {tri[2].x, tri[2].y, 1.} }}; - if (ABC.det()<1) return {-1,1,1}; // for a degenerate triangle generate negative coordinates, it will be thrown away by the rasterizator - return ABC.invert_transpose() * vec3{P.x, P.y, 1.}; -} - -void rasterize(const vec4 clip_verts[3], const IShader &shader, TGAImage &image, std::vector &zbuffer) { - vec4 pts [3] = { Viewport*clip_verts[0], Viewport*clip_verts[1], Viewport*clip_verts[2] }; // screen coordinates before persp. division - vec2 pts2[3] = { (pts[0]/pts[0].w).xy(), (pts[1]/pts[1].w).xy(), (pts[2]/pts[2].w).xy() }; // screen coordinates after perps. division - - int bbminx = std::max(0, static_cast(std::min(std::min(pts2[0].x, pts2[1].x), pts2[2].x))); // bounding box for the triangle - int bbminy = std::max(0, static_cast(std::min(std::min(pts2[0].y, pts2[1].y), pts2[2].y))); // clipped by the screen - int bbmaxx = std::min(image.width() -1, static_cast(std::max(std::max(pts2[0].x, pts2[1].x), pts2[2].x))); - int bbmaxy = std::min(image.height()-1, static_cast(std::max(std::max(pts2[0].y, pts2[1].y), pts2[2].y))); -#pragma omp parallel for - for (int x=bbminx; x<=bbmaxx; x++) { // rasterize the bounding box - for (int y=bbminy; y<=bbmaxy; y++) { - vec3 bc_screen = barycentric(pts2, {static_cast(x), static_cast(y)}); - vec3 bc_clip = { bc_screen.x/pts[0].w, bc_screen.y/pts[1].w, bc_screen.z/pts[2].w }; // check https://github.com/ssloy/tinyrenderer/wiki/Technical-difficulties-linear-interpolation-with-perspective-deformations - bc_clip = bc_clip / (bc_clip.x + bc_clip.y + bc_clip.z); - double frag_depth = bc_clip * vec3{ clip_verts[0].z, clip_verts[1].z, clip_verts[2].z }; - if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0 || frag_depth > zbuffer[x+y*image.width()]) continue; - TGAColor color; - if (shader.fragment(bc_clip, color)) continue; // fragment shader can discard current fragment - zbuffer[x+y*image.width()] = frag_depth; - image.set(x, y, color); - } - } -} - diff --git a/tgaimage.h b/tgaimage.h index 937aaffd..4712cdcd 100644 --- a/tgaimage.h +++ b/tgaimage.h @@ -23,6 +23,7 @@ struct TGAHeader { struct TGAColor { std::uint8_t bgra[4] = {0,0,0,0}; std::uint8_t bytespp = 4; + const std::uint8_t& operator[](const int i) const { return bgra[i]; } std::uint8_t& operator[](const int i) { return bgra[i]; } };