OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
OpenGL之触控反馈
OpenGL之粒子
OpenGL之天空盒
OpenGL之地形
光源分类
通常可以把不同的光源分为下面几种
光源 | 说明 |
---|---|
环境光(Ambient light) | 环境光看上去来自四面八方,场景中的一切被照亮的程度都一样。这近似于我们从大的、平等的光源获取的光照,比如天空 |
方向光(Directional light) | 方向光看上去似乎来自一个方向,光源好像处于极其远的地方。这与我们从太阳或月亮获取的光照相似 |
点光(Point light) | 点光看上去是从附近某处投射的光亮,而且光的密度随着距离而减少。这适于表示近处的光源,其把它们的光投射到四面八方,像一个灯泡或蜡烛一样 |
聚光(Spot light) | 聚光与点光类似,只是加了一个限制,只能向一个特定的方向投射。这是我们从手电筒或者聚光灯所获得的光照类型 |
把光线在物体表面反射的方式分为两类:
反射方式 | 说明 |
---|---|
漫反射(Diffuse reflection) | 漫反射是指光线平等地向所有方向蔓延,适于表示没有抛光表面的材质,比如地毯或外面的混凝土墙 |
镜面反射( Specular reflection) | 镜面反射在某个特定的方向上反射更加强烈,适于被抛光的或者闪亮的材质,比如光滑的金属或者刚刚打过蜡的汽车 |
朗伯体反射实现方向光
朗伯体反射描述了这样一个表面,它会反射所有方向上打到它的光线,以使它在任何观察点上看起来都是一样的。它的外观只依赖于它与光源所处的方位及其距离
使用一个水平表面和单个的、不随距离而减弱的方向光源,因此,唯一有影响的就是这个表面相对于该光源所处的方位
直接面对光源的表面 | 与光源成一定角度的表面 |
---|---|
我们可以看到表面垂直正对光源时,在这个角度,它捕捉并反射尽可能多的光线;表面相对光源旋转了45度时,这个表面反射的光线与旋转角的余弦有关
要计算出一个表面接收了多少光线,我们所需要做的就是计算出它直接面向光源时接收了多少光线,再把结果乘以那个角度的余弦值
计算高度图的方位
在给高度图添加朗伯体反射之前,需要某个方法获知其表面的方位是什么。因为高度图不是一个水平的表面,我们需要计算高度图上每个点的方位。可以用表面法线(surface normal)表示方位,它是一个特殊类型的向量,它垂直于表面,且有一个值为1的单位长度
因为表面法线是应用于表面的而不是点,在计算每个点的法线时,把这个点的邻接点合并在一起创建一个平面。我们将用两个向量来表示这个平面:一个从右侧的点指向左侧的点,另外一个从上面的点指向下面的点。如果我们计算这两个向量的叉积,就能得到一个垂直于这个平面的向量,然后,我们可以归一化那个向量以得到中间点的表面法线,为什么使用从右向左的向量,而不是从左向右呢?因为我们想要这个表面法线指向上方,离开高度图,所以我们用叉积的右手规则计算出每个向量需要指向的方向
我们假定每个点占用一个单位部分,x值向右增加,z值向下增加。其上边、左边、右边和下边的点的高度分别是0.2、0.1、0.1和0.1。要计算从右向左的向量,我们用右边的点减去左边的点得到值为(-2,0,0)的向量,再用上边和下边的点做同样的计算得到值为(0,-0.1,2)的向量。一旦有了这两个向量,我们就可以计算它们的叉积得到值为(0,4,0.2)的向量,我们再把这个向量归一化得到值为(0,0.9988,0.05) 的表面法线
给高度图加入法线向量
public class Heightmap {
// 法线向量占的位数
private static final int NORMAL_COMPONENT_COUNT = 3;
private static final int TOTAL_COMPONENT_COUNT = NORMAL_COMPONENT_COUNT + POSITION_COMPONENT_COUNT;
private static final int STRIDE = TOTAL_COMPONENT_COUNT * BYTES_PER_FLOAT;
private int mNumOfIndex;
private VertexBuffer mVertexBuffer;
private IndexBuffer mIndexBuffer;
private int mWidth;
private int mHeight;
public Heightmap(Bitmap bitmap) {
mWidth = bitmap.getWidth();
mHeight = bitmap.getHeight();
initVertexBuffer(bitmap);
initIndexBuffer();
}
/**
* 绑定着色器中 attribute 数据,包括位置和法线向量
*
* @param heightmapProgram
*/
public void bindData(HeightmapProgram heightmapProgram) {
int offset = 0;
mVertexBuffer.setVertexAttribPointer(
offset,
heightmapProgram.getAPosition(),
POSITION_COMPONENT_COUNT,
STRIDE
);
offset += POSITION_COMPONENT_COUNT * BYTES_PER_FLOAT;
mVertexBuffer.setVertexAttribPointer(
offset,
heightmapProgram.getANormal(),
NORMAL_COMPONENT_COUNT,
STRIDE
);
}
public void draw() {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer.getBufferId());
glDrawElements(GL_TRIANGLES, mNumOfIndex, GL_UNSIGNED_SHORT, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
/**
* 初始化索引Buffer
*/
private void initIndexBuffer() {
mNumOfIndex = numOfIndex();
short[] indexData = new short[mNumOfIndex];
int offset = 0;
for (short row = 0; row < mHeight - 1; row++) {
for (short col = 0; col < mWidth - 1; col++) {
short topLeft = (short) (row * mWidth + col);
short topRight = (short) (topLeft + 1);
short bottomLeft = (short) ((row + 1) * mWidth + col);
short bottomRight = (short) (bottomLeft + 1);
indexData[offset++] = topLeft;
indexData[offset++] = bottomLeft;
indexData[offset++] = topRight;
indexData[offset++] = topRight;
indexData[offset++] = bottomLeft;
indexData[offset++] = bottomRight;
}
}
mIndexBuffer = new IndexBuffer(indexData);
}
/**
* 初始化顶点Buffer,加上法向向量
*
* @param bitmap
*/
private void initVertexBuffer(Bitmap bitmap) {
float[] vertexData = new float[mWidth * mHeight * TOTAL_COMPONENT_COUNT];
int offset = 0;
int[] pixels = new int[mWidth * mHeight];
bitmap.getPixels(pixels, 0, mWidth, 0, 0, mWidth, mHeight);
for (int row = 0; row < mHeight; row++) {
for (int col = 0; col < mWidth; col++) {
// 点的位置
Point point = getPoint(pixels, row, col);
vertexData[offset++] = point.getX();
vertexData[offset++] = point.getY();
vertexData[offset++] = point.getZ();
//点的上下左右
Point top = getPoint(pixels, row - 1, col);
Point left = getPoint(pixels, row, col - 1);
Point right = getPoint(pixels, row, col + 1);
Point bottom = getPoint(pixels, row + 1, col);
// 得到法向向量
Vector rightToLeft = Vector.vectorBetween(right, left);
Vector topToBottom = Vector.vectorBetween(top, bottom);
Vector normal = rightToLeft.crossProduct(topToBottom).normalize();
// 将法向向量写入顶点数据数组中
vertexData[offset++] = normal.getX();
vertexData[offset++] = normal.getY();
vertexData[offset++] = normal.getZ();
}
}
mVertexBuffer = new VertexBuffer(vertexData);
}
/**
* 获取pixels像素中的某个像素的三维空间位置
*
* @param pixels
* @param row
* @param col
* @return
*/
private Point getPoint(int[] pixels, int row, int col) {
float x = (col / (float) (mWidth - 1)) - 0.5f;
float z = (row / (float) (mHeight - 1)) - 0.5f;
row = clamp(row, 0, mHeight - 1);
col = clamp(col, 0, mWidth - 1);
int pixel = pixels[row * mWidth + col];
float y = Color.red(pixel) / 255f;
return new Point(x, y, z);
}
private int clamp(int value, int min, int max) {
return Math.min(max, Math.max(value, min));
}
/**
* 返回高度图的索引数目
* 相邻四个像素点形成1个正方形即2个三角形,即6个顶点,整个bitmap形成(mWidth - 1) * (mHeight - 1) 个正方形
*
* @return
*/
private int numOfIndex() {
return (mWidth - 1) * (mHeight - 1) * 2 * 3;
}
}
给着色器加入方向光
顶点着色器
uniform mat4 u_Matrix;
uniform mat4 u_MVMatrix;
uniform mat4 u_IT_MVMatrix;
uniform vec4 u_PointLightPositions[3];
uniform vec3 u_PointLightColors[3];
vec3 materialColor;
vec4 eyeSpacePosition;
vec3 eyeSpaceNormal;
attribute vec4 a_Position;
varying vec3 v_Color;
uniform vec3 u_VectorToLight;
attribute vec3 a_Normal;
vec3 getAmbientLighting();
vec3 getDirectionalLighting();
vec3 getPointLighting();
void main()
{
materialColor = mix(vec3(0.180, 0.467, 0.153),
vec3(0.660, 0.670, 0.680),
a_Position.y);
eyeSpacePosition=u_MVMatrix*a_Position;
eyeSpaceNormal=normalize(vec3(u_IT_MVMatrix*vec4(a_Normal, 0.0)));
v_Color=getAmbientLighting();
v_Color+=getDirectionalLighting();
v_Color+=getPointLighting();
gl_Position = u_Matrix * a_Position;
}
vec3 getAmbientLighting(){
return materialColor*0.1;
}
vec3 getDirectionalLighting(){
return materialColor*0.3*max(dot(eyeSpaceNormal, u_VectorToLight), 0.0);
}
vec3 getPointLighting(){
vec3 lightingSum=vec3(0.0);
for (int i=0;i<3;i++){
vec3 toPointLight=vec3(u_PointLightPositions[i])-vec3(eyeSpacePosition);
float diatance=length(toPointLight);
toPointLight=normalize(toPointLight);
float cos=max(dot(toPointLight, eyeSpaceNormal), 0.0);
lightingSum+=materialColor*u_PointLightColors[i]*5.0*cos/diatance;
}
return lightingSum;
}
变量说明
变量 | 说明 |
---|---|
u_Matrix | 模型视图投影矩阵 |
u_MVMatrix | 表示模型视图矩阵,位置与其相乘,就会将位置信息装换到眼空间中 |
u_IT_MVMatrix | 表示模型视图矩阵倒置矩阵的转置矩阵,平面法线与之相乘,在进行归一化,可以取消缩放的影响 |
u_PointLightPositions | 点光源的位置,在眼空间中 |
u_PointLightColors | 点光源颜色 |
u_VectorToLight | 存储方向光源的归一化向量,在眼空间中 |
a_Position | 顶点位置信息 |
a_Normal | 高度图法线 |
v_Color | 最终片段的颜色信息,因为片段着色器未做任何改动,直接将该值赋值给了最终片段的颜色信息 |
materialColor | 山脉颜色,由mix函数根据山脉高度计算得到 |
eyeSpacePosition | 顶点在眼空间中的位置,由a_Position和u_MVMatrix 进行转换得到 |
eyeSpaceNormal | 在眼空间中的高度图法线,由a_Normal 和u_IT_MVMatrix 进行转换并归一化得到 |
方法说明
方法 | 说明 |
---|---|
getAmbientLighting() | 获取环境光。其值设为山脉颜色的0.1,将全局都提亮一些 |
getDirectionalLighting() | 获取方向光。相当于太阳,其光源值定为materialColor*0.3,通过计算指向光源的向量与表面法线的点积,来计算表面与光线之间夹角的余弦值。它的工作原理是,当两个向量都是归一化的向量时,那两个向量的点积就是它们之间夹角的余弦,为了避免出现负的结果,用max()把最小余弦值限制为0,然后,应用这个光线,把当前顶点的颜色与余弦值相乘。余弦值在0和1之间,因此,最终的颜色将是处于黑色和原色之间的某个颜色 |
getPointLighting() | 获取点光。对于当前顶点,循环计算每个点光,得到当前顶点到点光的向量并归一化,然后计算夹角余弦,再除以距离,并把其结果加人lightingSum,就是所有点光对当前顶点的光照值了,乘以5是为了放到效果,使其更明亮 |
更新着色器封装代码
public class HeightmapProgram extends BaseProgram {
private static final String U_MV_MATRIX = "u_MVMatrix";
private static final String U_IT_MV_MATRIX = "u_IT_MVMatrix";
private static final String U_VECTOR_TO_LIGHT = "u_VectorToLight";
private static final String U_POINT_LIGHT_POSITIONS = "u_PointLightPositions";
private static final String U_POINT_LIGHT_COLORS = "u_PointLightColors";
private static final String A_NORMAL = "a_Normal";
private final int mUMatrix;
private final int mUVectorToLight;
private final int mUMVMatrix;
private final int mUITMVMatrix;
private final int mUPointLightPositions;
private final int mUPointLightColors;
private final int mAPosition;
private final int mANormal;
public HeightmapProgram(Context context) {
super(context, R.raw.light9_heightmap_vertex, R.raw.particles8_heightmap_fragment);
mUMatrix = glGetUniformLocation(mProgram, U_MATRIX);
mUMVMatrix = glGetUniformLocation(mProgram, U_MV_MATRIX);
mUITMVMatrix = glGetUniformLocation(mProgram, U_IT_MV_MATRIX);
mUVectorToLight = glGetUniformLocation(mProgram, U_VECTOR_TO_LIGHT);
mUPointLightPositions = glGetUniformLocation(mProgram, U_POINT_LIGHT_POSITIONS);
mUPointLightColors = glGetUniformLocation(mProgram, U_POINT_LIGHT_COLORS);
mAPosition = glGetAttribLocation(mProgram, A_POSITION);
mANormal = glGetAttribLocation(mProgram, A_NORMAL);
}
public void setUniforms(float[] mvMatrix, float[] it_MVMatrix, float[] matrix,
float[] vectorToLight, float[] pointLightPositions, float[] pointLightColors) {
glUniformMatrix4fv(mUMVMatrix, 1, false, mvMatrix, 0);
glUniformMatrix4fv(mUITMVMatrix, 1, false, it_MVMatrix, 0);
glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
glUniform3fv(mUVectorToLight, 1, vectorToLight, 0);
glUniform4fv(mUPointLightPositions, 3, pointLightPositions, 0);
glUniform3fv(mUPointLightColors, 3, pointLightColors, 0);
}
public int getAPosition() {
return mAPosition;
}
public int getANormal() {
return mANormal;
}
}
更新渲染器代码
public class MyRenderer implements GLSurfaceView.Renderer, ITouchRenderer {
private static final String TAG = "MyRenderer";
/*private static final int[] SKY_ID = new int[]{
R.drawable.left, R.drawable.right,
R.drawable.bottom, R.drawable.top,
R.drawable.front, R.drawable.back,
};*/
private static final int[] SKY_ID = new int[]{
R.drawable.night_left, R.drawable.night_right,
R.drawable.night_bottom, R.drawable.night_top,
R.drawable.night_front, R.drawable.night_back,
};
private Context mContext;
private float[] modelMatrix = new float[16];
private float[] viewMatrix = new float[16];
private float[] viewMatrixForSkybox = new float[16];
private float[] projectionMatrix = new float[16];
private float[] tempMatrix = new float[16];
private float[] modelViewProjectionMatrix = new float[16];
private float[] modelViewMatrix = new float[16];
private float[] it_modelViewMatrix = new float[16];
private HeightmapProgram mHeightmapProgram;
private Heightmap mHeightmap;
private ParticlesProgram mParticlesProgram;
private ParticleSystem mParticleSystem;
private ParticleShooter mRedParticleShooter;
private ParticleShooter mGreenParticleShooter;
private ParticleShooter mBlueParticleShooter;
// 控制粒子的角度
private float mAngleVarianceInDegree = 30f;
// 控制粒子的速度
private float mSpeedVariance = 1f;
private long mGlobalStartTime;
private SkyBox mSkyBox;
private SkyProgram mSkyProgram;
private int mTextureId;
private int mCubeTextureId;
private float xRotate;
private float yRotate;
// 方向光源,指向太阳
private final float[] vectorToLight = {0.3f, 0.35f, -0.89f, 0f};
// 点光源位置
private final float[] pointLightPositions = new float[]{
-1f, 1f, 0f, 1f,
0f, 1f, 0f, 1f,
1f, 1f, 0f, 1f
};
// 点光源颜色
private final float[] pointLightColors = new float[]{
1f, 0.2f, 0.02f,
0.02f, 0.25f, 0.02f,
0.02f, 0.2f, 1f
};
public MyRenderer(Context context) {
mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0f, 0f, 0f, 0f);
glEnable(GLES20.GL_DEPTH_TEST);
glEnable(GLES20.GL_CULL_FACE);
// 初始化高度图
mHeightmapProgram = new HeightmapProgram(mContext);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.heightmap, options);
mHeightmap = new Heightmap(bitmap);
// 粒子着色器程序
mParticlesProgram = new ParticlesProgram(mContext);
// 粒子系统,maxParticleCount如果太小的话,可能只能看到比较新的粒子
mParticleSystem = new ParticleSystem(10000);
// 粒子方向
com.test.opengl.light9.geometry.bean.Vector particleDirection = new Vector(0f, 0.5f, 0f);
// 粒子发射器
mRedParticleShooter = new ParticleShooter(new Point(-1f, 0f, 0f), particleDirection, Color.RED, mAngleVarianceInDegree, mSpeedVariance);
mGreenParticleShooter = new ParticleShooter(new com.test.opengl.light9.geometry.bean.Point(0f, 0f, 0f), particleDirection, Color.GREEN, mAngleVarianceInDegree, mSpeedVariance);
mBlueParticleShooter = new ParticleShooter(new Point(1f, 0f, 0f), particleDirection, Color.BLUE, mAngleVarianceInDegree, mSpeedVariance);
// 粒子系统启动的时间
mGlobalStartTime = System.nanoTime();
// 使用图片的样式绘制粒子
mTextureId = com.test.opengl.light9.util.TextureHelper.loadTexture(mContext, R.drawable.particle_texture);
mSkyProgram = new SkyProgram(mContext);
mSkyBox = new SkyBox();
mCubeTextureId = TextureHelper.loadCubeTexture(mContext, SKY_ID);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
glViewport(0, 0, width, height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width / height, 1, 10);
// 更新视图矩阵
updateViewMatrix();
}
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawSky();
drawHeightmap();
drawParticles();
}
private void drawHeightmap() {
// 更新矩阵
setIdentityM(modelMatrix, 0);
// 用模型矩阵使高度图在x和z方向上变宽100倍,而在y方向上只变高10倍
// 着色器中的颜色插值依赖于顶点所在位置的y值,这不会扰乱,因为在顶点着色器中,设置v_Color的时间是在我们把它与矩阵相乘之前
Matrix.scaleM(modelMatrix, 0, 80f, 30f, 80f);
updateMvpMatrix();
mHeightmapProgram.useProgram();
final float[] vectorToLightInEyeSpace = new float[4];
final float[] pointPositionsInEyeSpace = new float[12];
Matrix.multiplyMV(vectorToLightInEyeSpace, 0, viewMatrix, 0, vectorToLight, 0);
Matrix.multiplyMV(pointPositionsInEyeSpace, 0, viewMatrix, 0, pointLightPositions, 0);
Matrix.multiplyMV(pointPositionsInEyeSpace, 4, viewMatrix, 0, pointLightPositions, 4);
Matrix.multiplyMV(pointPositionsInEyeSpace, 8, viewMatrix, 0, pointLightPositions, 8);
mHeightmapProgram.setUniforms(modelViewMatrix, it_modelViewMatrix, modelViewProjectionMatrix,
vectorToLightInEyeSpace, pointPositionsInEyeSpace, pointLightColors);
mHeightmap.bindData(mHeightmapProgram);
mHeightmap.draw();
}
private void drawParticles() {
// 关闭深度测试的写操作
glDepthMask(false);
// 创建并添加粒子
float currentTime = (System.nanoTime() - mGlobalStartTime) / 1000000000f;
mRedParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mGreenParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mBlueParticleShooter.addParticle(mParticleSystem, currentTime, 5);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrix();
// 累计混合技术,粒子越多,就越亮
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
mParticlesProgram.useProgram();
// 设置uniform的值
mParticlesProgram.setUniforms(modelViewProjectionMatrix, currentTime, mTextureId);
// to do ,注释掉mParticleShooter.bindData(mParticlesProgram);模拟器就不会挂
// 绑定粒子系统数据
mParticleSystem.bindData(mParticlesProgram);
// 绘制粒子系统的所有粒子
mParticleSystem.draw();
// 一次绘制完成后,关闭累计混合技术
glDisable(GL_BLEND);
glDepthMask(true);
}
private void drawSky() {
// 深度测试算法改为小于等于,让天空盒被绘制出来
glDepthFunc(GL_LEQUAL);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrixForSky();
mSkyProgram.useProgram();
mSkyProgram.setUniforms(modelViewProjectionMatrix, mCubeTextureId);
mSkyBox.bindData(mSkyProgram);
mSkyBox.draw();
// 深度测试算法改回去,以免影响其他物体的绘制
glDepthFunc(GL_LESS);
}
/**
* 更新高度图和粒子的模型视图投影矩阵
*/
private void updateMvpMatrix() {
// 得到视图模型矩阵
Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
// 视图模型矩阵的反转矩阵
Matrix.invertM(tempMatrix, 0, modelViewMatrix, 0);
// 视图模型矩阵的反转矩阵的转置矩阵
Matrix.transposeM(it_modelViewMatrix, 0, tempMatrix, 0);
Matrix.multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0);
}
/**
* 更新天空盒的模型视图投影矩阵
*/
private void updateMvpMatrixForSky() {
Matrix.multiplyMM(tempMatrix, 0, viewMatrixForSkybox, 0, modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
}
/**
* 处理拖拽事件
*
* @param deltaX
* @param deltaY
*/
public void handleDrag(float deltaX, float deltaY) {
Log.d(TAG, "handleDrag: deltaX=" + deltaX + " deltaY=" + deltaY);
xRotate += deltaX / 16f;
yRotate += deltaY / 16f;
if (yRotate > 90) {
yRotate = 90;
}
if (yRotate < -90) {
yRotate = -90;
}
// 更新视图矩阵
updateViewMatrix();
}
@Override
public void handlePress(float normalizedX, float normalizedY) {
}
/**
* 更新视图矩阵,包括viewMatrix和viewMatrixForSkybox
* 高度图和粒子的视图矩阵viewMatrix,代表相机,它应用于所有的物体
* 天空盒视图矩阵viewMatrixForSkybox,只表示旋转
*/
private void updateViewMatrix() {
setIdentityM(viewMatrix, 0);
Matrix.rotateM(viewMatrix, 0, -yRotate, 1f, 0f, 0f);
Matrix.rotateM(viewMatrix, 0, -xRotate, 0f, 1f, 0f);
// 使用 viewMatrixForSkybox 旋转天空盒
System.arraycopy(viewMatrix, 0, viewMatrixForSkybox, 0, viewMatrix.length);
// 使用 viewMatrix 一起旋转和平移高度图和粒子
translateM(viewMatrix, 0, 0, -1.5f, -5f);
}
}