项目做了一个学期还在拖着,假期接手跑了的组员负责的3D仿真部分,这两天差不多是把这部分做完了,顺便做个记录。
c#上使用opengl用的sharpgl,当前版本2.4,看样子也不会更新了,本来想用计算着色器做点后期效果,结果只支持到OpenGL4.0,说好的4.3呢?
看了一圈源码发现都是直接用委托调用opengl的dll,试着照着写了一下新函数结果不行,估计还需要改点别的,导师也说没必要花时间在这个上,就放弃了。
上张效果图
这几天把实现过程(踩到的坑)写一遍,加深一下印象。
VAO与VBO,数据传输
VAO(Vertex Array Object)顶点对象数组与VBO(Vertex Buffer Object)顶点缓存对象类似数组下标与数据的关系,物体的顶点数据(位置、颜色、法向量等)存放在VBO中,并利用相关函数设定各数据所在位置。
float[] verts =
{
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
gl.GenVertexArrays(1, bufferVAO);
gl.BindVertexArray(bufferVAO[0]);
gl.GenBuffers(1, buffer2D);
gl.BindBuffer(OpenGL.GL_ARRAY_BUFFER, buffer2D[0]);
IntPtr p = Marshal.AllocHGlobal(verts.Length * sizeof(float));
Marshal.Copy(verts, 0, p, verts.Length);
gl.BufferData(OpenGL.GL_ARRAY_BUFFER, verts.Length * sizeof(float), p, OpenGL.GL_STATIC_DRAW);
Marshal.FreeHGlobal(p);
gl.EnableVertexAttribArray(0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(0, 3, OpenGL.GL_FLOAT, false, (5 * sizeof(float)), (IntPtr)null);
gl.VertexAttribPointer(1, 2, OpenGL.GL_FLOAT, false, (5 * sizeof(float)), (IntPtr)(3 * sizeof(float)));
verts数组含有4个顶点,每组数据位5个float,前三个代表x、y、z位置信息,后两个为纹理坐标。该数组设定了一个铺屏四边形以便调试;
gl.GenVertexArrays(1, bufferVAO);
gl.BindVertexArray(bufferVAO[0]);
gl.GenBuffers(1, buffer2D);
gl.BindBuffer(OpenGL.GL_ARRAY_BUFFER, buffer2D[0]);
这四行为生成vao、绑定vao,生成vbo,绑定vbo。随后将verts数据传入vbo。
IntPtr p = Marshal.AllocHGlobal(verts.Length * sizeof(float));
Marshal.Copy(verts, 0, p, verts.Length);
gl.BufferData(OpenGL.GL_ARRAY_BUFFER, verts.Length * sizeof(float), p, OpenGL.GL_STATIC_DRAW);
sharpgl只允许使用IntPtr将数组(指针?)作为参数传入,因此前两行为c#特色。
gl.EnableVertexAttribArray(0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(0, 3, OpenGL.GL_FLOAT, false, (5 * sizeof(float)), (IntPtr)null);
gl.VertexAttribPointer(1, 2, OpenGL.GL_FLOAT, false, (5 * sizeof(float)), (IntPtr)(3 * sizeof(float)));
下面启用该VAO中index为0、1所在的顶点数组,并声明数据的分布:index为0(1)的位置大小为3(2),float类型,不进行归一化,每组数据间隔5个float,数据位置在开始没有偏移(偏移3个float)。
Texture tex=new Texture();
tex.Create(gl,"texture.png");
tex.Bind(gl);
加一个sharpgl写好的Texture类加载一个图片,用直接读取纹理的shader显示。
嗯,倒过来了,应该是opengl的坐标问题。
几个发现要注意的地方:
1、vbo自动更新,修改之后不需要重新bufferdata,一次就够。
2、不FreeHGlobal(p)内存爆炸,及时清理。
数据传输差不多就这些,把glsl贴上。
#version 430 core
layout (location = 0) in vec3 vert;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
void main(void)
{
TexCoords = aTexCoords;
gl_Position = vec4(vert,1);
}
#version 430
uniform sampler2D image;
in vec2 TexCoords;
layout (location = 0) out vec4 FragColor;
void main(void)
{
FragColor = vec4(texture(image, TexCoords).rgb, 1.0);
}
FrameBuffer
一个帧缓存就算是一个屏幕,只不过不会显示出来,只有0号FrameBuffer会被及时渲染到显示器上。
gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, 0);
BindFramebuffer()函数为为当前opengl对象(gl)绑定第n个帧缓存,前面的只能写GL_FRAMEBUFFER_EXT;
然后就是要命的地方:自己创建的帧缓存初始是空的,需要自己绑定各种附件。
附件分别有颜色缓存(n个),深度缓存,模板缓存。
framebuffer0默认是绑定好的,自己创建的就得自己绑定,而且就算没有用到深度缓存,不绑定的话是没有深度测试的。就会使遮挡按照顶点先后顺序进行(后来的遮挡之前的):
gl.FramebufferTexture2DEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_DEPTH_ATTACHMENT_EXT, OpenGL.GL_TEXTURE_2D, rboDepth[0], 0);//深度缓存绑定
gl.FramebufferTexture2DEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_COLOR_ATTACHMENT0_EXT + (uint)i, OpenGL.GL_TEXTURE_2D, bright_texture[i], 0);//颜色缓存绑定(输出纹理)
uint[] attachments = { OpenGL.GL_COLOR_ATTACHMENT0_EXT, OpenGL.GL_COLOR_ATTACHMENT1_EXT };
gl.DrawBuffers(2, attachments);//多重渲染写入
这里面的绑定对象都是gentexture生成的纹理数据,用之前要绑定一下然后设置参数什么的:
depth_texture.Bind(gl);
gl.TexImage2D(OpenGL.GL_TEXTURE_2D, 0, OpenGL.GL_DEPTH_COMPONENT32, (int)(width * Settings.ssaa), (int)(height * Settings.ssaa), 0, OpenGL.GL_DEPTH_COMPONENT, OpenGL.GL_FLOAT, null);
这个是深度缓存。
Phong光照
贴上部分fragment shader
//ambient 环境光
float ambientStrength = ambient[int(round(Object))];//环境光强度
float specularStrength = specular[int(round(Object))];//镜面光强度
vec3 diffuse=diff[int(round(Object));
float spec=0;
vec3 ambient = ambientStrength * light_color.xyz;
//diffuse 漫反射
vec3 vertexPosition = (model_matrix*vertex).xyz;
vec3 surfaceNormal = normalize((vertexnormal).xyz);
vec3 lightDirection = normalize(light_position - vertexPosition*ortho);
float diffuseLightIntensity = max(0, dot(surfaceNormal, lightDirection));
diffuse = diffuseLightIntensity * light_color;
//specular 镜面反射
vec3 viewDir = normalize(camera_position - vertexPosition);//视线向量
//vec3 H=normalize(viewDir+lightDirection); //Blinn-Phong
vec3 reflectDir = -lightDirection-2*dot(surfaceNormal,-lightDirection)*surfaceNormal;//反射光向量(和reflect函数一样:reflect(-lightDirection, surfaceNormal))
if(diffuseLightIntensity!=0)
{
spec = pow(max(dot(viewDir, reflectDir), 0.0), diffuse]);
}
vec3 specular = specularStrength * spec *light_color.xyz;
Blinn-Phong说是要快一些
spec =power(max(0,dot(s.Normal, h)),shininess)
h = normalize (lightDir + viewDir)
h是视线与入射光的法线
Phong是用视线与反射光做点乘,Blinn-phong是用视线和入射光的法线与片元法向量做点乘。看上去的话没什么大区别。
阴影,PCF,PCSS
阴影直接用的shadowmap,因此只能实现局部阴影,但由于读取板子的遮挡情况也需要用到shadowmap还有精度要求,只能限制地面在一定范围内,因此就不用立方体贴图做阴影了,有机会再写一下CSM。
不做处理:
柔和阴影:
自适应阴影:
然而PCSS做出来的阴影总有一圈一圈的阴影,很奇怪,搞了好久都没弄好。
而且PCF和PCSS都会暗一圈,可能是遮挡距离判定的问题?
HDR、Bloom
HDR使帧缓存中的颜色缓存输出不被裁剪到0~1范围内,因此超出1的高亮区域可以被提取出来
通过多次高斯模糊得到Bloom效果:
最后融混
感觉加了光晕之后会写实一点
交互(坐标轴移动、选择、删除)
picking的话就是用颜色做编码。
int selectId = selectedPixel[2] + selectedPixel[1] * 32 + selectedPixel[0] * 32 * 32 < objNum ? selectedPixel[2] + selectedPixel[1] * 32 + selectedPixel[0] * 32 * 32 : 0;
乘32是颜色间隔为8,试过乘64但是在id过大的时候会错位。可能是glsl中float转换到int的问题,32^3是32768,够用了。
看着3dsmax的坐标轴移动很好用,也打算照着做一个
导入三个arrow模型,旋转一下就是坐标轴的样子。但是要保证坐标轴在最前端,因此在vertex shader阶段把坐标轴的position.z提前
if(id>0.5f && id<3.5f)
{
gl_Position.z/=10;
}
然而这种方法引入了分支结构,据说gpu处理分支结构会慢,但这样写起来还是比较简单;用blending也可以做,但就要多一个渲染流程,感觉会更慢。
有问题的地方在于如何将屏幕上的移动操作映射到空间中。
用mv矩阵分别乘xyz方向上的单位向量再归一化,可以获得坐标轴在视口空间的方向向量。
如果乘上projectionview会出问题
vec4 x = new vec4(obj[objName[Util.selectId]].offset.x + 1, obj[objName[Util.selectId]].offset.y, obj[objName[Util.selectId]].offset.z, 1);
var mvp = viewMatrix * modelMatrix;
x = multiply(mvp, x);
vec4 y = new vec4(obj[objName[Util.selectId]].offset.x, obj[objName[Util.selectId]].offset.y + 1, obj[objName[Util.selectId]].offset.z, 1);
y = multiply(mvp, y);
vec4 z = new vec4(obj[objName[Util.selectId]].offset.x, obj[objName[Util.selectId]].offset.y, obj[objName[Util.selectId]].offset.z + 1, 1);
z = multiply(mvp, z);
vec4 o = new vec4(obj[objName[Util.selectId]].offset.x, obj[objName[Util.selectId]].offset.y, obj[objName[Util.selectId]].offset.z, 1);
o = multiply(mvp, o);
Camera.xDirect = x - o;
Camera.yDirect = y - o;
Camera.zDirect = z - o;
Camera.xDirect.z = 0;
Camera.yDirect.z = 0;
Camera.zDirect.z = 0;
var tmp = glm.normalize(new vec3(Camera.xDirect.x, Camera.xDirect.y, Camera.xDirect.z));
Camera.xDirect = new vec4(tmp.x, tmp.y, tmp.z, Camera.xDirect.w);
tmp = glm.normalize(new vec3(Camera.yDirect.x, Camera.yDirect.y, Camera.yDirect.z));
Camera.yDirect = new vec4(tmp.x, tmp.y, tmp.z, Camera.yDirect.w);
tmp = glm.normalize(new vec3(Camera.zDirect.x, Camera.zDirect.y, Camera.zDirect.z));
Camera.zDirect = new vec4(tmp.x, tmp.y, tmp.z, Camera.zDirect.w);