從零開始寫一個武俠冒險遊戲-6-用GPU提高性能(1)

從零開始寫一個武俠冒險遊戲-6-用GPU提高性能(1)

概述

咱們以前全部的繪圖工做都是直接使用基本繪圖函數來繪製的, 這樣寫出來的代碼容易理解, 不過這些代碼基本都是由 CPU 來執行的, 沒怎麼發揮出 GPU 的做用, 實際上如今的移動設備都有着功能不弱的 GPU(通常都支持 OpenGL ES 2.0/3.0), 本章的目標就是把咱們遊戲中繪圖相關的大部分工做都轉移到 GPU 上, 這樣既能夠解決咱們代碼目前存在的一些小問題, 同時也會帶來不少額外好處:html

  • 首先是性能能夠獲得很大提高, 咱們如今的幀速是40左右, 主要是雷達圖的實時繪製拖慢了幀速;
  • 方便在地圖類上實現各類功能, 如大地圖的局部顯示, 地圖平滑捲動;
  • 保證地圖上的物體狀態更新後重繪地圖時的效率;
  • 幀動畫每次起步時速度突然加快的問題, 反向移動時角色動做顯示爲倒退, 須要鏡像翻轉;
  • 狀態欄能夠經過 紋理貼圖 來使用各類中文字體(Codea不支持中文字體);
  • 最大的好處是: 能夠經過 shader 來本身編寫各類圖形特效.

Codea 裏使用 GPU 的方法就是用 meshshader 來繪圖, 而 mesh 自己就是一種內置 shader. 還有一個很吸引人的地方就是: 使用 mesh 後續能夠很容易地把咱們的 2D 遊戲改寫爲 3D 遊戲, 這也是咱們這個遊戲的一個嘗試: 玩家能夠自由地在 2D3D 之間轉換.編程

基於以上種種理由, 咱們後續會把遊戲中大部分圖形繪製工做都放到 GPU 上, CPU 只負責處理耗費資源不多的菜單選項等 UI 繪製.框架

本章先簡單介紹一下 Codea 中的 meshshader, 接着按照從易到難的順序, 依次把 幀動畫類, 地圖類狀態類 改寫爲用 GPU 繪製(也就是用 mesh 繪製)函數

這部份內容稍微深刻一些, 須要讀者對 OpenGL ES 2.0 中的座標系統有一點了解, 另外對於着色器語言 shader language 也要有必定了解, 這樣讀起來不會太吃力, 不過沒有這方面背景也沒關係, 多讀幾遍, 上機跑幾遍例程, 再本身胡亂修改修改看看是什麼效果, 這麼折騰一番也差很少會了.性能

由於一方面本章內容稍微難一些, 另外一方面本章的篇幅也比較長, 所以本章將拆分爲兩個或者三個子章節.字體

Codea 中的 mesh + shader 介紹

簡單介紹 mesh

meshCodea 中的一個用來繪圖的類, 用來實現一些高級繪圖, 用法也簡單, 先新建一個 mesh 實例, 接着設置它的各項屬性, 諸如設置頂點 m.vertices, 設置紋理貼圖 m.texture, 設置紋理座標 m.texCoords, 設置着色器 m.shader= shader(...) 等等, 最後就是用它的 draw() 方法來繪製, 若是有觸摸事件須要處理, 那就寫一下它的 touched(touch) 函數, 最簡單例程以下:動畫

function setup()
	m = mesh()	
	mi = m:addRect(x, y, WIDTH/10, HEIGHT/10)
	m.texture = readImage("Documents:catRunning")
	m.shader = shader(shaders["sprites"].vs,shaders["sprites"].fs)
	m:setRectTex(mi, s, t, w,h)
end

function draw()
	m:draw()
end

簡單介紹 shader

shaderOpenGL 中的概念, 咱們在移動設備上使用的 OpenGL 版本是 OpenGL ES 2.0/3.0, shader 是其中的着色器, 用於在管線渲染的兩個環節經過用戶的自定義編程實行人工干預, 這兩個環節一個是 頂點着色-vertex, 一個是 片斷(像素)着色-fragment, 也就是說實際上它就是針對 vertexfragment 的兩段程序, 它的最簡單例程以下:lua

