第二十一节—光照计算3
2020-10-17 本文已影响0人
L_Ares
本文为L_Ares个人写作,如需转载请表明原文出处。
本节依然实现上一节的效果,实现效果图可以看上一节的。
本节使用GLSL
来完成光照的计算。
//
// JDView.m
// 08GLSL光照计算
//
// Created by EasonLi on 2020/10/16.
//
#import "JDView.h"
#import <GLKit/GLKit.h>
#import <OpenGLES/ES3/gl.h>
#import <OpenGLES/ES3/glext.h>
#define MY_ORIGIN self.frame.origin
#define MY_SIZE self.frame.size
@interface JDView ()
//图层
@property (nonatomic, strong) CAEAGLLayer *mLayer;
//上下文
@property (nonatomic, strong) EAGLContext *mContext;
//渲染缓冲区
@property (nonatomic, assign) GLuint mRenderBuffer;
//帧缓冲区
@property (nonatomic, assign) GLuint mFrameBuffer;
//深度缓冲区
@property (nonatomic, assign) GLuint mDepthBuffer;
//顶点缓冲区
@property (nonatomic, assign) GLuint mVertexBuffer;
//定义一个Program
@property (nonatomic, assign) GLuint mProgram;
@end
//光源位置
static GLKVector3 lightPos = {1.2f,1.f,2.f};
@implementation JDView
#pragma mark - 重设宿主图层类型为CAEAGLLayer
/**
如果想把自己的图层子类对象设置成当前View的宿主图层,那么就必须要赶在图层创建
之前重写layerClass,更改返回的宿主图层的类。
这样就可以在UIView进行初始化的时候,把layerClass返回的类作为自己的宿主图层类。
一定要在UIView初始化之前完成这个操作,不然UIView创建完成,宿主图层确定类型是无法更改的。
*/
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
#pragma mark - 重写layoutSubViews
- (void)layoutSubviews{
//设置图层
[self setUpLayer];
//设置上下文
[self setUpContext];
//加载shader并且设置program
[self shaderAndProgram];
//清空缓冲区
[self cleanUpBuffers];
//设置渲染缓冲区
[self setUpRenderBufferAndDepthBuffer];
//设置帧缓冲区
[self setUpFrameBuffer];
//设置顶点数据和顶点缓冲区
[self setUpVertexDataAndBuffer];
//设置纹理
[self setUpTexture:@"test.png"];
//渲染
[self setupLinker];
}
#pragma mark - 设置图层
- (void)setUpLayer
{
//把图层交给CAEAGLLayer对象
self.mLayer = (CAEAGLLayer *)self.layer;
//设置屏幕缩放比例
[self setContentScaleFactor:[UIScreen mainScreen].scale];
/**
设置layer图层绘制的描述属性
(1). 颜色格式
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
(2). 是否保留上次绘制的内容
kEAGLDrawablePropertyRetainedBacking : @false
*/
self.mLayer.drawableProperties = @{kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8,kEAGLDrawablePropertyRetainedBacking : @false};
}
#pragma mark - 设置图形的上下文
- (void)setUpContext
{
//初始化上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
//上下文是否创建
if (!context) {
NSLog(@"上下文初始化失败");
return;
}
//设置当前上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"设置当前上下文失败");
return;
}
//上下文赋值
self.mContext = context;
}
#pragma mark - 加载shader并创建Program
- (void)shaderAndProgram
{
//获得vsh和fsh的路径
NSString *vshFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fshFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//判断当前属性中的program是否已经存在,已经存在的话,就要删除它
if (self.mProgram) {
glDeleteProgram(self.mProgram);
self.mProgram = 0;
}
//加载着色器并且获得program
self.mProgram = [self loadVertexShader:vshFile FragmentShader:fshFile];
//链接Program
glLinkProgram(self.mProgram);
//获取Program的链接状态
GLint linkStatus;
glGetProgramiv(self.mProgram, GL_LINK_STATUS, &linkStatus);
//判断Program是否链接成功
if (linkStatus == GL_FALSE) {
//失败状态下要存储errorinfo,所以定义一个元素类型为GLChar的数组存储
GLchar errorInfo[256];
//获取失败信息,最后一个参数是数组的首地址
glGetProgramInfoLog(self.mProgram, sizeof(errorInfo), 0, errorInfo);
NSLog(@"链接失败 : %@",[NSString stringWithUTF8String:errorInfo]);
return;
}
NSLog(@"链接成功");
//使用Program
glUseProgram(self.mProgram);
}
//加载着色器,并获取和shader关联好了的program
- (GLuint)loadVertexShader:(NSString *)vshFile FragmentShader:(NSString *)fshFile
{
//定义两个局部变量,用以存储着色器中的内容
GLuint vertexShader,fragmentShader;
//创建一个局部的program
GLuint program = glCreateProgram();
//编译顶点着色器和片元着色器,拿到着色器中的内容,并写入到两个临时的着色器变量中
//顶点着色器
[self compileShader:&vertexShader type:GL_VERTEX_SHADER file:vshFile];
[self compileShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fshFile];
//将着色器都附着到program上面
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
//即然已经都附着到了program上了,就不需要再留着两个临时着色器变量了,直接删除就行
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
//编译着色器
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
//读取着色器源码的内容
NSString *shaderContent = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
//着色器内容附着到着色器变量上要用到glShaderSource
//glShaderSource的第二个参数就是char类型的指针变量
//所以定义一个char类型的变量,存储着色器内容
const GLchar *cshContent = (GLchar *)[shaderContent UTF8String];
//创建一个shader,并把创建的shader赋值给传来的着色器的地址上
*shader = glCreateShader(type);
//将着色器内容附着到着色器对象上
//函数的最后一个参数是着色器字符串的长度
//在不知道的情况下可以直接写NULL,意为遇到NULL终止符就停止
glShaderSource(*shader, 1, &cshContent, NULL);
//将着色器源码编译成目标代码
glCompileShader(*shader);
}
#pragma mark - 清空缓冲区
- (void)cleanUpBuffers
{
//清空VBO
glDeleteBuffers(1, &_mVertexBuffer);
self.mVertexBuffer = 0;
//清空渲染缓冲区
glDeleteBuffers(1, &_mRenderBuffer);
self.mRenderBuffer = 0;
//清空帧缓冲区
glDeleteBuffers(1, &_mFrameBuffer);
self.mFrameBuffer = 0;
//清空深度缓冲区
glDeleteBuffers(1, &_mDepthBuffer);
self.mDepthBuffer = 0;
}
#pragma mark - 设置渲染缓冲区及其内部的深度缓冲区
- (void)setUpRenderBufferAndDepthBuffer
{
//申请渲染缓冲区空间
glGenRenderbuffers(1, &_mRenderBuffer);
//绑定渲染缓冲区
glBindRenderbuffer(GL_RENDERBUFFER, self.mRenderBuffer);
//把renderBuffer存储或者说绑定到CAEAGLLayer对象上
BOOL result = [self.mContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mLayer];
//判断是否存储或者说绑定成功
if (!result) {
NSLog(@"renderBuffer绑定图层失败");
return;
}
/***********************************************************************************/
//定义宽和高的临时变量,下面深度缓冲区存储的时候有用
int width,height;
/**
返回renderBuffer对象的参数,我们在这里获取当前绑定的渲染缓冲区的宽和高(单位:像素)
如果生成信息发生了错误,则不会生成params内容
glGetRenderbufferParameteriv (GLenum target, GLenum pname, GLint* params)
参数:
(1).target : 指定目标renderbuffer对象。符号常量必须为GL_RENDERBUFFER。
(2).pname : 指定renderbuffer对象参数的符号名称。
(3).params : 返回请求的参数
//这里注意,glGetRenderbufferParameteriv获取的图像分辨率是组件实际存储的分辨率,可能
//与glRenderbufferStorage函数的internalformat参数不一致
*/
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);
//申请深度缓冲区空间
glGenRenderbuffers(1, &_mDepthBuffer);
//绑定深度缓冲区
glBindRenderbuffer(GL_RENDERBUFFER, self.mDepthBuffer);
//存储深度缓冲区的信息
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
}
#pragma mark - 设置帧缓冲区
- (void)setUpFrameBuffer
{
//申请帧缓冲区
glGenFramebuffers(1, &_mFrameBuffer);
//绑定帧缓冲区
glBindFramebuffer(GL_FRAMEBUFFER, self.mFrameBuffer);
//在每次要做附着点设置之前,最好再确认一下,要设置的缓冲区是否绑定的正确,
//因为这里多了一个depthBuffer在共用GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.mDepthBuffer);
//设置depthBuffer在framebuffer上附着点
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, self.mDepthBuffer);
//同理,来确认一下渲染缓冲区绑定是不是正确
glBindRenderbuffer(GL_RENDERBUFFER, self.mRenderBuffer);
//设置renderBuffer在frameBuffer上附着点
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mRenderBuffer);
}
#pragma mark - 设置顶点数据和顶点缓冲区
- (void)setUpVertexDataAndBuffer
{
//顶点数组,一个立方体坐标信息(6个面,每个面2个三角形)
float vertices[] = {
//顶点位置 //法线 //纹理坐标
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
//设置VBO(顶点缓冲区)
glGenBuffers(1, &_mVertexBuffer);
//绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, self.mVertexBuffer);
//将顶点数组从CPU拷贝到GPU中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
//设置属性通道的开关,这里会在vsh中使用layout...in...只有vsh可以用layout...in...
//顶点通道
glEnableVertexAttribArray(0);
//法向量
glEnableVertexAttribArray(1);
//纹理坐标
glEnableVertexAttribArray(2);
//设置数据的读取格式
//顶点
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)NULL);
//法向量
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
//纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float)));
}
#pragma mark - 从图片中加载纹理
- (void)setUpTexture:(NSString *)fileName
{
//纹理需要的是位图,所以只要从路径读取的不是位图,那么就需要将它变成位图
CGImageRef bitImage = [UIImage imageNamed:fileName].CGImage;
//可以判断一下,是否拿到了这张位图
if (!bitImage) {
NSLog(@"解压缩图片失败 : %@",fileName);
//非正常运行程序导致程序退出。exit(0)是正常运行程序导致退出
exit(1);
}
//拿到图片以后读取图片的信息,宽高
size_t width = CGImageGetWidth(bitImage);
size_t height = CGImageGetHeight(bitImage);
//获取图片的字节数
GLubyte *bitByte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//创建上下文
CGContextRef bitContextRef = CGBitmapContextCreate(bitByte, width, height, 8, 4 * width, CGImageGetColorSpace(bitImage), kCGImageAlphaPremultipliedLast);
//在上下文上把图片绘制出来
//先设定好rect
CGRect rect = CGRectMake(0.f, 0.f, width, height);
/*******************图片翻转********************************/
//先向Y轴正方向移动一个图形高度的距离,这时候,图形的底边已经变成原来图形位置顶边了
CGContextTranslateCTM(bitContextRef, 0.f, rect.size.height);
//这时候把Y轴正方向向下,则图片也随之向下,那么远来是反过来的,这次又反过来一次,负负得正。
CGContextScaleCTM(bitContextRef, 1.f, -1.f);
/**********************************************************/
//使用默认方式绘制
CGContextDrawImage(bitContextRef, rect, bitImage);
//绘制完成后释放上下文
CGContextRelease(bitContextRef);
//绑定纹理并设置纹理参数
GLuint texture;
glGenTextures(1, &texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitByte);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
free(bitByte);
glUniform1i(glGetUniformLocation(self.mProgram, "Material.Texture"), 0);
}
#pragma mark - 渲染
-(void)render{
glEnable(GL_DEPTH_TEST);
glClearColor(1.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//先拿到mainScreen主屏的缩放因子
CGFloat scale = [UIScreen mainScreen].scale;
//设置视口
glViewport(MY_ORIGIN.x * scale, MY_ORIGIN.y * scale, MY_SIZE.width * scale, MY_SIZE.height * scale);
//光照颜色->片元/顶点着色器
glUniform3f(glGetUniformLocation(self.mProgram, "lightColor"), 1.0f, 1.0f, 1.0f);
//投影矩阵->顶点着色器
float aspect = (float)self.bounds.size.width / (float)self.bounds.size.height;
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathRadiansToDegrees(45.0f), aspect, 0.1f, 800.0f) ;
glUniformMatrix4fv(glGetUniformLocation(self.mProgram, "projection"), 1, GL_FALSE, (GLfloat*)projectionMatrix.m);
//模型视图矩阵-->顶点着色器(这里修改的是观察者或者说视点的位置,你也可以修改物体的位置)
float radius = 10.0f;
//这里只是求一个随机值,因为sin和cos的值域范围都是[-1,1],再扩大一下,所以乘10.f
float camX = sin(CACurrentMediaTime()) * radius;
float camZ = cos(CACurrentMediaTime()) * radius;
GLKVector3 viewPo = {camX,camX,camZ};
//获取世界坐标系去模型矩阵中.
/*
LKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ)
等价于 OpenGL 中
void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);
目的:根据你的设置返回一个4x4矩阵变换的世界坐标系坐标。
参数1:眼睛位置的x坐标
参数2:眼睛位置的y坐标
参数3:眼睛位置的z坐标
第一组:就是脑袋的位置
参数4:正在观察的点的X坐标
参数5:正在观察的点的Y坐标
参数6:正在观察的点的Z坐标
第二组:就是眼睛所看物体的位置
参数7:摄像机上向量的x坐标
参数8:摄像机上向量的y坐标
参数9:摄像机上向量的z坐标
第三组:就是头顶朝向的方向(因为你可以头歪着的状态看物体)
*/
GLKMatrix4 view1 = GLKMatrix4MakeLookAt(camX,camX,camZ,0,0,0,0,1,0);
view1 = GLKMatrix4Scale(view1, 5.0f, 5.0f, 5.0f);
glUniformMatrix4fv(glGetUniformLocation(self.mProgram, "view"), 1, GL_FALSE, (GLfloat *)view1.m);
//光源位置
glUniform3f(glGetUniformLocation(self.mProgram, "lightPo"), lightPos.x, lightPos.y, lightPos.z);
//观察者位置
glUniform3f(glGetUniformLocation(self.mProgram, "viewPo"), viewPo.x, viewPo.y, viewPo.z);
glDrawArrays(GL_TRIANGLES, 0, 36);
[self.mContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)setupLinker
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CADisplayLink *linker = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)];
linker.preferredFramesPerSecond = 24;
[linker addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
});
}
#pragma mark - 销毁
- (void)dealloc
{
if ([EAGLContext currentContext] == _mContext) {
[EAGLContext setCurrentContext: nil];
}
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end
最后记得把JDView
加入到ViewController
的view
上就行了。
实现效果:
效果图.pngshader文件
提取码 : nrmb
shader
的代码中有关于聚光灯模式,平行光模式,点光源模式等不同模式的计算方法,另外一定要清楚,这些光照的计算一定是在fragment_shader中实现的,因为它们的根本目的还是要为像素点上色,光照只不过是更现实的显示出像素点的颜色,得到的结果都是要设置给内建变量gl_FragColor
或者可以自建一个out vec FragColor
,当然还是建议直接使用GLSL的内建变量。