OpenGL 保存渲染的结果为图片

本文介绍了如何在OpenGL中使用glReadPixels函数将GPU渲染的内容保存为图片。glReadPixels用于从显存中读取像素数据,指定矩形区域、数据格式和类型。保存时,借助stb_image_write库将数据转换成BMP、JPEG等格式。由于OpenGL图像原点与常见图片格式不同,通常需要进行坐标翻转处理。示例中,作者将Shadertoy上的Shader移植到本地OpenGL,成功渲染出蛋糕效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在OpenGL中GPU渲染完数据向CPU回传显存的唯一方式为glReadPixels。其函数原型为

void glReadPixels(GLint x,
GLint y,
GLsizei width,
GLsizei height,
GLenum format,
GLenum type,
void * data);

前四个参数定义了一个像素矩形空间指向我们想要保存的区域

第五个参数指定了像素数据的格式,可以传入的值有GL_STENCIL_INDEXGL_DEPTH_COMPONENTGL_DEPTH_STENCILGL_REDGL_GREENGL_BLUEGL_RGBGL_BGRGL_RGBA, 或GL_BGRA.

 第六个指针指定了数据的类型,必须为以下值之一GL_UNSIGNED_BYTEGL_BYTEGL_UNSIGNED_SHORTGL_SHORTGL_UNSIGNED_INTGL_INTGL_HALF_FLOATGL_FLOATGL_UNSIGNED_BYTE_3_3_2GL_UNSIGNED_BYTE_2_3_3_REVGL_UNSIGNED_SHORT_5_6_5GL_UNSIGNED_SHORT_5_6_5_REVGL_UNSIGNED_SHORT_4_4_4_4GL_UNSIGNED_SHORT_4_4_4_4_REVGL_UNSIGNED_SHORT_5_5_5_1GL_UNSIGNED_SHORT_1_5_5_5_REVGL_UNSIGNED_INT_8_8_8_8GL_UNSIGNED_INT_8_8_8_8_REVGL_UNSIGNED_INT_10_10_10_2GL_UNSIGNED_INT_2_10_10_10_REVGL_UNSIGNED_INT_24_8GL_UNSIGNED_INT_10F_11F_11F_REVGL_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);
}

得到的结果如下

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值