shaders = {
sprites = { vs=[[
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
	vColor = color;
	vTexCoord = texCoord;
	gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

void main()
{
	// 取得像素點的紋理採樣
	lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
	gl_FragColor = col;
}
]]}
}

爲方便調用,咱們把它們寫在兩段字符串中, 而後放到一個表裏..net

Codea 中的 meshshader 能夠到官網查看手冊, 或者看看 Codea App 的內置手冊(中文版), 還算全面.code

大體介紹了 meshshader 以後, 就要開始咱們的改寫工做了, 先從 幀動畫類開始.

用 mesh 改寫幀動畫類

思路

仔細分析以後, 發現用 mesh 去實現幀動畫, 簡直是最合適不過了, 只要充分利用好它的紋理貼圖和紋理座標屬性, 就能夠很方便地從一張大圖上取得一幅幅小圖, 並且動畫顯示速度控制也很好寫, 咱們先用 mesh 建立一個矩形, 把整副幀動畫素材圖做爲它的紋理貼圖, 這樣咱們就能夠經過設置不一樣的紋理座標來取得不一樣的子幀, 並且它的紋理座標的參數特別適合描述子幀: 左下角 x, 左下角 y, 寬度, 高度, 注意, 紋理座標的範圍是 [0,1].

結合具體代碼進行說明

下面看看代碼:

function setup()
	...
	-- 新建一個矩形, 保存它的標識索引 mi
	mi = m:addRect(self.x, self.y,WIDTH/10,HEIGHT/10)
	--	把整副幀動畫素材設置爲紋理貼圖
	m.texture = readImage("Documents:catRunning")
	--	計算出各子幀的紋理座標存入表中
	coords = {{0,3/4,1/2,1/4}, {1/2,3/4,1/2,1/4}, {0,2/4,1/2,1/4}, {1/2,2/4,1/2,1/4}, 
			{0,1/4,1/2,1/4}, {1/2,1/4,1/2,1/4}, {0,0,1/2,1/4}, {1/2,0,1/2,1/4}}
	-- 把第一幅子幀設置爲它的紋理座標
	m:setRectTex(mi, self.coords[1][1], self.coords[1][2] ,self.coords[1][3], self.coords[1][4])
	...
end

由於咱們這幅素材圖分 2 列, 4 行, 共有 8 副子幀, 第一幅子幀在左上角, 因此第一幅子幀對應的紋理座標就是 {0, 3/4, 1/2, 1/4}, 其他以此類推, 咱們把全部子幀的紋理座標按顯示順序依次存放在一個表中, 後續能夠方便地過遞增索引來循環顯示.

先在 setup() 中設置好 timespeed 的值, 接着在 draw() 中能夠經過這段代碼來控制每幀的顯示時間:

function draw()
	...
	-- 若是停留時長超過 speed,則使用下一幀
    if os.clock() - time >= speed then
        i = i + 1
        time = os.clock()
    end
    ...
end

咱們通常用幀動畫來表現玩家控制的角色, 須要移動它的顯示位置, 能夠在 draw() 中用這條語句實現:

-- 根據 x, y 從新設置顯示位置
	m:setRect(mi, x, y, w, h)

目前咱們的代碼須要每副子幀的尺寸同樣大, 若是子幀尺寸不同大的話, 就須要作一個轉換, 咱們決定讓屬性紋理座標表仍然使用真實座標, 新增一個類方法來把它轉換成範圍爲 [0,1] 的表, 以下:

-- 原始輸入爲形如的表:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
       {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}

-- 把絕對座標值轉換爲相對座標值
function convert(coords)
	local w, h = m.texture.width, m.texture.height
	local n = #coords
	for i = 1, n do
		coords[i][1], coords[i][2] = coords[i][1]/w, coords[i][2]/h
		coords[i][3], coords[i][4] = coords[i][3]/w, coords[i][4]/h
	end
end

用 shader 實現鏡像翻轉

如今還有一個問題, 就是當角色先向右移動, 而後改成向左移動時, 角色的臉仍然朝向右邊, 看起來就像是倒着走同樣, 由於咱們的幀動畫素材中橘色就是臉朝右的, 該怎麼辦呢? 有種辦法是作多個方向的幀動畫素材, 好比向左, 向右, 向前, 向後, 這貌似是通用的解決方案, 不過咱們這裏有一種辦法能夠經過 shader 實現左右鏡像翻轉, 而後根據移動方向來決定是否調用翻轉 shader.

