前言
前幾章我們簡單介紹了一下如何通過Google提供的SDK來展示全景圖和VR視頻受楼。這章節(jié)我們來介紹如何手動渲染VR場景,主要涉及兩個重要的類:GvrActivity和GvrView。
GvrActivity
先依賴庫
implementation 'com.google.vr:sdk-base:1.180.0'
GvrActivity提供了VR相關(guān)的一些細節(jié)膳凝,通過代碼可以看到,它持有一個GvrView對象恭陡,但是需要我們手動set才可以蹬音,GvrActivity處理了一些相關(guān)事件以及管理GvrView的生命周期等等。
所以我們需要先創(chuàng)建一個繼承GvrActivity休玩,然后通過setGvrView(GvrView gvrView)
函數(shù)將GvrView對象賦給它著淆,這樣有些事件,比如GvrView的生命周期管理等就不需要我們再操心了拴疤。
GvrView
GvrView才是用來渲染的永部,我們在這個View上渲染VR場景或組件。它有兩個接口呐矾,如下:
public interface StereoRenderer {
@UsedByNative
void onNewFrame(HeadTransform headTransform);
@UsedByNative
void onDrawEye(Eye eye);
@UsedByNative
void onFinishFrame(Viewport viewport);
void onSurfaceChanged(int width, int height);
void onSurfaceCreated(EGLConfig config);
void onRendererShutdown();
}
public interface Renderer {
@UsedByNative
void onDrawFrame(HeadTransform headTransform, Eye leftEye, Eye rightEye);
@UsedByNative
void onFinishFrame(Viewport viewport);
void onSurfaceChanged(int width, int height);
void onSurfaceCreated(EGLConfig config);
void onRendererShutdown();
}
這里注意幾個重要的函數(shù):
- onSurfaceCreated:這里我們可以進行一些初始化操作苔埋。比如初始化材料,創(chuàng)建Buffer蜒犯,加載著色器等等
- onNewFrame:StereoRenderer獨有的组橄,在繪制一幀畫面前做一些準備工作。
- onDrawEye/onDrawFrame:在這里我們進行繪制工作罚随。
下面我們用一個簡單的demo來看看GvrActivity和GvrView如何使用玉工。
簡單demo
源碼如下:
import android.os.Bundle
import com.google.vr.sdk.base.*
import com.huichongzi.vrardemo.databinding.ActivityGvrDemoBinding
import javax.microedition.khronos.egl.EGLConfig
import android.opengl.GLES30
class GvrDemoActivity : GvrActivity(), GvrView.StereoRenderer {
private var _binding : ActivityGvrDemoBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityGvrDemoBinding.inflate(layoutInflater)
setContentView(_binding?.root)
//初始化
gvrView = _binding?.gvrView
gvrView.setEGLConfigChooser(8,8,8,8,16,8)
gvrView.setRenderer(this)
gvrView.setTransitionViewEnabled(true)
gvrView.enableCardboardTriggerEmulation()
}
override fun onNewFrame(headTransform: HeadTransform?) {
}
override fun onDrawEye(eye: Eye?) {
GLES30.glEnable(GLES30.GL_DEPTH_TEST) //啟用深度測試,自動隱藏被遮住的材料
//清除緩沖區(qū)淘菩,即將緩沖區(qū)設(shè)置為glClearColor設(shè)定的顏色
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT)
}
override fun onFinishFrame(viewport: Viewport?) {
}
override fun onSurfaceChanged(width: Int, height: Int) {
}
override fun onSurfaceCreated(config: EGLConfig?) {
//設(shè)置清除顏色遵班,即背景色
GLES30.glClearColor(0f,0f,1f,1f)
}
override fun onRendererShutdown() {
}
override fun onBackPressed() {
finish()
}
}
布局很簡單,只有一個GvrView瞄勾。
我們這個Activity實現(xiàn)了GvrView.StereoRenderer
接口费奸。在onCreate
中將GvrView實例set一下,并且通過setRenderer
函數(shù)將GvrView.StereoRenderer
關(guān)聯(lián)GvrView进陡。然后我們在GvrView.StereoRenderer
的對應(yīng)函數(shù)中實現(xiàn)渲染即可。
在這個demo中微服,我們只是繪制了一個藍色背景色趾疚。在onSurfaceCreated
中設(shè)置清除顏色(背景色),然后在onDrawEye
中清除緩沖區(qū)(用剛才設(shè)置的顏色)以蕴,這樣就繪制了藍色的背景色糙麦,運行結(jié)果如下:
這樣我們就可以看到背景色了,下一步我們就可以繪制一些物體丛肮。
OpenGL繪制三角形
我們先來復(fù)習一下如何用OpenGL來繪制圖形赡磅。OpenGL繪制的基本單元有點、線和三角形宝与,其他所有的圖形實際上都是由三角形組成的焚廊。所以我們來看看如何用OpenGL繪制三角形冶匹。
由于OpenGL相關(guān)知識過于龐大,這里就不詳細解釋太多咆瘟,大家自行查閱OpenGL資料嚼隘。
著色器
首先我們需要創(chuàng)建頂點著色器(Vrtex Shader)和片段著色器(Fragment Shader)。著色器是用來實現(xiàn)圖像渲染的袒餐,用來替代固定渲染管線的可編輯程序飞蛹,在整個繪制過程中發(fā)揮著重要的作用,如圖:
其中頂點著色器主要就是處理頂點數(shù)據(jù)的灸眼,比如位置卧檐、顏色;片段著色器就是處理每個片段的顏色和屬性焰宣。著色器相關(guān)知識一兩句說不清楚泄隔,大家自行查閱資料吧。
其實著色器就是一段程序代碼宛徊,所以我們可以直接用字符串佛嬉。但是在Android Studio中提供了著色器類型文件,即glsl文件闸天。如果想創(chuàng)建這樣的文件暖呕,首先需要為Android Studio安裝一個GLSL Support插件
然后我們在創(chuàng)建新文件的菜單中就會發(fā)現(xiàn)多出一個GLSL Shader類型
我們在raw目錄下創(chuàng)建一個頂點著色器vertex_simple_shade.glsl
attribute vec4 vPosition;
void main() {
gl_Position = vPosition;
gl_PointSize = 10.0;
}
這里簡單的將輸入的頂點坐標vPosition拷貝給gl_Position,并設(shè)置頂點直徑為10(繪制點的時候會用到)苞氮。
再創(chuàng)建一個片段著色器fragment_simple_shade.glsl
precision mediump float;
void main() {
gl_FragColor = vec4(1.0,1.0,1.0,1.0);
}
只是設(shè)定顏色為白色而已湾揽。關(guān)于著色器語法大家同樣查閱資料吧。
加載著色器
首先創(chuàng)建編譯著色器笼吟,的到著色器id库物,代碼如下:
fun compileShader(type: Int, code : String) : Int{
//創(chuàng)建一個著色器
val id = GLES20.glCreateShader(type)
if(id != 0){
//載入著色器源碼
GLES20.glShaderSource(id, code)
//編譯著色器
GLES20.glCompileShader(id)
//檢查著色器是否編譯成功
val status = IntArray(1)
GLES20.glGetShaderiv(id, GLES20.GL_COMPILE_STATUS, status, 0)
if(status[0] == 0){
//創(chuàng)建失敗
GLES20.glDeleteShader(id)
return 0
}
return id
}
else {
return 0
}
}
這里
code就是將上面我們創(chuàng)建的glsl文件讀取為字符串即可,所以我們前面說著色器程序直接使用字符串也可以贷帮。
type就是著色器類型戚揭,頂點著色器
GLES20.GL_VERTEX_SHADER
和片段著色器GLES20.GL_FRAGMENT_SHADER
再得到頂點和片段著色器的id后,下一步將它們鏈接到程序中撵枢,代碼如下:
fun linkProgram(vertexShaderId : Int, fragmentShaderId : Int) : Int{
//創(chuàng)建一個GLES程序
val id = GLES20.glCreateProgram()
if(id != 0){
//將頂點和片元著色器加入程序
GLES20.glAttachShader(id, vertexShaderId)
GLES20.glAttachShader(id, fragmentShaderId)
//鏈接著色器程序
GLES20.glLinkProgram(id)
//檢查是否鏈接成功
val status = IntArray(1)
GLES20.glGetProgramiv(id, GLES20.GL_LINK_STATUS, status, 0)
if(status[0] == 0){
GLES20.glDeleteProgram(id)
return 0
}
return id
}
else{
return 0
}
}
這樣我們就得到了一個programId特纤,最后通過GLES20.glUseProgram(programId)
將其使用起來即可落萎,后面我們會看到在哪一步來處理這些豌鸡。
繪制
準備好著色器后磺平,我們就可以著手繪制三角形了,在頁面中添加一個GLSurfaceView沃但,然后我們準備三個頂點坐標磁滚,如下
val triangleCoords = floatArrayOf(0.5f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
)
OpenGL的坐標系是屏幕中心是(0,0),上下左右的最大值都是1宵晚,所以在手機上x軸和y軸上相同數(shù)值對應(yīng)的實際長度并不一樣垂攘,所以上面數(shù)值上雖然看著是等腰三角形维雇,但是實際上并不是。
然后為GLSurfaceView設(shè)置Radnerer搜贤,并繪制三角形谆沃,代碼如下:
setRenderer(object : GLSurfaceView.Renderer{
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
//設(shè)置背景顏色
GLES20.glClearColor(0f,0f,1f,1f)
//初始化頂點坐標緩沖
vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(
ByteOrder.nativeOrder()).asFloatBuffer()
vertexBuffer.put(triangleCoords)
vertexBuffer.position(0)
activity?.apply {
//加載著色器
val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.vertex_simple_shade, this)
val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
program = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)
GLES20.glUseProgram(program)
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
//設(shè)置視圖窗口
GLES20.glViewport(0,0,width,height)
}
override fun onDrawFrame(gl: GL10?) {
//繪制背景
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
val attr = GLES20.glGetAttribLocation(program, "vPosition")
//準備坐標數(shù)據(jù)
GLES20.glVertexAttribPointer(attr, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)
//啟用頂點位置句柄,注意這里是屬性0仪芒,對應(yīng)著頂點著色器中的layout (location = 0)
GLES20.glEnableVertexAttribArray(attr)
//繪制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)
//禁用頂點位置句柄
GLES20.glDisableVertexAttribArray(attr)
}
})
在onSurfaceCreated初始化頂點數(shù)據(jù)緩沖唁影,然后加載著色器。
在onDrawFrame中繪制三角形掂名,注意這里先通過GLES20.glGetAttribLocation(program, "vPosition")
獲取我們在頂點著色器中定義的vPosition屬性据沈,然后將頂點坐標傳入,最后通過GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)
來繪制三角形饺蔑。
有關(guān)OpenGL各個函數(shù)這里同樣不詳細介紹了锌介,這里只重點說說glDrawArrays這個函數(shù)。它有三個參數(shù):
- mode:繪制模式猾警。這里有很多中孔祸,比如我們用的
GLES20.GL_TRIANGLES
就是三角形;還有點GLES20.GL_POINTS
发皿,繪制出來就是三個點崔慧;還有閉環(huán)的線GLES20.GL_LINE_LOOP
,繪制出來就是三角形的三條邊穴墅;還有非閉環(huán)的線GLES20.GL_LINE_STRIP
等等 - first:從哪個點開始
- count:繪制點的數(shù)量
繪制結(jié)果就不上圖了惶室,就是一個三角形。
這樣我們回顧了如何繪制三角形玄货,接下來看看如果在GvrView中繪制一個三角形皇钞。
繪制三角形
在GvrView中繪制三角形與在上面類似,同樣需要兩個著色器松捉,我們先復(fù)用上面兩個看看是什么效果夹界。
然后在onSurfaceCreated中加載著色器,在onDrawEye中繪制三角形即可惩坑,代碼如下:
override fun onSurfaceCreated(config: EGLConfig?) {
GLES20.glClearColor(0f,0f,1f,1f)
//初始化頂點坐標緩沖
vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
vertexBuffer.put(triangleCoords)
vertexBuffer.position(0)
//加載著色器
val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.vertex_simple_shade, this)
val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
objProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)
objectPositionParam = GLES20.glGetAttribLocation(objProgram, "vPosition")
}
override fun onDrawEye(eye: Eye) {
GLES20.glEnable(GLES20.GL_DEPTH_TEST) //啟用深度測試掉盅,自動隱藏被遮住的材料
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
GLES20.glUseProgram(objProgram)
//啟用頂點位置句柄
GLES20.glEnableVertexAttribArray(objectPositionParam)
//準備坐標數(shù)據(jù)
GLES20.glVertexAttribPointer(objectPositionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)
//繪制物體
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)
//禁用頂點位置句柄
GLES20.glDisableVertexAttribArray(objectPositionParam)
}
可以看到在左右分別繪制了一個三角形,所以說在GvrView中左右其實是兩個區(qū)域以舒,而圖形會在兩個區(qū)域都進行繪制。同時我們也看到上面繪制的圖形是無法變動的慢哈,沒有隨著設(shè)備的移動而變化蔓钟,也就是說只是單純的繪制在屏幕上,并沒有繪制在VR空間中卵贱。下一步我們來看看如果讓它動起來滥沫。
動起來
如果圖形不動侣集,那么就失去了VR的意義,而如何才能讓圖形動起來兰绣?這就用到了我們之前提到的著色器世分,我們知道在頂點著色器中可以對頂點坐標進行轉(zhuǎn)換,正是通過它我們可以實現(xiàn)圖形的各種移動或變形缀辩。
著色器
先來重新創(chuàng)建一個頂點著色器gvr_vertex_shade.glsl
uniform mat4 u_MVP; //外部輸入臭埋,4x4矩陣
attribute vec4 vPosition;
void main() {
gl_Position = u_MVP * vPosition;
}
這里除了定義了一個輸入vPosition,還定義了一個Uniform類型的輸入u_MVP臀玄。這里先簡單說一下Uniform和Attribute瓢阴,在著色器中有三種類型變量
- Uniform:是全局的,在頂點和片段著色器中都可以訪問健无,一般用來表示轉(zhuǎn)換矩陣荣恐、顏色、材質(zhì)等
- Attribute:只在頂點著色器中使用累贤,一般用來表示頂點的一些數(shù)據(jù)叠穆,如坐標、頂點顏色等等臼膏;
- Varying:除它們倆還有一個Varying硼被,它是用來在頂點著色器和片段著色器間傳遞數(shù)據(jù)的,一般在頂點著色器中修改它的值讶请,在片段著色器中使用祷嘶。
所以在我們的代碼里u_MVP就是轉(zhuǎn)換矩陣,通過它與vPosition相乘來得到新的坐標夺溢,這樣我們通過修改u_MVP就可以實現(xiàn)渲染位置的變動论巍。
片段著色器不變,保持原樣即可风响。
初始化
那么接下來就是u_MVP如何得到嘉汰?我們來修改一下GvrDemoActivity的代碼。
首先設(shè)置相機的位置状勤,我們在onNewFrame
中來做這部分操作
override fun onNewFrame(headTransform: HeadTransform?) {
//設(shè)置相機位置
Matrix.setLookAtM(camera, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f)
}
然后在onSurfaceCreated
中做一些初始化的操作鞋怀,如下:
override fun onSurfaceCreated(config: EGLConfig?) {
GLES20.glClearColor(0f,0f,1f,1f)
//初始化頂點坐標緩沖
vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
vertexBuffer.put(triangleCoords)
vertexBuffer.position(0)
//加載著色器
val vertexShaderId = ShaderUtils.compileVertexShader(R.raw.gvr_vertex_shade, this)
val fragmentShaderId = ShaderUtils.compileFragmentShader(R.raw.fragment_simple_shade, this)
objProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId)
//獲取u_MVP這個輸入量
objectModelViewProjectionParam = GLES20.glGetUniformLocation(objProgram, "u_MVP")
objectPositionParam = GLES20.glGetAttribLocation(objProgram, "vPosition")
//初始化modelTarget
Matrix.setIdentityM(modelTarget, 0)
Matrix.translateM(modelTarget, 0,0.0f, 0.0f, -3.0f)
}
在這里主要做以下操作:
- 初始化緩沖
- 加載著色器
- 獲取著色器屬性:這里可以看到通過
glGetUniformLocation
和glGetAttribLocation
分別獲取兩種不同類型的屬性。 - 初始化物體的世界坐標轉(zhuǎn)換矩陣modelTarget
這里來說一下modelTarget持搜,要理解這部分以及下面計算繪制的部分密似,需要你先了解OpenGL中各種坐標系的知識,因為這部分知識也很龐大葫盼,我這里簡單說一下残腌,大家想詳細了解可以自行查詢相關(guān)資料。
坐標系
在OpenGL中存在幾種坐標系:
局部坐標系:物體的局部空間,比如上面我們定義的頂點坐標triangleCoords抛猫,它是物體的本地坐標
世界坐標系:物體在三維空間中的坐標蟆盹。我們需要將物體的各個頂點坐標轉(zhuǎn)換到三維空間的坐標,上面的modelTarget就是這個轉(zhuǎn)換矩陣闺金,將頂點坐標與它相乘就會得到世界坐標系中的位置
視圖坐標系:以觀察點為中心的坐標系逾滥。我們知道觀察的位置不同看到的景象也是不同的,所以需要將世界坐標轉(zhuǎn)換成視圖坐標败匹,這個后面會處理
投影坐標系:以上都是針對三維的頂點坐標進行轉(zhuǎn)換寨昙,但是最終呈現(xiàn)在屏幕上還是一個二維平面,所以需要一個投影過程哎壳,所以需要將視圖坐標轉(zhuǎn)換成投影坐標
屏幕坐標系:最后將投影坐標轉(zhuǎn)成屏幕坐標并顯示出來毅待,這部分我們不需要自己處理。
所以可以看到物體的頂點通過以上4個坐標系(局部坐標系归榕、世界坐標系尸红、視圖坐標系和投影坐標系)和三個變換矩陣,得到了最終坐標才進行繪制刹泄,這部分處理如圖:
計算繪制
簡單復(fù)習了OpenGL的坐標系知識后外里,我來看看最后一步。
最后在onDrawEye
中計算回繪制物體特石,代碼如下:
override fun onDrawEye(eye: Eye) {
GLES20.glEnable(GLES20.GL_DEPTH_TEST)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
//將眼睛的位置變化應(yīng)用到相機
Matrix.multiplyMM(view, 0, eye.eyeView, 0, camera, 0)
val perspective = eye.getPerspective(Z_NEAR, Z_FAR)
Matrix.multiplyMM(modelView, 0, view, 0, modelTarget, 0)
Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, modelView, 0)
//將modelViewProjection輸入頂點著色器(u_MVP)
GLES20.glUseProgram(objProgram)
GLES20.glUniformMatrix4fv(objectModelViewProjectionParam, 1, false, modelViewProjection, 0)
//啟用頂點位置句柄
GLES20.glEnableVertexAttribArray(objectPositionParam)
//準備坐標數(shù)據(jù)
GLES20.glVertexAttribPointer(objectPositionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)
//繪制物體
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)
//禁用頂點位置句柄
GLES20.glDisableVertexAttribArray(objectPositionParam)
}
注意這個函數(shù)有一個參數(shù)eye盅蝗,這里面包含著當前視線的一些信息,比如當我們移動手機的時候姆蘸,我們的視線是變化的墩莫,onDrawEye中的eye中的屬性也是實時變化的。這樣我們通過這種變化來調(diào)整繪制實現(xiàn)場景的移動逞敷。
首先通過相機和眼睛的位置狂秦,得到一個世界坐標系到眼坐標系(視圖坐標系)的轉(zhuǎn)換矩陣view;然后將物體的世界坐標轉(zhuǎn)換成眼坐標modelView推捐;在通過投影矩陣轉(zhuǎn)換成投影坐標modelViewProjection裂问;最后將這個最終的轉(zhuǎn)換矩陣傳入著色器的u_MVP,這樣在著色器中頂點坐標通過這一系列轉(zhuǎn)換就成功的計算成最終坐標牛柒,這樣就可以進行繪制的堪簿。
計算完成后進行繪制即可。
可以看到皮壁,我們在VR世界中成功繪制了一個三角形椭更,隨著手機(視線)的移動景象也有了變化。