《蜘蛛俠:平行宇宙》的視覺解析與濾鏡實現

早在四年前,索尼想要製做全新風格的「蜘蛛俠」電影這一消息被泄露時,一個顯眼的詞彙便被反覆說起——「rejuvenate」,譯爲「使其恢復年輕」。當時,除去《蜘蛛俠:返校日》以外,由索尼出品的《蜘蛛俠》真人電影已有五部之多(託比·馬圭爾主演《蜘蛛俠》三部曲,以及安德魯·加菲爾德主演的兩部《超凡蜘蛛俠》)。索尼高層但願以一部動畫電影來讓這個系列從新恢復活力。html

而爲了拍攝這部動畫電影,索尼找到了當時因《天降美食》系列以及《樂高大電影》等做品在業界小有名氣的導演搭檔——菲爾·羅德和克里斯托弗·米勒。有着天馬行空的創意以及各類鬼才想法的這兩位導演提出的要求則是:但願這部動畫讓觀衆感到「就像走進了漫畫裏通常」。他們同時也爲可以在這部電影中使用到真人電影沒法達成的敘事手段而感到興奮。dom

第一次看到《蜘蛛俠:平行宇宙》這部電影時,就被其中的十足創新的視覺動態效果所驚豔,跟以往 3D 動畫追求天然流暢截然相反,《蜘蛛俠:平行宇宙》反而在往漫畫觀感體驗靠攏,讓 3D 動畫 2D 化,並且不是單純的 2D 化。ide

正如在一篇文章裏看到的評論:混搭上各種平行宇宙間不一樣的漫畫風格,保留下了對話框、漫畫分格、字幕特效,還尤爲增強了手繪塗鴉的部分,並刻意打造出這部分的「稚拙感」。當這些再加上神奇到炸裂的分鏡,足以讓人看得應接不暇。接下來咱們簡單的解析下這電影的亮點:工具


1、視覺表達的創新點

1. 一拍二模擬 2D 動畫

在進入正題故事沒多久,相信看慣了 3D 動畫的你們都會有種稍稍不適應的感受...「這電影是否是有些卡?」動畫

沒錯,它確實就是卡。這是電影第一個讓人感受有些「異常」的地方,卻也是電影最特別的地方。仔細看你能夠發現只有人物角色是有着卡頓感的,而它們的周圍環境卻很流暢,造成了一種微妙的對比。關於這一點,在知乎中有網友給咱們做出瞭解答:ui

電影通常採用的是 24 幀每秒的制式,也就是說若是是手繪 2D 動畫片,須要每秒連續畫 24 張畫以使畫面中動畫流暢,這也就是動畫中俗稱的1拍1。spa

可是有人發現我一樣的動做只畫第 1,3,5,7....合計總共只畫 12 幀,而後把每幀停留 2 倍時間,觀衆並不會明顯察覺,而動畫師卻省下一半繪製時間,這就是動畫中俗稱的1拍2,這個行爲的發明只是爲了節約成本。3D 動畫通常不這麼作是由於 3D 角色動畫師在設置好關鍵幀後,其中間幀是自動生成的,因此自己不必去節省中間幀。翻譯

1拍2是 2D 動畫的傳統作法,3D 動畫強行這麼作可讓人產生 2D 動畫的質感錯覺,但同時 Sony 選擇了背景動畫、鏡頭動畫和其它位移動畫又保持1拍1,從而保持 3D 動畫特有的鏡頭順滑的優點。設計

這樣的處理手法一方面使得畫面有種 2D 逐幀動畫特有的逐幀質感(如同小時候看的阿凡提動畫那樣),另外一方面產生一種天然中卻又有點違和的感覺,比如人物和場景不在同一個時空中,這使得這個電影的視覺表現與故事主題產生了一種呼應和統一。3d

2. 漫畫手法還原

電影中咱們能夠看到不少在美式漫畫中經常使用的手法和效果。如爆炸的 BOOM、跳樓時的 AHHHHH、吐絲時的 THWIP 這些手繪擬聲詞;主角奔跑時、參與戰鬥前描述內心活動的對話框;速度線、感知危險的信號提示等等。這些都在不斷讓咱們重溫閱讀漫畫的感覺。下面是這些表現手法在電影中的應用 :