由於咱們只是左右翻轉, 能夠這樣想象: 在圖像中心垂直畫一條中線, 把中線左邊的點翻到中線右邊, 把中線右邊的點翻到中線左邊, 也就是每一個點只改變它的 x 值, 假設一個點原來的座標爲 (x, y), 翻轉後它的座標就變成了 (1.0-x, y), 注意, 此處由於是紋理座標, 因此該點座標範圍仍然是 [0,1], 此次變化只涉及頂點, 因此咱們只須要修改 vertex shader, 代碼以下:

void main()
{
	vColor = color;
	// vTexCoord = texCoord;
	vTexCoord = vec2(1.0-texCoord.x, texCoord.y);
	gl_Position = modelViewProjection * position;
}

不過這樣處理在每一個子幀的尺寸有差別時會出現顯示上的問題, 由於咱們的紋理座標是手工計算出來的, 它所肯定的子幀不是嚴格對稱的, 解決辦法就是給出一個精確左右對稱的紋理座標, 這樣弄起來也挺麻煩, 其實最簡單的解決辦法是把素材處理一下, 讓每副子幀的尺寸相同就行了.

用 shader 去掉素材白色背景

在使用 runner 素材時, 由於它的背景是白色, 須要處理成透明, 以前咱們專門寫了一個函數 Sprites:deal() 預先對圖像作了處理, 如今咱們換一種方式, 直接在 shader 裏處理, 也很簡單, 就是在用取樣函數獲得當前像素的顏色時, 看看它是否是白色,如果則使用 shader 內置函數 discard 將其丟棄, 注意, 這裏的顏色值必須寫成帶小數點的形式, 由於它是一個浮點類型, 對應的 fragment shader 代碼以下:

// 定義一個用於比較的最小 alpha 值, 由用戶自行控制
uniform vec4 maxWhite;

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    
    if ( col.r > maxWhite.x &&  col.g > maxWhite.y && col.b > maxWhite.z) 
    	discard;
    else	    
    	gl_FragColor = col;
}

試着執行一下, 發現效果還不錯.

發現還有個小問題, 就是修改了 self.wself.h 後, 顯示的區域出現了錯誤, 看了代碼, 須要在 Sprites:init() 中修改一下, 修改前爲:

self.mi = self.m:addRect(self.x, self.y, self.w, self.h)

修改後爲:

self.mi = self.m:addRect(self.x, self.y, w, h)

完整代碼

寫成類的完整代碼以下:

-- c06.lua

--# Shaders
-- 用 mesh/shader 實現幀動畫,把運算量轉移到 GPU 上,可用 shader 實現各類特殊效果
Sprites = class()

function Sprites:init()
    self.m = mesh()
    self.m.texture  = readImage("Documents:catRunning")
    self.m.shader = shader(shaders["sprites"].vs,shaders["sprites"].fs)
    self.coords = {{0,3/4,1/2,1/4}, {1/2,3/4,1/2,1/4}, {0,2/4,1/2,1/4}, {1/2,2/4,1/2,1/4}, 
                    {0,1/4,1/2,1/4}, {1/2,1/4,1/2,1/4}, {0,0,1/2,1/4}, {1/2,0,1/2,1/4}}
    self.i = 1
    
    local w,h = self.m.texture.width, self.m.texture.height
    local ws,hs = WIDTH/w, HEIGHT/h
    self.x, self.y = w/2, h/2
    self.w, self.h = WIDTH/10, HEIGHT/10
    self.mi = self.m:addRect(self.x, self.y, self.w, self.h)
    self.speed = 1/30
    self.time = os.clock()
end

function Sprites:convert()
	local w, h = self.m.texture.width, self.m.texture.height
	local n = #self.coords
	for i = 1, n do
		self.coords[i][1], self.coords[i][2] = self.coords[i][1]/w, self.coords[i][2]/h
		self.coords[i][3], self.coords[i][4] = self.coords[i][3]/w, self.coords[i][4]/h
	end
end

