在OpenGL中GPU渲染完数据向CPU回传显存的唯一方式为glReadPixels。其函数原型为
void glReadPixels( | GLint x, |
GLint y, | |
GLsizei width, | |
GLsizei height, | |
GLenum format, | |
GLenum type, | |
void * data) ; |
前四个参数定义了一个像素矩形空间指向我们想要保存的区域
第五个参数指定了像素数据的格式,可以传入的值有GL_STENCIL_INDEX
, GL_DEPTH_COMPONENT
, GL_DEPTH_STENCIL
, GL_RED
, GL_GREEN
, GL_BLUE
, GL_RGB
, GL_BGR
, GL_RGBA
, 或GL_BGRA
.
第六个指针指定了数据的类型,必须为以下值之一GL_UNSIGNED_BYTE
, GL_BYTE
, GL_UNSIGNED_SHORT
, GL_SHORT
, GL_UNSIGNED_INT
, GL_INT
, GL_HALF_FLOAT
, GL_FLOAT
, GL_UNSIGNED_BYTE_3_3_2
, GL_UNSIGNED_BYTE_2_3_3_REV
, GL_UNSIGNED_SHORT_5_6_5
, GL_UNSIGNED_SHORT_5_6_5_REV
, GL_UNSIGNED_SHORT_4_4_4_4
, GL_UNSIGNED_SHORT_4_4_4_4_REV
, GL_UNSIGNED_SHORT_5_5_5_1
, GL_UNSIGNED_SHORT_1_5_5_5_REV
, GL_UNSIGNED_INT_8_8_8_8
, GL_UNSIGNED_INT_8_8_8_8_REV
, GL_UNSIGNED_INT_10_10_10_2
, GL_UNSIGNED_INT_2_10_10_10_REV
, GL_UNSIGNED_INT_24_8
, GL_UNSIGNED_INT_10F_11F_11F_REV
, GL_UNSIGNED_INT_5_9_9_9_REV
, or GL_FLOAT_32_UNSIGNED_INT_24_8_REV
.
最后一个参数传入我们写入像素数据的区块首地址,必须预先用malloc分配合适的大小
stb_image常被用来导入OpenGL纹理图片,对于保存像素格式数据到BMP、JPEG、JPG等图片格式数据我们可以使用对应的stb_image_write,由于函数的实现都已经放在头文件中,只需要在包含头文件前确保宏STB_IMAGE_WRITE_IMPLEMENTATION已有定义
由于OpenGL定义图像原点(0,0)在左下方,而BMP、JPEG、JPG等图片格式将原点定义在左上方,因此通过glReadPixels获取到的像素数据还需要经过一层垂直翻转,函数如下
void flip(uint8_t** buf)
{
int totalLength = SCR_HEIGHT*SCR_WIDTH*3;
int oneLineLength = SCR_WIDTH*3;
static uint8_t* tmp = (uint8_t*)malloc(SCR_HEIGHT*SCR_WIDTH*3);
memcpy(tmp,*buf,SCR_WIDTH*SCR_HEIGHT*3);
memset(*buf,0,sizeof(uint8_t)*SCR_HEIGHT*SCR_WIDTH*3);
for(int i = 0; i < SCR_HEIGHT;i++){
memcpy(*buf+oneLineLength*i,tmp+totalLength-oneLineLength*(i+1),oneLineLength);
}
}
如不经过翻转,保存的图片可能为:
对于案例,因为正好快要过生日的原因,打算从Shadertoy上参考渲染蛋糕的Shader移植到本地OpenGL,移植方法可以参考我的博文
//BirthdayCake.cpp
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <shader.h>
#include <stdlib.h>
#include <stb_image_write.h>
#include <stb_image.h>
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
const int resolution[2] = { SCR_WIDTH,SCR_HEIGHT };
float deltaTime = 0.0f;
float lastTime = 0.0f;
unsigned int quadVAO = 0;
unsigned int quadVBO;
void renderQuad();
void flip(uint8_t** buf)
{
int totalLength = SCR_HEIGHT*SCR_WIDTH*3;
int oneLineLength = SCR_WIDTH*3;
static uint8_t* tmp = (uint8_t*)malloc(SCR_HEIGHT*SCR_WIDTH*3);
memcpy(tmp,*buf,SCR_WIDTH*SCR_HEIGHT*3);
memset(*buf,0,sizeof(uint8_t)*SCR_HEIGHT*SCR_WIDTH*3);
for(int i = 0; i < SCR_HEIGHT;i++){
memcpy(*buf+oneLineLength*i,tmp+totalLength-oneLineLength*(i+1),oneLineLength);
}
}
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
char windowTile[32];
int titleUpdateRate = 0;
Shader render("Shader/birthday_cake/birthday_cake.vs","Shader/birthday_cake/birthday_cake.fs");
render.use();
glUniform2iv(glGetUniformLocation(render.ID,"iResolution"),1,&resolution[0]);
float Time = 0;
stbi_set_flip_vertically_on_load(true);
void *imageData = (void*)malloc(3*sizeof(char)*SCR_WIDTH*SCR_HEIGHT);
while(!glfwWindowShouldClose(window))
{
auto time = static_cast<float>(glfwGetTime());
deltaTime = time - lastTime;
lastTime = time;
sprintf_s(windowTile,"current FPS = %2f", 1. / deltaTime);
if (titleUpdateRate == 0)
{
glfwSetWindowTitle(window,windowTile);
glReadPixels(0,0,SCR_WIDTH,SCR_HEIGHT,GL_RGB,GL_UNSIGNED_BYTE,imageData);
flip(&((uint8_t*)imageData));
int res = stbi_write_png("screenCapture.png",SCR_WIDTH,SCR_HEIGHT,3,imageData,0);
}
titleUpdateRate++;
if (titleUpdateRate > 10)
titleUpdateRate = 0;
glUniform1f(glGetUniformLocation(render.ID,"runtime_data.iTime"),time);
renderQuad();
glfwSwapBuffers(window);
glfwSwapInterval(1);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void renderQuad()
{
if (quadVAO == 0)
{
float quadVertices[] = {
// positions // texture Coords
-1.0f, 1.0f, 0.0f, 0.0f, (float)SCR_HEIGHT,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f, (float)SCR_WIDTH,(float)SCR_HEIGHT,
1.0f, -1.0f, 0.0f, (float)SCR_WIDTH, 0.0f,
};
// setup plane VAO
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
}
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
}
//birthday_cake.vs
#version 450 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texlCoord;
out vec2 fragCoord;
void main()
{
fragCoord = texlCoord;
gl_Position = vec4(position,1.0);
}
//birthday_cake.fs
#version 450 core
out vec4 fragColor;
in vec2 fragCoord;
struct runtime_param
{
float iTime;
};
uniform runtime_param runtime_data;
uniform ivec2 iResolution;
#define time runtime_data.iTime
//Set ANTIALIAS_ALWAYS to false if the animation is too slow
#define ANTIALIAS_ALWAYS true
#define ANTIALIAS_SAMPLES 4
#define ANIMATE_CAMERA
#define PI 3.14159265359
#define EXPOSURE 34.
#define GAMMA 2.1
#define SOFT_SHADOWS_FACTOR 4.
#define MAX_RAYMARCH_ITER 128
#define MAX_RAYMARCH_ITER_SHADOWS 16
#define MIN_RAYMARCH_DELTA 0.0015
#define GRADIENT_DELTA 0.0002
#define OBJ_FLOOR 1.
#define OBJ_CEILING 2.
#define OBJ_BACKWALL 3.
#define OBJ_LEFTWALL 4.
#define OBJ_RIGHTWALL 5.
#define OBJ_LIGHT 6.
#define OBJ_SHORTBLOCK 7.
#define OBJ_TALLBLOCK 8.
#define OBJ_CAKE_BASE 9.
#define OBJ_CAKE_ICING 10.
#define OBJ_CAKE_CANDLE 11.
#define OBJ_CAKE_FLAME 12.
//RGB wavelengths(波长): 650nm,510nm,475nm
// RGB wavelengths: 650nm, 510nm, 475nm
const vec3 lightColor = vec3(16.86, 8.76 +2., 3.2 + .5);
const vec3 lightDiffuseColor = vec3(.78);
const vec3 leftWallColor = vec3(.611, .0555, .062);
const vec3 rightWallColor = vec3(.117, .4125, .115);
const vec3 whiteWallColor = vec3(.7295, .7355, .729);
const vec3 cakeBaseColor = vec3(.05, .04, .02);
const vec3 cakeIcingColor = vec3(1.5, 1.5, 1.9);
const vec3 cakeCandleColor = vec3(0.1, 0.2, 1.3);
const vec3 cakeCandleFlameColor = vec3(0.3, .15, 0.05)*0.1;
const vec3 cameraTarget = vec3(556, 548.8, 559.2) * .5;
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.) + length(max(d, 0.));
}
//https://iquilezles.org/articles/distfunctions
float sdRoundedCylinder( vec3 p, float ra, float rb, float h )
{
vec2 d = vec2( length(p.xz)-2.0*ra+rb, abs(p.y) - h );
return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rb;
}
float sdSphere(vec3 p, float r, vec3 scale)
{
return length(p*scale)-r;
}
float distort(float sdf, vec3 p, vec3 x, float period)
{
vec3 dist = sin(p*period)*x;
float d = dist.x+dist.y+dist.z;
return sdf+d;
}
vec3 distortV(vec3 p, vec3 x, vec3 period, float phase)
{
vec3 dist = sin(p*period+phase);
float d = dist.x+dist.y+dist.z;
return p+d*x;
}
vec3 rotateX(in vec3 p, float a) {
float c = cos(a); float s = sin(a);
return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}
vec3 rotateY(vec3 p, float a) {
float c = cos(a); float s = sin(a);
return vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
vec3 rotateZ(vec3 p, float a) {
float c = cos(a); float s = sin(a);
return vec3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
}
vec2 mapBlocks(vec3 p, vec3 ray_dir) { // ray_dir may be used for some optimizations
vec2 res = vec2(OBJ_SHORTBLOCK, sdBox(rotateY(p + vec3(-250, -82.5, -169.5), 0.29718), vec3(83.66749, 83.522452, 82.5)));
//vec2 obj1 = vec2(OBJ_TALLBLOCK, sdBox(rotateY(p + vec3(-368.5, -165, -351.5), -0.30072115), vec3(87.02012, 165, 83.6675)));
//if (obj1.y < res.y) res = obj1;
return res;
}
vec2 map(vec3 p, vec3 ray_dir) { // ray_dir may be used for some optimizations
vec2 res = vec2(OBJ_FLOOR, p.y);
vec2 obj1 = vec2(OBJ_CEILING, 548.8 - p.y);
if (obj1.y < res.y) res = obj1;
vec2 obj2 = vec2(OBJ_BACKWALL, 559.2 - p.z);
if (obj2.y < res.y) res = obj2;
vec2 obj3 = vec2(OBJ_LEFTWALL, 556. - p.x);
if (obj3.y < res.y) res = obj3;
vec2 obj4 = vec2(OBJ_RIGHTWALL, p.x);
if (obj4.y < res.y) res = obj4;
vec2 obj5 = vec2(OBJ_LIGHT, sdBox(p + vec3(-278, -548.8, -292+150), vec3(65, 0.05, 65)));
if (obj5.y < res.y) res = obj5;
vec2 obj6 = mapBlocks(p, ray_dir);
if (obj6.y < res.y) res = obj6;
vec2 cakeBase = vec2(OBJ_CAKE_BASE, sdRoundedCylinder(p + vec3(-250, -180., -169.5), 30., 10., 20.));
if(cakeBase.y < res.y) res = cakeBase;
vec2 cakeIcing = vec2(OBJ_CAKE_ICING,
sdRoundedCylinder(distortV(p, vec3(0.0,p.y<190.?2.:0.,0.0), vec3(0.3,0.0,0.3), 0.) + vec3(-250, -195., -169.5), 31., 10., 11.));
//distort(sdRoundedCylinder(p + vec3(-250, -180., -169.5), 32., 10., 22.),
// p, vec3(.2,0.2,0.2), 0.3));
if(cakeIcing.y < res.y) res = cakeIcing;
vec2 cakeCandle = vec2(OBJ_CAKE_CANDLE,
sdRoundedCylinder(distortV(p, vec3(0.1, 0., 0.1), vec3(1.5,1.5,1.5), 0.) + vec3(-250, -225., -169.5), 1.5, 2., 15.));
if(cakeCandle.y < res.y) res = cakeCandle;
vec2 cakeCandleFlame = vec2(OBJ_CAKE_FLAME,
sdSphere(distortV(p, vec3(0.1, 0.3, 0.1), vec3(1.8,1.0,1.8), time*20.) + vec3(-250, -250., -169.5), 2., vec3(1.,.3,1.)));
if(cakeCandleFlame.y < res.y) res = cakeCandleFlame;
return res;
}
vec2 map(vec3 p) {
return map(p, vec3(0,0,0));
}
vec3 gradientNormal(vec3 p) {
return normalize(vec3(
map(p + vec3(GRADIENT_DELTA, 0, 0)).y - map(p - vec3(GRADIENT_DELTA, 0, 0)).y,
map(p + vec3(0, GRADIENT_DELTA, 0)).y - map(p - vec3(0, GRADIENT_DELTA, 0)).y,
map(p + vec3(0, 0, GRADIENT_DELTA)).y - map(p - vec3(0, 0, GRADIENT_DELTA)).y));
}
float raymarch(vec3 ray_start, vec3 ray_dir, out float dist, out vec3 p, out int iterations) {
dist = 0.0;
float minStep = 0.1;
vec2 mapRes;
for (int i = 1; i <= MAX_RAYMARCH_ITER; i++) {
p = ray_start + ray_dir * dist;
mapRes = map(p, ray_dir);
if (mapRes.y < MIN_RAYMARCH_DELTA) {
iterations = i;
return mapRes.x;
}
dist += max(mapRes.y, minStep);
}
return -1.;
}
bool raymarch_to_light(vec3 ray_start, vec3 ray_dir, float maxDist, float maxY, out float dist, out vec3 p, out int iterations, out float light_intensity) {
dist = 0.;
float minStep = 1.0;
light_intensity = 1.0;
float mapDist;
for (int i = 1; i <= MAX_RAYMARCH_ITER_SHADOWS; i++) {
p = ray_start + ray_dir * dist;
mapDist = mapBlocks(p, ray_dir).y;
if (mapDist < MIN_RAYMARCH_DELTA) {
iterations = i;
return true;
}
light_intensity = min(light_intensity, SOFT_SHADOWS_FACTOR * mapDist / dist);
dist += max(mapDist, minStep);
if (dist >= maxDist || p.y > maxY) { break; }
}
return false;
}
vec3 interpolateNormals(vec3 v0, vec3 v1, float x) {
x = smoothstep(0., 1., x);
return normalize(vec3(mix(v0.x, v1.x, x),
mix(v0.y, v1.y, x),
mix(v0.z, v1.z, x)));
}
float ambientOcclusion(vec3 p, vec3 n) {
float step = 20.;
float ao = 0.;
float dist;
for (int i = 1; i <= 3; i++) {
dist = step * float(i);
ao += max(0., (dist - map(p + n * dist).y) / dist);
}
return 1. - ao * 0.12;
}
vec3 render(vec3 ray_start, vec3 ray_dir) {
float dist; vec3 p; int iterations;
float objectID = raymarch(ray_start, ray_dir, dist, p, iterations);
vec3 color = vec3(0);
if (p.z >= 0.) {
if (objectID == OBJ_FLOOR) color = whiteWallColor;
else if (objectID == OBJ_CEILING) color = whiteWallColor;
else if (objectID == OBJ_BACKWALL) color = whiteWallColor;
else if (objectID == OBJ_LEFTWALL) color = leftWallColor;
else if (objectID == OBJ_RIGHTWALL) color = rightWallColor;
else if (objectID == OBJ_LIGHT) color = lightDiffuseColor;
else if (objectID == OBJ_SHORTBLOCK) color = whiteWallColor;
else if (objectID == OBJ_TALLBLOCK) color = whiteWallColor;
else if (objectID == OBJ_CAKE_BASE) color = cakeBaseColor;
else if (objectID == OBJ_CAKE_ICING) color = cakeIcingColor;
else if (objectID == OBJ_CAKE_CANDLE) color = cakeCandleColor;
else if (objectID == OBJ_CAKE_FLAME) color = cakeCandleFlameColor;
if (objectID == OBJ_LIGHT || objectID == OBJ_CAKE_FLAME) {
color *= lightColor;
} else {
float lightSize = 25.;
vec3 lightPos = vec3(278, 548.8 -50., 292 - 250);
if (objectID == OBJ_CEILING) { lightPos.y -= 550.; }
lightPos.x = max(lightPos.x - lightSize, min(lightPos.x + lightSize, p.x));
lightPos.y = max(lightPos.y - lightSize, min(lightPos.y + lightSize, p.y));
vec3 n = gradientNormal(p);
vec3 l = normalize(lightPos - p);
float lightDistance = length(lightPos - p);
float atten = ((1. / lightDistance) * .5) + ((1. / (lightDistance * lightDistance)) * .5);
vec3 lightPos_shadows = lightPos + vec3(0, 140, -50);
vec3 l_shadows = normalize(lightPos_shadows - p);
float dist; vec3 op; int iterations; float l_intensity;
bool res = raymarch_to_light(p + n * .11, l_shadows, lightDistance, 400., dist, op, iterations, l_intensity);
if (res && objectID != OBJ_CEILING) l_intensity = 0.;
l_intensity = max(l_intensity,.25);
vec3 c1 = color * max(0., dot(n, l)) * lightColor * l_intensity * atten;
// Indirect lighting
vec3 c2_lightColor = lightColor * rightWallColor * .08;
float c2_lightDistance = p.x + 0.00001;
float c2_atten = 1. / c2_lightDistance;
vec3 c2_lightDir0 = vec3(-1,0,0);
vec3 c2_lightDir1 = normalize(vec3(-300., 548.8/2.,559.2/2.) - p);
float c2_perc = min(p.x * .01, 1.);
vec3 c2_lightDirection = interpolateNormals(c2_lightDir0, c2_lightDir1, c2_perc);
vec3 c2 = color * max(0., dot(n, c2_lightDirection)) * c2_lightColor * c2_atten;
vec3 c3_lightColor = lightColor * leftWallColor * .08;
float c3_lightDistance = 556. - p.x + 0.1;
float c3_atten = 1. / c3_lightDistance;
vec3 c3_lightDir0 = vec3(1,0,0);
vec3 c3_lightDir1 = normalize(vec3(556. + 300., 548.8/2.,559.2/2.) - p);
float c3_perc = min((556. - p.x) * .01, 1.);
vec3 c3_lightDirection = interpolateNormals(c3_lightDir0, c3_lightDir1, c3_perc);
vec3 c3 = color * max(0., dot(n, c3_lightDirection)) * c3_lightColor * c3_atten;
color = color * .0006 + c1;
color += c2 + c3; // Fake indirect lighting
// Ambient occlusion
float ao = ambientOcclusion(p, n);
color *= ao;
}
}
return color;
}
vec3 rotateCamera(vec3 ray_start, vec3 ray_dir) {
ray_dir.x = -ray_dir.x; // Flip the x coordinate to match the scene data
vec3 target = normalize(cameraTarget - ray_start);
float angY = atan(target.z, target.x);
ray_dir = rotateY(ray_dir, PI/2. - angY);
float angX = atan(target.y, target.z);
ray_dir = rotateX(ray_dir, - angX);
#ifdef ANIMATE_CAMERA
float angZ = smoothstep(0., 1., (time - 5.) * .1) * sin(time * 1.1 + .77) * .05;
ray_dir = rotateZ(ray_dir, angZ);
#endif
return ray_dir;
}
vec3 moveCamera(vec3 ray_start) {
ray_start += vec3(278, 273, -400);
#ifdef ANIMATE_CAMERA
vec3 ray_start_a = ray_start
+ vec3(cos(time * 0.8) * 140., cos(time * 0.9) * 140., (cos(time * .3) + 1.) * 190.);
return mix(ray_start, ray_start_a, smoothstep(0., 1., (time - 5.) * .1));
#else
return ray_start;
#endif
}
void main() {
vec2 resolution = iResolution.xy;
vec3 ray_start = vec3(0, 0, -1.4);
vec3 color = vec3(0);
if (ANTIALIAS_ALWAYS || time < 5.) {
// ANTIALIAS
float d_ang = 2.*PI / float(ANTIALIAS_SAMPLES);
float ang = d_ang * .333;
float r = .4;
for (int i = 0; i < ANTIALIAS_SAMPLES; i++) {
vec2 position = vec2((fragCoord.x + cos(ang)*r - resolution.x *.5) / resolution.y, (fragCoord.y + sin(ang)*r - resolution.y *.5) / resolution.y);
vec3 ray_s = moveCamera(ray_start);
vec3 ray_dir = rotateCamera(ray_s,normalize(vec3(position, 0) - ray_start));
color += render(ray_s, ray_dir);
ang += d_ang;
}
color /= float(ANTIALIAS_SAMPLES);
} else {
// NO ANTIALIAS
vec2 position = vec2((fragCoord.x - resolution.x *.5) / resolution.y, (fragCoord.y - resolution.y *.5) / resolution.y);
vec3 ray_s = moveCamera(ray_start);
vec3 ray_dir = rotateCamera(ray_s, normalize(vec3(position, 0) - ray_start));
color += render(ray_s, ray_dir);
}
color *= EXPOSURE;
color = pow(color, vec3(1. / GAMMA));
fragColor = vec4(color, 1);
}
得到的结果如下