這些表現手法,使得電影畫面多了份在平面漫畫中才有的張力,這和它們多年曆史沖刷下變化而來的屬性有關(漫畫的歷史沿襲不一樣文化的不一樣路徑,且在發展中有了不少的交融,這裏主要從美式漫畫手法的角度出發)。咱們以漫畫中常見的對話框爲例。

你們都知道圖像成爲人類表達媒介的時間要早於文字,然而,在人們學會使用連續的圖像來表達一個善始善終的故事以前,遇到須要說明的情節,單靠一幅圖像是沒法知足的,因而,一直到公元前兩千多年前的埃及人,他們想到了一種能夠說明的辦法。

這是古埃及麥勒盧卡(Mereruka)陵墓中,兩個男人正在屠殺鬣狗的情景壁畫。讓人值得注意的是,在兩個角色之間的空白處飄浮着象形文字,經翻譯,內容是其中一我的對另外一我的說:「 捉緊它!

文字的出現幫助幫助繪者豐富了圖像以外的信息——表達主角的思想活動,同時又增長了畫面的緊張感

這些文字雖是總體的一部分,但也獨立於圖像以外,和圖像關聯度不大。而到了公元前7世紀左右的中美洲文明中,出現了用抽象的符號來歸納語言的方式,這種符號後來逐漸發展成爲一種相似卷軸或旗幟的圖形,在歐洲中世紀和文藝復興的藝術中都曾出現。或許是由於受限於構圖,這種對話方式所涉及的語言較爲簡略,但這個時候文字已可以比較好得融合到畫面中

從18世紀開始流行政治性諷刺漫畫,其中出現了大量的對白。這個時期的漫畫家們已經很是熟悉如何使用對話框了,有的像簡化了的卷軸,有的前粗後細的相似氣球的氣泡,總之形式便很是接近現代漫畫中的對話框。並且這些對話框已經能承載更多的文字,對畫面起到了必定的渲染氛圍的做用

發展到了20世紀初,對話框在漫畫中的做用已經很關鍵,同時也衍生了擬聲詞、情緒符號等等漫畫手法,它們不只僅是內容、思想的傳達,還具有着情緒表達、推進劇情、氛圍渲染等等的做用

回到咱們的電影。正是這些在平面漫畫中常見手法和元素,使得靜態的漫畫如同能夠「發聲」通常,且由於這些手法讓《蜘蛛俠:平行宇宙》比起通常的 3D 電影,在視覺層面豐富了觀衆其它感官的感覺。看回前面的動圖,哪怕沒有聲音,咱們都能經過眼睛更具體得「聽」到它發生了什麼。

3. 不用運動模糊

既然要還原漫畫閱讀體驗,一些 3D 動畫裏常常用的手法天然也要拋棄,好比運動模糊。可是去掉運動模糊後,本來在 3D 動畫中流暢的運動感和速度感會變得很是不足,這個決定一般會對電影風格產生直接影響。

關於這個問題,電影的特效總監迪米安是這麼說的,咱們知道咱們必須解決(沒有運動模糊)。咱們只是不想用任何看起來過於傳統的東西來解決它。咱們想從2D動畫中尋找解決的方案和想法。咱們真的在找一些東西,它看起來像是受到動畫的啓發,或者是經過塗片、拉伸幾何圖形來表現運動模糊的方式,但卻不是那樣作的。最後,採用了一些解決方案。Imageworks 的線條工具容許藝術家繪製一樣鏈接形狀的運動線,也有從相機快門啓發的技術。例如,迪米安說,快速相機平底鏡上的背景,不管是合成仍是藉助特效,都會被塗上很大的污點和劃線,但卻會以一種很是塊狀的插圖方式出現。咱們基本上儘可能避免任何看起來像是平滑的漸變或者由於運動模糊而變得模糊的東西。相反,這是一種很是圖形化的處理。

後面咱們能夠看到,電影裏經過疊影(相機快門啓發的方式)、場景的錯位速度線等方式來補充畫面的動感,並保證了電影自己的風格語言。

4. 色散/高光溢色

實際上咱們能夠從電影畫面裏看到通常產生於比較老化或者早期的鏡頭拍攝的照片質感(高光溢色),還有一些鏡頭色散的效果,這其實也是電影工做室有意爲之。

