又到了眾所期待的 Ren'Py 節目時間。大家好,我是節目主持人雪凡。
「我是絲蔻兒。」
「我是音符呀。」
啊哈,兩位熟悉的搭檔再次登場,在下可是無任歡迎的呢。
不過,本回的內容......也算是上回的擴展與補充。程式碼可是有相當份量的說。看到程式碼就該咚咚登場的泰克斯 (text),人又到哪去了?
「那傢伙狀況挺糟的,壓根就沒出門啊。」
「呀......那、那個......小泰他,好像是在煩惱些什麼......我們有點不好叫他......」
哈啊?
「嘛啊~也不知怎麼搞的...... 自從上次上完節目,就看他一臉失魂落魄的樣子,嘴裏總在喃喃自語:『我絕對不可能和絲蔻兒越來越像』什麼的......完全不知道在想什麼呢!」
「我不可能和絲蔻兒越來越像,我不可能和絲蔻兒越來越像......」
「大、大概就像這樣......」
......
呃,好,咱大概懂了,這真是一椿悲哀的意外。不過這個話題和我無關,完全無關,就此跳過吧。
今天要帶給大家的主題有兩大項。
首先擋在面前的是「粒子系統」。在這個主題中,咱們會聊到如何使用大量的小圖片來創造動畫。至於第二個,則是動態的圖片修改器 "Image Manipulators"。
前者能讓您大量創造像是「雨水落下」一類的畫面效果。至於後者,則能讓遊戲無需預存圖片的全部版本--只需保存少少幾張圖,與之相關的衍生版本(如一張背景圖可能有多種打光變化)就能透過即時算圖自動搞定--這不光是節約儲存空間,也可減少您需要管理的圖片總量,是相當方便的功能。
「雖然程式碼確實不少,但本回的內容遠遠沒有上回那麼繁瑣。請不要被上回嚇倒倒了。」
「那麼,這就開始吧!」
走囉!
照慣例,最難的東西放在最前面--今天就從粒子系統開始。
要讓畫面上出現一大堆,具有隨機動畫的相似小圖片(比方說水花、雨點、雪片、花瓣等等),一張張貼圖、手工寫 ATL 做動畫這種事,多半不是個好主意。畢竟結局我已經見到了:「少女(年)伏在桌上,臉上帶著一絲笑容,彷彿睡著了那般,靜靜地陷入了永眠」--世界線變動率 怎樣也無法超過一啊!
一言以蔽之,會累死的。
雖然說過勞死也是遊戲製作者的本職學能,不過如此一來,會給社會版記者造成很大的困擾,還請不要那麼做。
為了減少社會版記者......不對,應該說各位遊戲作者的麻煩,Ren'Py 提供了相當不錯用的 Sprite 系統。
如前所述,Sprite 系統能在畫面上隨機顯示很多小圖片,並附上相應的移動特效......像雪花或櫻花紛紛飄落這種效果,用本系統就能輕易做到。
▲ 圖:官網對粒子效果的示範之一--讓多片櫻花瓣從邊緣的隨機位置,以隨機的速度與方向飄落。以上這種簡單的粒子效果,用 SnowBlossom() 函式就能創造出來;稍後會有示範的,請別著急。另一方面,圖上標出的 Sprite 就是 Ren'Py 粒子系統中的「粒子」了。
【Sprite 與 Particle 的用語解說】 說到這裡,我已經聽到有熟悉遊戲引擎設計的同學在慘叫了。
--「不對......不對不對不對!"Sprite" 再怎麼翻譯也不會變成『粒子』,粒子系統應該是 Particle System 才對。Sprite 系統明明是別的東西。這兩者在遊戲引擎的世界中是完全不一樣的呀!」
啊啊,非常遺憾,您的說法完全正確。在一般的遊戲引擎中,Sprite 確實是被翻成「精靈系統」而非「粒子系統」,而其功能也和我們這邊要講的粒子系統完全不同。
但我也沒說錯!Ren'Py 引擎中,就是將粒子系統命名為 Sprite,我也沒辦法啊!這都是世界的錯!
音符:「不,世界是無辜的......」
咳!誰的錯先不管。那麼在一般的遊戲引擎中,Sprite 系統(精靈系統)又是指什麼呢?
一般來說,Sprite 是指一個「2D 圖片的容器」。
這個容器具有自己的長寬大小、具有在螢幕上的座標位置、甚至具有自己的旋轉角度、透明度等等。但是這個容器實際顯示些什麼,卻不是由容器自己決定,而是由容器中裝入的圖片來決定。
......聽起來似乎很熟悉?之前好像用過這個概念?
是的!之前我們操控立繪時,Sprite 的概念確實早就已經包含於其中了。
以下是一段連續的圖片操作。您可以試試看,看看自己認不認得出來,一般遊戲引擎中所謂的 Sprite 究竟是哪一部份?
show elminster smile
show elminster at right
show elminster at left
show elminster:
alpha 0.5
show elminster confuse
show elminster at center with ease
......答案是 "elminster" 這個標籤 (tag)。之前我們將其稱之為「圖片名稱中的第一字節」,不過(僅管 Ren'Py 官方沒有這麼稱呼),事實上它就是其他遊戲引擎中會見到的那種 Sprite。
注意:前面的解說乃是針對一般遊戲引擎的補充,而非針對 Ren'Py 的。如果您感到非常困惑甚至開始混淆,那麼請把這部份的內容忘光光。這對後續的說明沒有任何影響。
知道粒子系統是什麼後,就從最簡單的開始吧。
如果您只是想要櫻花、雪花紛紛飄落的那類效果,您可以直接使用「SnowBlossom()」函式來創造。如下:
image snowflakes = SnowBlossom("snowflake.png") # 定義雪片飄飄落下的效果 # 注意:這其實也是一張圖片 (image)。 label start: scene black # 黑底 show snowflakes # 顯示飄落效果
▲ 圖:上例中的 snowflake.png 圖片,每一片飄下來的雪片都長這樣。如果您希望粒子系統中包含多種 snowflake,可以用 ATL 的 choice,或多個 contains 搭配多個 SnowBlossom() 試試。請見後續的範例。
▲ 圖:螢幕效果示範。有部份雪片破碎是因為截圖時沒有垂直同步的關係,並非真正顯示成那樣。
非常簡單吧?
上例中的 "snowflake.png" 只是一張簡單的雪結晶圖片,不過您也可以將其設為任意 Displayable。
因為其他參數都沒設定,所以保持著預設值。比方說,雪花在螢幕上的片數就被預設為 10 片,而顯示效果則是隨機往下飄,且會模擬微風從左往右,輕輕吹拂的感覺。
SnowBlossom() 中可變更的重要參數包括......
SnowBlossom() 還有些別的參數,不過個人覺得那些參數可有可無,無需理會。完美主義者請見官網說明頁,自行對照參考。
以下是一個用 SnowBlossom() 創造的「灰塵隨機飄散」效果。--咱這邊還一併用上了第六章提過的 ATL,請務必參考看看。
# 定義大小灰塵,共四種大小。 image mmsprite middle: "dust.png" zoom 0.75 image mmsprite small: "dust.png" zoom 0.5 image mmsprite vsmall: "dust.png" zoom 0.25 image mmsprite vvsmall: "dust.png" zoom 0.1 # 漂浮的灰塵效果。這是一個 ATL 區塊, image dust: # 本區塊內有四個獨立的 SnowBlossom,我把它們打包成一個 dust。 # xspeed 有正有負,表示灰塵可往左或往右飄;而 yspeed 亦同。這裡的速度想當然爾不能設太大,不然就不像是灰塵了…… # fast 表示剛 show 圖時,螢幕上就會立刻堆滿灰塵。否則,灰塵會慢條斯理(真的很慢)地從畫面最邊緣慢慢湧現出來。 contains: # 最小(遠)的灰塵數量最多,count 有 30。 SnowBlossom("mmsprite vvsmall", count=30, xspeed = (-20, 20), yspeed = (-20, 20), fast = True) contains: SnowBlossom("mmsprite vsmall", count=20, xspeed = (-20, 20), yspeed = (-20, 20), fast = True) contains: SnowBlossom("mmsprite small", count=10, xspeed = (-20, 20), yspeed = (-20, 20), fast = True) contains: # 最大(近)的灰塵數量最少,count 只有 5。 SnowBlossom("mmsprite middle", count=5, xspeed = (-20, 20), yspeed = (-20, 20), fast = True) # 使用方法一樣 label start: scene background # 顯示背景 show dust # 顯示剛剛定義好的灰塵 with fade
看到了嗎!ATL 可是萬用大殺器啊!就算搭配粒子系統也能運作得很好。最好去習慣使用它,這樣才不會吃虧。
「ATL 是第六回的主題,忘記內容或嫌太難而跳過的笨蛋們,請一定要回去複習唷!」
以上發言不代表本台立場!絲蔻兒......光裝可愛是不夠的,控制一下妳的毒舌,算我求妳......
如果您需要一些連 SnowBlossom() 也不能滿足的超特狂暴粒子效果,那就需要用 SpriteManager 物件寫 Python 程式碼了。
這部份扯到物件,程式碼不少,對初接觸物件的人可能稍微有點難度。如果真的看不懂,各位暫時跳過也沒問題。願意挑戰的同學,這就請聽在下的解說吧。坐坐坐。
【物件 (object)】 前面提到「SpriteManager 物件」,那麼,這邊所說的「物件」到底是什麼呢?一言以蔽之,粒子系統是由「Sprite」與「SpriteManager」兩種類別構成的。
粒子系統是以 SpriteManager 為基本單位來使用的。個別的 Sprite,會由 SpriteManager 透過 SpriteManager 物件內部的 .create(displayable) 方法建立。每建好一個 Sprite 物件,我們都該設定好那個 sprite 物件的初始位置(sprite.x 和 sprite.y)。之後再將 SpriteManager 創造出的 Sprite 們,全部儲存到一個列表 (list) 中,如此一來,日後就可以利用各位自己親手寫的「更新函式」,對列表中的所有 Sprite,進行位置變更動作。
總之,建立一個自訂的 Sprite 系統,大略流程如下:
--很有點複雜對吧?
以上內容,第一次看看不懂是很正常的。請對照下面的特大號範例一起看。
「官網的範例」中,直接使用了這一整套流程,搞得腳本之中散滿亂七八糟的程式碼,烏煙瘴氣。不過在官方 tutorial 示範遊戲中,卻有把這些流程封裝在物件裡,因此程式本身就好看且方便很多、而資源管理也容易了不少。我個人強烈推薦使用物件模式,因此下方的範例,也是以 tutorial 中的程式碼為範本來介紹(註解當然是我寫的)。
為了理解方便,強烈建議各位在繼續往下看之前,先去執行一下官方 tutorial 範例遊戲,看看以下這坨東西到底會產生怎樣的結果--只要在 tutorial 的選單中,選最下方的 "Sprites" 選項就能看到。
見識過效果了嗎?那我們來看看程式怎麼寫吧......請深呼吸。這是本回最難的範例了。
init python: # 建立一個名叫 StarField 的新類別。 # 最前面的 class 是新定義一個類別時,必定要用的關鍵字,不能改動。後面的 (object) 另有意義但暫時說不清,總之也不要去變更它。 class StarField(object): # 以下是 StarField 這個類別內部包含的東西的定義 # 一言以蔽之,總共包含三個函式:__init__()、add()、update()。 # 先定義 __init__() 函式。__init__() 函式決定了,透過本類別新建一個物件時,到底要怎麼做初始化設定的。 # 引數中的 self,是在表示日後用本類別創造出來的物件自身。(註:在 Python 類別中定義的函式,第一個引數大多是 self。透過物件使用時本函數時也不用特意傳入這個引數,Python 會自動幫您連結好。) def __init__(self): # 我們要在 __init__() 中初始化 SpriteManager 與 spriteList,並把 spriteList 填滿 self.sm = SpriteManager(update=self.update) # 初始化 SpriteManager,並命名為 self.sm(也就是 self 物件的子物件 sm)。 # self.update 是什麼?其實就是本類別內部定義的三個函式之一的 update 函式。之後會有定義的。不說這個,比起來,您稍微感覺出 self 的用法了嗎? self.stars = [ ] # 這就是前述的 spriteList。名字差異不重要啦。 # 目前是空的,我們等會得填滿他。 # 開始填入 spriteList,有好幾種不同的 displayable d = Transform("star.png", zoom=.02) # 第一種 displayable,主要只是改變大小…… # 這邊的 Transform() 做的事情,其實和我們前面(第六回)花了大篇幅講過的 ATL 沒有不同。只是 python block 中不能用 ATL,所以才用這種寫法。 # Transform() 的使用說明見此:https://www.renpy.org/doc/html/trans_trans_python.html#Transform for i in range(0, 50): # 用 for 迴圈重複填入五十個相同的 displayable 到 spriteList 中。 # 下面的 add() 是輔助程式。工作只是呼叫 self.sm.create(),並將產生出的 Sprite 的位置初始化後,放入 self.stars。您可以先往下找來看看。 self.add(d, 20) # 20 是速度,模擬愈遠方(小)的星星移動的愈慢 # 繼續變更 displayable,設成其他大小…… d = Transform("star.png", zoom=.025) # 星星的大小變大了 for i in range(0, 25): self.add(d, 80) # 比較大的星星,移動速度也比較快了。 # 下面以此類推…… d = Transform("star.png", zoom=.05) for i in range(0, 25): self.add(d, 160) d = Transform("star.png", zoom=.075) for i in range(0, 25): self.add(d, 320) d = Transform("star.png", zoom=.1) for i in range(0, 25): self.add(d, 640) d = Transform("star.png", zoom=.125) for i in range(0, 25): self.add(d, 1280) # 所有的 sprite 都建立完成,以後不會再呼叫 self.add() 與 self.sm.create() 了。 # 定義 add():用來加入並初始化粒子的助手函式 def add(self, d, speed): s = self.sm.create(d) # 透過 self.sm.create 產生 sprite。sprite 一定要透過 sm (SpriteManager) 的 .create() 來產生。 start = renpy.random.randint(0, 840) # 在顯示範圍寬度的區域內,隨機生成 x 軸起點 s.y = renpy.random.randint(0, 600) # 在螢幕高度的範圍內,隨機生成 y 值 self.stars.append((s, start, speed)) # 將 sprite (連同更新時所需要的附屬資料,如移動速度一起)塞入 spriteList(本例中就是 self.stars)中。 # 由此可知 sprite 中每個元素的格式都為一個三元元組,其中記錄了三份資料:(用來顯示的 sprite 本體, x 軸起點, x 軸平移速度) # 定義 update():更新位置用的函式 def update(self, st): for s, start, speed in self.stars: # 用 for 迴圈處理整個 self.stars (spriteList)。self,stars 中,每個元素又可被拆開為三個部份(見 add() 中的說明) # 算法:x 新值 = (x 初值 + 速度 * 經過總時間) 取顯示範圍寬度的餘數 - 20 s.x = (start + speed * st) % 840 - 20 return 0 # 不等待,以最快速度進行更新 # StarField 類別定義到此結束,接下來是呼叫示範 label start: scene black # 只是設為黑底。記得嗎?我們剛剛定義的 StarField 沒有定義任何底色。 show expression (StarField().sm) as starfield # 靠這句顯示粒子效果 # ======== 上一句的說明 ========== # 上面是一個普通的 show X as Y 語句。不過 X == expression (StarField().sm) # expression (...) 表示括弧中的內容是 python 語句--要 show 的東西不用 renpy 腳本語法,而是直接用 python 語句表示。 # 而 StarField().sm 這個 python 語句,意思是用 StarField 類別建立一個全新的 StarField 物件(之前辛苦定義的 __init__() 會在此時執行完畢),並用 .sm 標示新的 StarField 物件內部的 sm (SpriteManeger 子物件)--這個 sm 很重要,因為它就是 show 要顯示的目標物。 # 總之,要顯示的目標是 SpriteManager 物件,而非 StarField 物件。請注意這個差別。 # 最後的 as starfield,則是用在事後消除上,最好也給一下。 # ======== 上一句的說明 ========== with wipeleft # 同時套用到上面兩行的轉場特效。 "StarField 顯示中……" hide starfield # 將 starfield 消掉
瘋狂吧。至少我的手快給打斷了。
不管怎麼說,到此您應該能順利自訂 Sprite 系統了。
SpriteManager() 還有些其他參數可以輸入:比方說創建時可以輸入一個 event function,每當 event 發生就會被呼叫......另外也可以自定義 width 與 height 屬性,讓 SpriteManager 只包括有限的長寬而非全螢幕等等,諸如此類。詳情請見 SpriteManager 的說明。至於 event 的使用,則請參考官網範例,其中有用到這個功能,不過我們這邊不講。我看到有些同學頭上已經在冒煙了......
夠啦,Sprite 系統實在太殘暴了!這根本就是在踐踏我們對遊戲的愛!身為主持人我也快受不了了,快來點簡單點的......
「那、那個,簡單是相對而言啦......」
總之請看!
雖說「修改器 (manipulator)」這個名字聽來讓人有些困惑,不過從技術上說,Image Manipulator (以下簡稱 IM)只是 Ren'Py 的世界中,眾多 Displayable 的一種。您可以在所有可使用 Displayable 的地方利用它,就和利用普通的 Displayable 一樣。
忘記 Displayable 是什麼的同學,請參看第六回。
它的最大特徵在於:當它生成時,需要以一張圖片 (Image),或一張現有的 IM--做為原料。
注意:一定要是「圖片或 IM」,才能做為 IM 的原料。如果您企圖用其他類型的 Displayable--如 Solid--來產生 IM,Ren'Py 也會用華麗的錯誤畫面來糾正您的。
喔對了,因為 IM 可接受 IM 做為原料,故您也可以將多個 IM 嵌套使用。
如同它的名字,「圖片修改器 (IM)」可讓一張圖片,透過各種各樣的數學運算,被修改成另一張不同的圖片。比方說,裁切出一部份、變更色調、翻轉、組裝多張圖片等等。而這個被修改過的新圖片,可以用在任何「可擺放圖片」的地方。
IM 做出的東西,每一份都是完整的圖片,因此,任何一個功能比較完善的圖片編輯器(如 Gimp 那種),都能替代它,做出同樣甚至更好的效果。而且因為它的本質是「圖片」,所以 IM 也不支援插值動畫,具體說來,您不能併用 ATL 與 IM 來平滑地過渡色相環,只能喀地一下切換過去。
因為上述種種限制存在,所以 IM 不像前回的 ATL 那樣激動人心、不可取代。
它只是一個補充,用在「有很多相似但不同的圖片」要顯示,但又不想實際將這些圖片保存在硬碟之中時。
那麼以下,依照咱對這些 IM 的理解,為各位介紹各種可能用的上的 IM......
輸入兩張圖片 "base" 與 "mask",用 mask 來調整 base 的透明度。最後顯示出的圖片是經過透明度調整後的 base 圖。
調整的方法是:用 mask(遮罩圖片)的「紅色 channel」來取代 base(內容圖片)的 alpha channel。
【alpha channel】 就是透明度通道的意思。
alpha channel 的數值越大(越接近百分之百),則越「不」透明。
舉例來說,如果 mask 圖片中塗滿紅色,表示 base 圖片全不透明(會完全顯示出來)。
用法如下:
image memory = "bg/memory.png" # 回憶畫面 image spot light center = "effect/red_center.png" # 中間紅周圍白的圖片 image memory fuzzy = im.AlphaMask(base = "memory", mask = "spot light center") # 周圍消去的回憶畫面 label start: scene black with dissolve show memory fuzzy with dissolve "這是去年夏天,在那個滿是蟬鳴的小水塘邊發生的故事......"
▲ 圖:memory.jpg 示意圖。本圖做為 base 使用。
「......這傢伙是誰啊?」
只是我的好姬友......喂喂!不要打那種無謂的岔!
▲ 圖:red_center.jpg 示意圖。本圖做為 mask 使用。需特別留意「不顯示的地方要設為黑色(而不能設為白色)」。您可以想想為什麼。
▲ 圖:以黑色為底色時的顯示成果。
將多張圖片合成一張。
這有什麼用?官網提供的範例是有趣個的解答,快看下面!
image girl clothed happy = im.Composite( (300, 600), # 合併後的圖片大小為 300 x 600 (0, 0), "girl_body.png", # 第一張圖(底圖):少女身體圖片,放置位置(左上角位置)為 (0,0) (0, 0), "girl_clothes.png", # 第二張圖:衣服 (100, 100), "girl_happy.png" # 第三張圖:表情 )
......這不就是紙娃娃系統嗎!
有沒有燃燒起來的感覺啊?我第一次看到時可是很興奮的。
事實上,若不考慮記憶體消耗,第六回的 Fixed 也可以做到同樣效果。然而 Fixed 所用的介面比 im.Composite() 要抽象,此時直接用 im.Composite() 會更方便一些。
輸入一張圖片,輸出一張去飽和度後的,黑白版本的圖片。
image world normal = "world.png" # 普通圖片 image world gray = im.Grayscale("world.png") # 黑白圖片
▲ 圖:這是之前第三回時就用過的 library.jpg 圖片,由 Uncle Mugen 原作,我稍加修改後製。接下來就以這張圖作為範本嚐試改變顏色。
▲ 圖:im.Grayscale() 的效果。
「原來最近的泰克斯也做了 Grayscale 處理呀。」
「那個嗎,我覺得好像不是......」
跳過,快跳過這個話題啦!
請繼續看。
輸入一張圖片,輸出一張有舊照片效果的圖片。
image world normal = "world.png" # 普通圖片 image world Sepia = im.Sepia("world.png") # 泛黃圖片
▲ 圖:im.Sepia() 的效果。
什麼,您嫌 im.Sepia() 還不夠黃?太黑了?
請先等等,在後面,咱會提供一些進階的調整辦法......
官方還有提供一些像是 im.Crop()、im.FactorScale()、im.Scale()、im.Flip()、im.Tile() 等的 IM。
關於以上這些東西,個人覺得沒啥使用價值,但還是稍微介紹一下好了。
以上的 IM 操作,大多用 ATL 等其他方法,也能達到相似的效果。因此沒有勉強使用的必要。
不過,儘管效果雷同,底層的實作方式卻略有差異。IM 一律是「預先」產生一張完整的修改版本圖片,ATL 則更多是臨時性的修改--這些差異,可能會反映在遊戲的執行效率與記憶體消耗上……總之如果用 ATL 的執行效率明顯不夠好時,您還是可以用以上這些 IM 試試,看能否得到更好的效果。
【更多的 IM】 如果您完整看過官方的 tutorial 遊戲,您可能會注意到,Ren'Py 中其實還有「更多的 IM」可用。只是咱這邊沒講。那些零零碎碎的 IM 沒被寫入 Ren'Py 的新版官方手冊之中,顯然已經不再被官方繼續推荐使用,而且那些功能,也大多都可以被下面所要介紹的 im.MatrixColor() 所取代。
如果您真有興趣,想找它們的資料,請用 "im" 關鍵字去搜索官網 wiki,會發現不少的。
本節的重頭戲要來了,請深呼吸。
前面提過 im.Sepia() 和 im.Grayscale() 可以把圖片變成舊照片與黑白圖片--那麼,有沒有可能將圖片改成其他顏色呢?
答案是可以的。
您可以透過矩陣,將圖片中的某些顏色加深,或平移色相,或濾去顏色,或增加亮度......幾乎無所不能!
im.MatrixColor 所用的顏色轉換矩陣(由我們這些遊戲作者給出),格式應該要像下面這樣:
[ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t ]
它在圖片顏色轉換的意義上,是被這樣表述的:
R' = (a * R) + (b * G) + (c * B) + (d * A) + e G' = (f * R) + (g * G) + (h * B) + (i * A) + j B' = (k * R) + (l * G) + (m * B) + (n * A) + o A' = (p * R) + (q * G) + (r * B) + (s * A) + t
這個方程式是什麼意思呢?
事實上,以上的方程式,是用來對「圖片上的每一個單獨像素」做運算用的--假設有一張 800 x 600 大小的圖片,圖片上就有 800 x 600 = 480000 個像素點,要分別做 480000 次上面這組計算。
【註】 音符:「每個像素都有自己原本的 RGBA 顏色值。如紫羅蘭色的像素點,其 RGBA 為 (0.93, 0.51, 0.93, 1.0)、而橄欖色的像素點為 (0.5, 0.5, 1.0, 1.0) 等等。」
我們先將其分開,單抽第一行 R'(新的紅色強度產生公式)來看......
R' = (a * R) + (b * G) + (c * B) + (d * A) + e 新的紅色強度 = (a * 原紅色強度) + (b * 原綠色強度) + (c * 原藍色強度) + (d * 原不透明度強度) + e
很明顯地,新紅色的最終強度,受到舊顏色的四條通道與一個常數 (e) 的共同影響。
如果希望新舊紅色強度不改變(新紅色強度 = 舊紅色強度),則矩陣的 abcde 中,a 設為 1.0,bcde 都設為零,這樣就可以了。
除了紅色以外,其他通道的狀況也相同:
R' = (a * R) + (b * G) + (c * B) + (d * A) + e G' = (f * R) + (g * G) + (h * B) + (i * A) + j B' = (k * R) + (l * G) + (m * B) + (n * A) + o A' = (p * R) + (q * G) + (r * B) + (s * A) + t 新的紅色強度 = (a * 原紅色強度) + (b * 原綠色強度) + (c * 原藍色強度) + (d * 原不透明度強度) + e 新的綠色強度 = (f * 原紅色強度) + (g * 原綠色強度) + (h * 原藍色強度) + (i * 原不透明度強度) + j 新的藍色強度 = (k * 原紅色強度) + (l * 原綠色強度) + (m * 原藍色強度) + (n * 原不透明度強度) + o 新的不透明度強度 = (p * 原紅色強度) + (q * 原綠色強度) + (r * 原藍色強度) + (s * 原不透明度強度) + t
由此,如果您希望您的新圖片和舊圖片顏色完全相同,那麼您的矩陣應該會是如下這般:
[ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0 ]
當然實務上,您不可能真的希望新圖片和舊圖片完全一樣。現在我們可以來點變化。
比方說,以下的矩陣可以將紅色強度削弱為一半......
[ 0.5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0 ]
以下的矩陣可以將圖片的紅色變綠色,綠色變藍色,藍色變紅色......
[ 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0 ]
以下的矩陣可以將圖片均勻加亮 10%......
[ 1, 0, 0, 0, 0.1, 0, 1, 0, 0, 0.1, 0, 0, 1, 0, 0.1, 0, 0, 0, 1, 0 ]
如此一來,您應該能看出 im.MatrixColor 能做到什麼,又對什麼無能為力了吧。(所以別指望用它來做高斯模糊,那是不可能的!)
如果您對矩陣運算有興趣,中文維基百科在這個題目上寫得很不錯,推荐參考。
矩陣本身有點抽象,但 im.MatrixColor() 的使用方法卻很單純。
如下所示:
image 圖片名 = im.MatrixColor(圖片, 矩陣)
示範:
image street bright = im.MatrixColor("bg/street.png", [ 1, 0, 0, 0, 0.1, 0, 1, 0, 0, 0.1, 0, 0, 1, 0, 0.1, 0, 0, 0, 1, 0 ] )
或用 im.matrix 來顯式產生矩陣物件(先前的程式碼中的矩陣,是用列表 (list) 去模擬的):
image street bright = im.MatrixColor("bg/street.png", im.matrix([ 1, 0, 0, 0, 0.1, 0, 1, 0, 0, 0.1, 0, 0, 1, 0, 0.1, 0, 0, 0, 1, 0 ]) )
用 im.matrix() 顯式產生矩陣物件要多打一些字,那好處在哪裡呢?
其實,顯式產生矩陣物件後,矩陣就能夠進行相乘了。
而把「多個矩陣依序相乘後的結果矩陣」拿給 im.MatrixColor() 產生圖片,和「多個矩陣依序用 im.MatrixColor() 做串接運算」,效果是一樣的。
連續用 im.MatrixColor() 嵌套處理兩次的作法:
init python: # 為了程式清晰,把矩陣提前定義出來 bright = im.matrix([ 1, 0, 0, 0, 0.1, # 亮度增加 10% 的矩陣 0, 1, 0, 0, 0.1, 0, 0, 1, 0, 0.1, 0, 0, 0, 1, 0 ]) nored = im.matrix([ 0, 0, 0, 0, 0, # 去除紅色的矩陣 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0 ]) image street bright nored = im.MatrixColor( im.MatrixColor("bg/street.png", bright), # 先套用 bright 矩陣,產生發亮版本的 IM nored # 以發亮版本的 IM 為原料,再套用 nored 矩陣,產生最終版本的 IM )
先矩陣相乘再算圖的作法:
init python: bright = im.matrix([ 1, 0, 0, 0, 0.1, # 亮度增加 10% 的矩陣 0, 1, 0, 0, 0.1, 0, 0, 1, 0, 0.1, 0, 0, 0, 1, 0 ]) nored = im.matrix([ 0, 0, 0, 0, 0, # 去除紅色的矩陣 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0 ]) image street bright nored = im.MatrixColor( "bg/street.png", bright * nored # 先相乘再交給 im.MatrixColor() 做運算。注意:「矩陣相乘的前後順序,等於串接運算的前後順序」。順序顛倒結果是不一樣的! )
以上兩種算法得出的最終圖片,是完全一樣,不過第一個方法耗時是第二個方法的兩倍左右。
我不打算在此用數學證明這兩者為什麼相同,不過請相信我吧。
對於簡單的顏色變化,手工拼湊矩陣長什麼樣是沒問題,但如果有稍複雜一點的需求--比方說要把色相環旋轉 30 度之類的--用手拆矩陣簡直是一種將榮耀歸於色彩學的虔誠祭祀行為。不,不要這麼做,Ren'Py 已經內建了一些矩陣產生函式;您可以用這些函式更直覺地產生矩陣,而無需親手替矩陣填數字。
注意,以下這些用 im.matrix 開頭的函式只會產生「矩陣」,而不會直接產生圖片。要真正用它們來產生圖片,您得將這些矩陣套用給 im.MatrixColor() 使用。如下:
image street color_changed = im.MatrixColor( "street.jpg", im.matrix.hue(30) )
那來看看,有哪些矩陣產生函式可用吧!
改變圖片亮度。
b 為新圖片的亮度之強度,值在 -1.0 ~ 1.0 之間。負數表示變暗,正數表示變亮。
▲ 圖:im.matrix.brightness(-0.2) 的效果。
▲ 圖:im.matrix.brightness(0.2) 的效果。
用指定的兩個顏色來重新著色。
原圖片的黑色會被著為 black_color 的顏色,原圖片的白色則會被著為 white_color 的顏色。至於其他顏色則會依據以上兩個顏色進行線性插值。
這有點難懂。舉例來說:
▲ 圖:im.matrix.colorize("#000", "#F00") 的效果。最白的地方變成紅色,黑的地方依然是黑色。
▲ 圖:im.matrix.colorize("#F00", "#000") 的效果。白的地方依然是白色,黑的地方變成紅色。
▲ 圖:im.matrix.colorize("#000", "#888") 的效果。黑的地方依然為黑,但最白處只剩下 50% 灰色。
請注意:因為算法限制,用 im.matrix.colorize() 這個方式著色,圖片的色域與對比,只會越著越小!而不會加大!
如果希望著色後將對比重新拉開,還請在著色後,自行用下面會講到的 im.matrix.contrast() 調整一下。
因為功能近似,建議和後面介紹的 im.matrix.tint() 對照參考。
改變圖片的對比。
其中變數 c 為對比的強度,這個值以 1.0 為中心。當 c 為 0.0 ~ 1.0 時表示對比減小,1.0 以上則表示對比增加。
c = 0.0 表示毫無對比,整張圖會變成灰色的純色。
▲ 圖:im.matrix.contrast(0.5) 的效果。
▲ 圖:im.matrix.contrast(1.5) 的效果。
去飽和。
意思就是去掉圖片中的所有顏色,讓圖片變成灰階的。和前面介紹過的 im.Grayscale() 效果完全一樣。
旋轉色相環,改變圖片顏色。
h 是色相環的旋轉角度,理論上應該在 0 ~ 360 之間,不過實際上 Ren'Py 對此並無限制--如果旋轉角度設為 -90 ,就會自動等於 270、設為 450 則會自動等於 90 度......其他均可以此類推。
▲ 圖:色相環旋轉 60 度的示意圖。這意味紅色變黃色,綠色變淺藍,藍色變紫色......請參照上面的色相環以此類推。
用色相環改變圖片顏色的技巧,通常只適用於小物品或小部件上。因為大型圖片中,往往會有某些部分的顏色,永遠不該被我們做任何改變--如人物立繪中的皮膚色,又或背景中的天空藍等等。這些地方要是變色了,玩家會感覺遊戲很 Low 的。(當然存心要做特效時就另當別論了)
將圖片變成負片。
▲ 圖:負片效果。
「老實說呀,很少有要在遊戲中直接使用負片效果的地方呢。不過,除了直接用負片來創造效果以外,某些比較特殊的顏色變化,矩陣直接算並不容易算。這時各位也可先試著把圖轉成負片來算圖,等算好了之後,再一次反轉回來。」
「有時反轉算圖反而會比較容易。算圖算不出來時可參考這招試試啦。」
調整圖片整體的透明度。
o 為透明度,數值介於 0.0 與 1.0 之間。設為 0.0 表示完全透明,設為 1.0 表示完全不透明。
重新調整飽和度。
level 為新圖片飽和度的強度。0.0 為最小值,表示把圖片變為純灰階,等同於 im.matrix.desaturate();1.0 為飽和度等同原圖(不改變);更高的數值表示增加圖片的飽和度,圖片會變得更鮮艷。
▲ 圖:im.matrix.saturation(0.3) 的結果。飽和度降低到很接近灰階,但仔細看還是有點顏色。綠葉還是綠的。
▲ 圖:im.matrix.saturation(2.0) 的結果。飽和度因此而大幅提升,鮮艷到看起來很假......
其實這個函式還有更多參數可設,不過一般人大概是用不到,有興趣者可以去官網看看。
本矩陣提供了濾色功能。
tint 的 r、g、b 三個參數,表示三條顏色通道個別的「保留率」--最大為 1.0 (100%),最小為 0.0 (0%)。
...... 說起來,本函式其實就是前面介紹過的「im.matrix.colorize(black_color, white_color)」的簡化版,功能等於「im.matrix.colorize("#000", 顏色)」。當然了因為邏輯系出同源,這個矩陣產生函式,也有顏色色域越塗越窄的毛病。
rgb 介面並不特別好用,又失去 im.matrix.colorize() 原本具有的靈活性,故在下是不建議使用本函式啦。當需要著色時,推荐直接用 im.matrix.colorize() 來上色就好。
官方對 IM 的完整說明,可參考這一頁。
im.MatrixColor() 是個很有彈性的東西,發揮想像力可以作出很多不同的色彩調整。
比方說,在下覺得預設的 im.Sepia() 看起來不夠黃而偏灰,讓咱不太滿意......於是就嚐試著做些自訂,讓這個世界變得更黃褐色(?)一點吧。
▲ 圖:這是原始圖片。
▲ 圖:因為我只想保留亮度,所以乾脆先全部轉為灰階。就用 im.matrix.desaturate() 或 im.matrix.saturation(0.0) 來處理一下。
▲ 圖:用 im.matrix.colorize("#f00", "#fee"),因應亮度不同塗上濃淡不同的顏色。此處之所以上「紅色」而非黃褐色,是因為 im.matrix.colorize() 直接上黃褐色不太方便--為何如此很不好解說,總之,不信您大可親自試試。
▲ 圖:用 im.matrix.hue(30),旋轉色相環 30 度,紅色就變成黃褐色了。不過這飽和度實在太高,看起來嶄新無比,一點也不舊......
▲ 圖:用 im.matrix.saturation(0.3) 將飽和度降下來。以上,這就是我自己發明的泛黃照片效果了。和預設的 im.Sepia() 相比,是否別有一番感覺呢?
程式碼本身很簡單,見下:
image library sepia = im.MatrixColor("bg/library.png", im.matrix.saturation(0.0) * im.matrix.colorize("#f00","#fee") * im.matrix.hue(30) * im.matrix.saturation(0.3)) # 四個矩陣連乘
那麼,今天的心得就到此告一段落。
這次的節目應該還算好懂,咱為此可是花了不少功夫,各位覺得如何呢?快稱讚我吧(挺胸)。
「......我說啊,觀眾在粒子系統那邊就全死光了吧?」
呃?這個、那個......唔,粒子系統確實比較難一點......可惡......別哪壺不開提哪壺啊!......別講這個啦,粒子系統什麼的,那個是例外!
「佔了正文 50% 的東西,你非要說是例外?還真虧你說得出口啊......而且,隨隨便便就說內容很簡單什麼的,要是咱們的觀眾不幸正好沒聽懂,那不是太傷人了嗎?用詞稍微謹慎一點啊!」
呃?......呃呃呃......
「等等......哎呀呀,不會吧!難道你......是在繞彎子說觀眾是笨蛋?......討厭!怎麼能這麼壞心眼呢,真是好可怕的孩子啊。」
......不!不對!不是!我才沒這樣想!......那是妳吧啊啊啊!
「夠了呀,絲蔻兒......」
「耶?才剛要進入真正有趣的地方......」
這到底是哪門子有趣來著......
算、算了!不要再討論這個話題了!
咳嗯!
下回,雪凡與好朋友們的 Ren'Py 遊戲引擎初學心得提示,將會替 Ren'Py 中那堆子繁雜到讓人幾乎崩潰的基本圖形操作,作個大略的收尾。咱會和各位聊聊 Ren'Py 中的圖層觀念、播放影片的方法、Transform 語句的使用等等等......這些項目說起來,都是些小巧簡單到不知該放在哪個單元的瑣碎小物,但丟著不理也不是個辦法......另外本來預定在本回中就要加以說明的自定義轉場,也由此一併順延到下回,下次再聊。
其實說要替圖形主題收尾,Ren'Py 中還有一個與圖形有關的,名叫 screen 的大題目......不過那東西的規模實在有點大,就之後再說了。先讓大夥喘口氣,日後再戰!
收尾的老話還是那麼一句--
「下回,敬請期待!」
Comments
是說有可能用自訂粒子物件搭配碰 撞做彈幕小遊戲嗎 XD
Quoting Ian:
我沒這樣想過耶!……不過或許可 以一試?