function Sprites:draw()
    -- 依次改變貼圖座標,取得不一樣的子幀
    self.m:setRectTex(self.mi, 
    				  self.coords[(self.i-1)%8+1][1], self.coords[(self.i-1)%8+1][2], 
    				  self.coords[(self.i-1)%8+1][3], self.coords[(self.i-1)%8+1][4])
    -- 根據 self.x, self.y 從新設置顯示位置
    self.m:setRect(self.mi, self.x, self.y, self.w, self.h)
    -- 若是停留時長超過 self.speed,則使用下一幀
    if os.clock() - self.time >= self.speed then
        self.i = self.i + 1
        self.time = os.clock()
    end
    
    self.m:draw()
end

function Sprites:touched(touch)
    self.x, self.y = touch.x, touch.y
end


-- 遊戲主程序框架
function setup()
    displayMode(OVERLAY)
    
    -- 幀動畫素材1
    img1 = readImage("Documents:runner")
    pos1 = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120},
			{320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
       
	-- 幀動畫素材2       
	img2 = readImage("Documents:catRunning")
    local w,h = 1024,1024
    pos2 = {{0,h*3/4,w/2,h/4},{w/2,h*3/4,w/2,h/4},{0,h*2/4,w/2,h/4},{0,h*2/4,w/2,h/4},
            {0,h*1/4,w/2,h/4},{w/2,h*1/4,w/2,h/4},{0,h*0/4,w/2,h/4},{0,h*0/4,w/2,h/4}}
      
	-- 開始初始化幀動畫類            
    myS = Sprites()
    myS.m.texture = img1
    myS.coords = pos1
    -- 若紋理座標爲絕對數值, 而非相對數值(即範圍在[0,1]之間), 則需將其顯式轉換爲相對數值
    myS:convert()
    
    -- 使用自定義 shader
    myS.m.shader = shader(shaders["sprites1"].vs,shaders["sprites1"].fs)
    -- 設置 maxWhite
    myS.m.shader.maxWhite = 0.8
    

    -- 設置速度
    myS.speed = 1/20
    myS.x = 500
end

function draw()
    background(39, 31, 31, 255)
    -- 繪製 mesh
    myS:draw()
    sysInfo()
end

function touched(touch)
    myS:touched(touch)
end

-- 系統信息
function sysInfo()
    -- 顯示FPS和內存使用狀況
    pushStyle()
    --fill(0,0,0,105)
    -- rect(650,740,220,30)
    fill(255, 255, 255, 255)
    -- 根據 DeltaTime 計算 fps, 根據 collectgarbage("count") 計算內存佔用
    local fps = math.floor(1/DeltaTime)
    local mem = math.floor(collectgarbage("count"))
    text("FPS: "..fps.."    Mem:"..mem.." KB",650,740)
    popStyle()
end


-- Shader
shaders = {

sprites = { vs=[[
// 左右翻轉着色器
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
    vColor = color;
    // vTexCoord = texCoord;
    vTexCoord = vec2(1.0-texCoord.x, texCoord.y);
    gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    gl_FragColor = col;
}
]]},


sprites1 = { vs=[[
// 把白色背景轉換爲透明着色器
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

// 定義一個用於比較的最小 alpha 值, 由用戶自行控制
uniform float maxWhite;

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    
    if ( col.r > maxWhite.x &&  col.g > maxWhite.y && col.b > maxWhite.z)  
    	discard;
    else	    
    	gl_FragColor = col;
}
]]}
}

回頭來看, 就發現用 mesh 改寫後的幀動畫類既簡單又高效. 並且有了 shader 這個大殺器, 咱們能夠很是方便地爲角色添加各類特效, 上面用過的 鏡像去素材白色背景 就是兩種比較簡單的特效, 咱們在下面介紹幾種其餘特效.

在幀動畫角色上用 shader 增長特效

角色灰化

一些遊戲, 好比 魔獸世界, 在玩家控制的角色死亡時, 會進入靈魂狀態, 這時全部的畫面所有變爲灰色, 咱們也能夠在這裏寫一段 shader 來實現這個效果, 不過咱們打算稍做修改, 只把玩家角色變爲灰色, 屏幕上的其他部分都保持原色.

先寫一個從彩色到灰度的轉換函數, 這個函數要在 fragment shader 中使用:

float intensity(vec4 col) {
    // 計算像素點的灰度值
    return 0.3*col.x + 0.59*col.y + 0.11*col.z;
}