導演 Peter Ramsey 在接受採訪時是這麼對電影畫面解釋的:某種意義上說,你根本不用花費精力去注意什麼,由於咱們作的就是還原漫畫,而咱們在看漫畫的時候會發現,有時候漫畫裏都有錯誤,有時候印刷很差,顏色都塗到了線條以外,因此看起來就以爲模糊不清。並且這和真人電影是相通的,好比有時鏡頭中一些東西是沒有對上焦有些模糊,這樣觀衆纔會集中注意力在重要而清晰的畫面上。

色散/高光溢色的效果在這裏就是起到虛化和模糊的做用。如導演提到的印刷很差的狀況,在 20 世紀初中期的印刷行業常常出現,當時的 CMYK 4色印刷會由於有對版的偏差出現致使噴色錯版,這樣的錯版在後面發展中也成爲了一種風格化的處理,導演注意到這點,並應用到了電影中,這既把漫畫的質感還原得更到位,讓觀者產生共鳴同時也利用這樣的錯位生成一種視覺景深感,使得電影的層次更強,解決了向 2D 靠近後在電影顯得平的問題。

這部電影在視覺表達上有不少的突破,創做團隊很大膽得把多種風格不一的人物混合在一塊兒,使得平行宇宙的概念合理化,並經過平面漫畫的方式把視覺語言和整個電影的題的結合達到了高度的統一。整個電影透露出一種到處不穩定的矛盾感,創做團隊沒有把這種矛盾消弱,而是把他們都展示了出來,這樣的碰撞感覺讓人不得不佩服他們對於總體的把控和自信。


2、視覺效果與濾鏡實現

電影從片頭就火燒眉毛的展示出了它在表現形式上的創新,但仔細觀察你會發現裏面用到的效果並不是從0到1的創新,只不過索尼的動畫團隊把這些視覺元素應用的淋漓盡致。

因爲工做中涉及了短視頻濾鏡和轉場的 OpenGL Shader 代碼編寫,因此在二刷電影的時候特別留意了電影裏的特效,並思考有哪些是能夠經過 Shader 來實現的。固然了,電影裏的效果是動畫設計師反覆調整和多重加工出來的,絕非一段代碼就能完美模擬。這裏僅僅是從技術角度去探討電影裏一些效果在 Shader 代碼層面的可行性。

1. HalfTone 半調濾鏡

電影高度還原了漫畫應有的觀賞體驗,在畫面的渲染上使用了 Ben-Day dot (本戴點)讓咱們感覺到了閱讀紙質漫畫書的質感。Ben-Day dot 與 HalfTone 的不一樣之處在於 Ben-Day dot 在特定區域中的大小和分佈老是相同,而 HalfTone 能根據圖像的顏色細節呈現大小和漸變不必定點。咱們能夠用 HalfTone 半調濾鏡去生成差很少的質感。下面截取幾張截圖可見一斑:

科普下 Ben-Day dot (本戴點),全名是「本戴點狀製版法」,以插圖畫家和印刷商 Benjamin Henry Day, Jr.(19 世紀出版商 Benjamin Henry Day 的兒子)命名,在 1879 年被髮明的印刷製版技術。它根據顏色和視錯覺原理,經過小彩色點的間隔疏密、大小或重疊來生成所須要的效果。例如,洋紅色圓點間隔比較寬就會造成粉紅色。 20 世紀 50 年代和 60 年代的彩色漫畫書很受歡迎,可是全綵漫畫的成本很高,所以漫畫出版商在四色印刷( 青色、品紅色、黃色、黑色 )中使用了 Ben-Day dot,經過它打印出陰影和二次色 ,如綠色、紫色、橙色、肉色等,由於本戴點的特性這種方式能夠節省大量的油墨,漫畫商經過這樣廉價的印刷方式印刷在便宜紙張上生產漫畫書,從而能夠得到較高的收益。

後來這種方式被美國波普藝術大師羅伊·利希滕斯坦(RoyLichtenstein,1923~1997)充分運用在繪畫上並使其發揚光大。在 20 世紀中葉,抽象表現繪畫盛極一時之際,他和紐約年輕一代畫家,提出了新形式的具象繪畫——波普藝術,在 60 年代一鳴驚人,成爲美國新藝術運動的主推進人,甚至許多曾受美國商業影響的域外文化,亦感覺他做品的震撼力。利希滕斯坦的繪畫或雕刻做品,引用自漫畫、日用品、廣告以及一些美術史上名畫家的做品造型,結合本戴點的特色再現了美國社會當時濃厚的商業文化和時代特徵。

咱們來看看實現的方式。在 OpenGL 的 Shader 語言中,經過建立一個排列的圓形的 pattern,結合圖像的 RGB 通道色值獲得圖像的半調圖案,在跟原圖進行「變亮」圖像混合模式,能夠獲得咱們想要的半調質感:

precision highp float;
uniform sampler2D inputImageTexture;
varying vec2 textureCoordinate;

vec2 center = vec2(.5, .5);
float angle = 1.57;
float scale = 1.;
vec2 tSize = vec2(1000., 563.); 

float blendLighten(float base, float blend) {
	return max(blend,base);
}

vec3 blendLighten(vec3 base, vec3 blend) {
	return vec3(blendLighten(base.r,blend.r),blendLighten(base.g,blend.g),blendLighten(base.b,blend.b));
}

vec3 blendLighten(vec3 base, vec3 blend, float opacity) {
	return (blendLighten(base, blend) * opacity + base * (1.0 - opacity));
}

float pattern() {
    float s = sin( angle ), c = cos( angle );
    vec2 tex = textureCoordinate * tSize - center;
    vec2 point = vec2( c * tex.x - s * tex.y, s * tex.x + c * tex.y ) * scale;
    return ( sin( point.x ) * sin( point.y ) ) * 4.0;
}

void main() {
    vec4 color = texture2D(inputImageTexture, textureCoordinate);
    float average = ( color.r + color.g + color.b ) / 3.0;
    vec4 halftone = vec4( vec3( average * 10.0 - 5.0 + pattern() ), color.a );
    gl_FragColor = color + vec4(.02)*halftone;
    gl_FragColor = vec4(blendLighten(color.rgb, halftone.rgb, .05), 1.);
}
複製代碼

而後使用圖層混合模式「變亮」,將兩張圖作像素融合:

這裏作了一個網頁 Demo,點擊連接後經過右上角的濾鏡開關能夠看到先後效果:Demo1

2. Glitch 故障效果

Glitch 故障效果在不少都互聯網產品中一樣能夠看到應用,最典型的好比抖音的 LOGO 動畫以及短視頻中的濾鏡效果。在電影中,因爲另外幾位蜘蛛俠來自不一樣的平行世界致使原子不穩定,表現出了差很少的花裏胡哨的故障效果。

索尼工做室在製做故障效果的時候使用了手繪圖案+多層效果融合的方式來呈現,固然了,代碼想要模擬這樣的效果並不容易,若是能夠找到一張合適的遮罩圖也許能夠大體模擬出來。不過這裏只單純展現常規的基於圖像自己的故障效果(代碼經過基於時間來作像素和顏色通道偏移來模擬故障效果):

precision highp float; 
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture2;
varying vec2 textureCoordinate;
uniform float time;

float amount = 0.1;
float speed = 0.5;