而後修改片斷着色代碼:

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    col.rgb = vec3(intensity(col));
    gl_FragColor = col;
}

若是咱們但願在灰化的同時實現虛化, 也就是讓角色變淡, 能夠連 alpha 一塊兒修改, 這種淡化特效能夠用於角色使用了隱匿技能後的顯示, 代碼以下:

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    col.rgba = vec4(intensity(col));
    gl_FragColor = col;
}

效果很不錯, 徹底達到了咱們的預約目標.

中毒狀態

不少遊戲中, 角色若是中毒了, 會在兩個地方顯示出來, 一個是狀態欄, 一個是角色自己, 好比 仙劍奇俠傳 中會給角色渲染一層深綠色, 咱們用 shader 實現的話, 只須要把取樣獲得的像素點顏色乘以一個指定的顏色值(綠色或其餘), 該指定顏色可隨時間變化而變深, 也能夠由於吃了解毒藥而逐漸變淺(在咱們的設定裏不存在一吃藥就變好的狀況, 只能慢慢好), 這部分處理能夠充分利用 mesh 的一個方法 setRectColor() 來實現, 代碼以下:

function setup()
	...
	myS.m:setRectColor(myS.mi, 0, 255,0,255)
	...

shader 中只須要把取樣點的顏色跟該顏色vColor相乘便可, 咱們的模板代碼就是這樣的:

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    gl_FragColor = col;

因此咱們只須要在 setup() 中設置一下, 而後調用名爲 spritesshader 便可.

效果完美, 後面能夠根據遊戲須要再加一個根據時間流逝綠色變淡或者變深的處理.

角色的其餘狀態, 例如受傷出血也能夠經過相似的方法實現(把 vColor 改成紅色便可), 可自行試驗.

角色光粒子化

實際上, 咱們上面實現的幾種特效都是比較簡單的, 最後咱們來一個複雜點的, 角色昇華, 變成光粒子消散在空中, 固然這種特效也能夠放在 NPC 身上, 代碼以下:

---後續補充

本章用到的 shader 代碼

下面列出咱們在這裏用於實行各類特性的 shader 代碼:

-- Shader
shaders = {

sprites = { vs=[[
// 左右翻轉着色器
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
    vColor = color;
    // vTexCoord = texCoord;
    vTexCoord = vec2(1.0-texCoord.x, texCoord.y);
    gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    gl_FragColor = col;
}
]]},


sprites1 = { vs=[[
// 把白色背景轉換爲透明着色器
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

// 定義一個用於比較的最小 alpha 值, 由用戶自行控制
uniform vec4 maxWhite;

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    
    if ( col.r > maxWhite.x &&  col.g > maxWhite.y && col.b > maxWhite.z) 
    	discard;
    else	    
    	gl_FragColor = col;
}
]]},


sprites2 = { vs=[[
// 局部變灰
//--------vertex shader---------
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying vec2 vTexCoord;
varying vec4 vColor;

uniform mat4 modelViewProjection;

void main()
{
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],
fs=[[
//---------Fragment shader------------
//Default precision qualifier
precision highp float;

varying vec2 vTexCoord;
varying vec4 vColor;

// 紋理貼圖
uniform sampler2D texture;

float intensity(vec4 col) {
    // 計算像素點的灰度值
    return 0.3*col.x + 0.59*col.y + 0.11*col.z;
}

void main()
{
    // 取得像素點的紋理採樣
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    col.rgba = vec4(intensity(col));
    gl_FragColor = col;
}
]]}
}

本章小結

使用 mesh繪圖時, 能夠選擇不加載 shader, 若是須要自定義修改圖像中的某些顯示效果, 就要選擇加載 shader 了.

關於幀動畫類的 GPU 改造暫時就寫這麼多, 下一節準備說說如何用 mesh 來改寫地圖類.

全部章節連接

從零開始寫一個武俠冒險遊戲-1-狀態原型
從零開始寫一個武俠冒險遊戲-2-幀動畫
從零開始寫一個武俠冒險遊戲-3-地圖生成
從零開始寫一個武俠冒險遊戲-4-第一次整合
從零開始寫一個武俠冒險遊戲-5-使用協程
從零開始寫一個武俠冒險遊戲-6-用GPU提高性能(1)

相關文章
相關標籤/搜索