float random1d(float n){
    return fract(sin(n) * 43758.5453);
}
float random2d(vec2 n) {
    return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
float randomRange (in vec2 seed, in float min, in float max) {
    return min + random2d(seed) * (max - min);
}
float insideRange(float v, float bottom, float top) {
    return step(bottom, v) - step(top, v);
}
float rand(vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

float blendDarken(float base, float blend) {
	return min(blend,base);
}

vec3 blendDarken(vec3 base, vec3 blend) {
	return vec3(blendDarken(base.r,blend.r),blendDarken(base.g,blend.g),blendDarken(base.b,blend.b));
}

vec3 blendDarken(vec3 base, vec3 blend, float opacity) {
	return (blendDarken(base, blend) * opacity + base * (1.0 - opacity));
}
void main() {
    vec2 uv = textureCoordinate;
    float sTime = floor(sin(time*0.0005) * speed * 6.0 * 24.0);
    vec3 inCol = texture2D(inputImageTexture, uv).rgb;
    vec3 outCol = inCol;
    float maxOffset = amount/4.0;
    vec2 uvOff;
    for (float i = 0.0; i < 10.0; i += 1.0) {
        if (i > 10.0 * amount) break;
        float sliceY = random2d(vec2(sTime + amount, 2345.0 + float(i)));
        float sliceH = random2d(vec2(sTime + amount, 9035.0 + float(i))) * 0.25;
        float hOffset = randomRange(vec2(sTime + amount, 9625.0 + float(i)), -maxOffset, maxOffset);
        uvOff = uv;
        uvOff.x += hOffset;
        vec2 uvOff = fract(uvOff);
        if (insideRange(uv.y, sliceY, fract(sliceY+sliceH)) == 1.0 ){
            outCol = texture2D(inputImageTexture, uvOff).rgb;
        }
    }
    float maxColOffset = amount/6.0;
    vec2 colOffset = vec2(randomRange(vec2(sTime + amount, 3545.0),-maxColOffset,maxColOffset), randomRange(vec2(sTime , 7205.0),-maxColOffset,maxColOffset));
    uvOff = fract(uv + colOffset);
    float rnd = random2d(vec2(sTime + amount, 9545.0));
    if (rnd < 0.33) {
        outCol.r = texture2D(inputImageTexture, uvOff).r;
    } else if (rnd < 0.66) {
        outCol.g = texture2D(inputImageTexture, uvOff).g;
    } else {
        outCol.b = texture2D(inputImageTexture, uvOff).b;
    }
    vec3 inCol2 = texture2D(inputImageTexture2, uv).rgb;
    vec3 finalColor = blendDarken(outCol, inCol2);
    gl_FragColor = vec4(finalColor, 1.0);
}
複製代碼

一樣的,經過 Demo 右上角的濾鏡開關能夠看到先後效果: Demo2

3. RGB Shift/Split RGB 分離

細心能夠發現,上面第二個 Glitch 效果一樣用到了 RGB 顏色分離,在電影中出現了大量的這樣的效果,把色彩變化應用到極致,有些魔性有些虛幻。

Shader 實現起來會相比上面兩個濾鏡更簡單一下,經過對圖像的 RGB 三個顏色作拆分以及座標偏移就能夠實現:

precision highp float;
uniform sampler2D inputImageTexture;
varying vec2 textureCoordinate;
uniform float time;

float amount = 0.01;
float angle = 0.;

void main() {
    vec2 offset = amount * vec2(cos(time*.001), sin(time*.001));
    vec4 cr = texture2D(inputImageTexture, textureCoordinate + offset);
    vec4 cga = texture2D(inputImageTexture, textureCoordinate);
    vec4 cb = texture2D(inputImageTexture, textureCoordinate - offset);
    gl_FragColor = vec4(cr.r, cga.g, cb.b, cga.a);
}
複製代碼

下面兩張圖片對比先後效果:

一樣的,經過 Demo 右上角的濾鏡開關能夠看到先後效果:Demo3

最後,這部電影能夠說的太多太多,有太多讓人以爲驚豔的點,沒辦法一一都提到,咱們只能從視覺方面和實現的可能性簡單得聊聊,歡迎補充。


在快寫完文章的時候,想起開頭 Sony 說到的「rejuvenate(使其恢復年輕)」。咱們能夠感受到這個電影從故事和風格無不充斥着如今年輕人會喜歡的元素,能夠說這是一部很酷的做品,可是在看到了不少資料後咱們不由有一些感慨,這些很酷炫的效果和元素風格其實在上個世紀就已經存在且有着很深的歷史痕跡,從某種意義上,與其說是更年輕的體現,感受倒不如說是這是一種復古了。由此咱們也延伸出了一些問題:

有人說風格是個輪迴,這麼一看,所謂的「更年輕的風格」或者「過氣的風格」都像是個僞命題。也就是說,是否是並不存在什麼年輕或過期的風格,只有時間時間久了,看膩了的風格?

又或者說,是否是由於技術的創新才讓產品(電影或者其它)變得年輕了,和風格,實際沒太大關係?感受挺有意思,能夠想一想。


相關連接:

相關文章
相關標籤/搜索