隨便聊聊水面效果的2D實現(一)
0. 引子
一直想隨便寫寫自己關於水面效果2D實現的一些瞭解,可惜各種原因一直拖沓,幸而近來有些事情終算告一段落,自己也有了一些閒暇時間,於是便有了這篇東西 :)
1. 概述
關於水面效果的實現方法,google一下非常之多,目前的很多遊戲也有非常好的呈現,其中最令我印象深刻的當數《Crysis》~
自己由於工作原因接觸過一段時間的CryEngine,對於Crysis的水面渲染有一點點的瞭解,當然其中細節非常複雜,但就基本原理來講,就是將整塊水面細分成適當粒度的三角面,然後通過動態改變各個三角面的頂點位置來模擬水面的運動:
可能用Crysis舉例略顯“高大上”了一些,但其實就在我們接觸比較多的
那麼還有沒有其他方法來實現水面效果,並且能夠克服上面所提到的這些問題呢?其實答案很簡單,想必很多朋友也想到了,那就是使用Shader
2. 方法
使用Shader來實現2D水面效果,網上亦有不少資料,在此我也僅僅是簡單的按照自己的理解重述一遍而已,示例程式碼基於cocos2d-x-3.3rc0(由於原理程式碼都使用GLSL,所以引擎平臺其實並不重要,程式碼改改形式在Unity中使用應該也是可以的),箇中原始碼內容其實就是個簡單的HelloWorld,唯一值得一提的就是WaterEffectSprite型別,在此完整列出:
//WaterEffectSprite.h #ifndef __WATER_EFFECT_SPRITE_H__ #define __WATER_EFFECT_SPRITE_H__ #include "cocos2d.h" USING_NS_CC; class WaterEffectSprite : public Sprite { public: static WaterEffectSprite* create(const char *pszFileName); public: bool initWithTexture(Texture2D* texture, const Rect& rect); void initGLProgram(); }; #endif
//WaterEffectSprite.cpp
#include "WaterEffectSprite.h"
WaterEffectSprite* WaterEffectSprite::create(const char *pszFileName) {
auto pRet = new (std::nothrow) WaterEffectSprite();
if (pRet && pRet->initWithFile(pszFileName)) {
pRet->autorelease();
}
else {
CC_SAFE_DELETE(pRet);
}
return pRet;
}
bool WaterEffectSprite::initWithTexture(Texture2D* texture, const Rect& rect) {
if (Sprite::initWithTexture(texture, rect)) {
#if CC_ENABLE_CACHE_TEXTURE_DATA
auto listener = EventListenerCustom::create(EVENT_RENDERER_RECREATED, [this](EventCustom* event) {
setGLProgram(nullptr);
initGLProgram();
});
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
#endif
initGLProgram();
return true;
}
return false;
}
void WaterEffectSprite::initGLProgram() {
auto fragSource = (GLchar*)String::createWithContentsOfFile(
FileUtils::getInstance()->fullPathForFilename("Shaders/WaterEffect.fsh").c_str())->getCString();
auto program = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, fragSource);
auto glProgramState = GLProgramState::getOrCreateWithGLProgram(program);
setGLProgramState(glProgramState);
}
WaterEffectSprite的內容其實非常簡單,僅僅是繼承了Sprite型別然後將其fragment shader改寫為使用WaterEffect.fsh,而WaterEffect.fsh便是我們需要真正實現效果邏輯的地方~
OK,準備工作結束,我們可以屢起袖子,進入正題了 :)
#“旋轉”畫素
第一種方法類似於“旋轉”畫素,相關的解釋可以看看這裡,另外這裡也有一份HLSL實現,使用GLSL編寫,大概是這個樣子:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main() {
float timeFactor = 1;
float texFactor = 10;
float ampFactor = 0.01f;
// just like rotate pixel according to texture coordinate
v_texCoord.x += sin(CC_Time.y * timeFactor + v_texCoord.x * texFactor) * ampFactor;
v_texCoord.y += cos(CC_Time.y * timeFactor + v_texCoord.y * texFactor) * ampFactor;
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;
}
其中timeFactor可以控制水波運動的快慢,texFactor可以控制水波運動的“粒度”,ampFactor則可控制水波運動的幅度,給張截圖:
當然,由於我們單獨控制UV兩個方向的紋理座標偏移,所以相關引數自然也可以不同,就像這樣:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main() {
float timeFactorU = 1;
float texFactorU = 10;
float ampFactorU = 0.01f;
float timeFactorV = 1;
float texFactorV = 10;
float ampFactorV = 0.01f;
v_texCoord.x += sin(CC_Time.y * timeFactorU + v_texCoord.x * texFactorU) * ampFactorU;
v_texCoord.y += cos(CC_Time.y * timeFactorV + v_texCoord.y * texFactorV) * ampFactorV;
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;
}
如果再將這些引數變為uniform,那麼擴充套件性就更強了 :)
# “偏移”畫素
第二種方法其實類似於3D方式的水面渲染,首先我們計算水面上任意一點的“高度”,然後將其直接對映到對應貼圖座標的偏移中,方法很簡單,直接按照“高度”值成比例做偏移即可(此處我不是非常肯定,但感覺上這種對映方法似乎是平行對映(parallax mapping)的一種簡單應用,熟悉的朋友可以告知一下~)(這裡和這裡也有相關的介紹)
相關shader程式碼大概是這個樣子:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
// get wave height based on distance-to-center
float waveHeight(vec2 p) {
float timeFactor = 4.0;
float texFactor = 12.0;
float ampFactor = 0.01;
float dist = length(p);
return cos(CC_Time.y * timeFactor + dist * texFactor) * ampFactor;
}
void main() {
// convert to [-1, 1]
vec2 p = -1.0 + 2.0 * v_texCoord;
vec2 normal = normalize(p);
// offset texcoord along dist direction
v_texCoord += normal * waveHeight(p);
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;
}
其中timeFactor、texFactor 和ampFactor 的含義與第一種方法相同(其實從正弦曲線函式y=Asin(ωx+φ)中引數含義的角度可以更好的理解:)),同樣給張截圖:
與第一種方法一樣,我們也可以以上面的程式碼為基礎,稍稍做些擴充套件,簡單的譬如改變水波的中心位置:
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
// get wave height based on distance-to-center
float waveHeight(vec2 p) {
float timeFactor = 4.0;
float texFactor = 12.0;
float ampFactor = 0.01;
float dist = length(p);
return cos(CC_Time.y * timeFactor + dist * texFactor) * ampFactor;
}
void main() {
vec2 center = vec2(0, 0);
vec2 p = (v_texCoord - center) * 2.0;
vec2 normal = normalize(p);
// offset texcoord along dist direction
v_texCoord += normal * waveHeight(p);
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;
}
再來張截圖:
複雜一些的還有引入簡單的光照:
基本思路就是通過水麵任意點的“高度”變化計算出該點的normal值,接著就是普通的光照計算了(示例程式碼可能有誤,僅作參考了~)
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
// get wave height based on distance-to-center
float waveHeight(vec2 p) {
float timeFactor = 4.0;
float texFactor = 12.0;
float ampFactor = 0.01;
float dist = length(p);
return cos(CC_Time.y * timeFactor + dist * texFactor) * ampFactor;
}
// get point fake normal
vec3 waveNormal(vec2 p) {
vec2 resolution = vec2(480, 320);
float scale = 240;
float waveHeightRight = waveHeight(p + vec2(2.0 / resolution.x, 0)) * scale;
float waveHeightLeft = waveHeight(p - vec2(2.0 / resolution.x, 0)) * scale;
float waveHeightTop = waveHeight(p + vec2(0, 2.0 / resolution.y)) * scale;
float waveHeightBottom = waveHeight(p - vec2(0, 2.0 / resolution.y)) * scale;
vec3 t = vec3(1, 0, waveHeightRight - waveHeightLeft);
vec3 b = vec3(0, 1, waveHeightTop - waveHeightBottom);
vec3 n = cross(t, b);
return normalize(n);
}
void main() {
vec2 p = -1.0 + 2.0 * v_texCoord;
vec2 normal = normalize(p);
v_texCoord += normal * waveHeight(p);
vec4 lightColor = vec4(1, 1, 1, 1);
vec3 lightDir = vec3(1, 1, 1);
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor * lightColor * max(0, dot(lightDir, waveNormal(p)));
gl_FragColor.a = 1;
}
這裡僅僅引入了一個平行光,效果有限,不過同樣給張截圖:)
# 凸凹對映
第三種方法可能大家都耳熟能詳了,就是3D渲染中常見的凸凹對映,其中法線貼圖可能是最常見的一種凸凹對映技術了,在此我們亦可以仿照3D的做法,將法線貼圖對映至普通的Sprite之上,以達到模擬水面的效果~
當然,之前所列出的WaterEffectSprite類需要做些簡單修改,大抵是改寫一下其中的initGLProgram方法:
void WaterEffectSprite::initGLProgram() {
auto fragSource = (GLchar*)String::createWithContentsOfFile(
FileUtils::getInstance()->fullPathForFilename("Shaders/WaterEffect.fsh").c_str())->getCString();
auto program = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, fragSource);
auto glProgramState = GLProgramState::getOrCreateWithGLProgram(program);
setGLProgramState(glProgramState);
auto normalMapTextrue = TextureCache::getInstance()->addImage("Textures/water_normal.jpg");
Texture2D::TexParams texParams = { GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT };
normalMapTextrue->setTexParameters(texParams);
getGLProgramState()->setUniformTexture("u_normalMap", normalMapTextrue);
}
我們還需要準備一張水面Normal貼圖,我使用的大概是這麼一張:
GLSL程式碼大致上簡單的實現了一下水面的折射效果以及簡單的normal UV動畫(程式碼可能有誤,僅作參考了~)
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_normalMap;
vec3 waveNormal(vec2 p) {
vec3 normal = texture2D(u_normalMap, p).xyz;
normal = -1.0 + normal * 2.0;
return normalize(normal);
}
void main() {
float timeFactor = 0.2;
float offsetFactor = 0.5;
float refractionFactor = 0.7;
// simple UV animation
vec3 normal = waveNormal(v_texCoord + vec2(CC_Time.y * timeFactor, 0));
// simple calculate refraction UV offset
vec2 p = -1 + 2 * v_texCoord;
vec3 eyePos = vec3(0, 0, 100);
vec3 inVec = normalize(vec3(p, 0) - eyePos);
vec3 refractVec = refract(inVec, normal, refractionFactor);
v_texCoord += refractVec.xy * offsetFactor;
gl_FragColor = texture2D(CC_Texture0, v_texCoord) * v_fragmentColor;
}
同樣給張截圖:
當然,我們還可以繼續引入光照(例如高光)等元素來加強水面效果的顯示,不過3D味道也會愈來愈濃,有興趣的朋友可以深入嘗試一下 :)
# 其他
我所見到的其他2D水面實現方法大抵都是上面方法的一些變種,如果你還知道其他方式,就請不吝告之一下吧~
3. 後記
OK,講了不少東西,也該停一停了,這次講了一些我自己歸類為Water Effect的2D水面效果實現方法,另外一類我覺得比較重要的還有個人稱為Ripple Effect的2D水面效果,有機會下次再隨便講講吧,就這樣了~
相關推薦
隨便聊聊水面效果的2D實現(一)
0. 引子 一直想隨便寫寫自己關於水面效果2D實現的一些瞭解,可惜各種原因一直拖沓,幸而近來有些事情終算告一段落,自己也有了一些閒暇時間,於是便有了這篇東西 :) 1. 概述 關於水面效果的實現方法,google一下非常之多,目前的很多遊戲也有非常好的呈現,其中
基於神經網路的2D攝像頭的手勢識別系統實現(一)
一、手勢識別的分類 若按照攝像頭的種類(2D攝像頭、深度攝像頭)來分,可分為兩類,1)基於2D攝像頭的二維手勢識別 和 2)基於3D攝像頭(如微軟的kinnect)三維手勢識別。早期的手勢識別識別是基於二維彩色影象的識別技術,所謂的二維彩色影象是指通過普通攝
異步線程池的實現(一)-------具體實現方法
fun format 測試 路徑 線程池。 用戶體驗 deb tar clas 本篇是這個內容的第一篇,主要是寫:遇到的問題,和自己摸索實現的方法。後面還會有一篇是總結性地寫線程池的相關內容(偏理論的)。 一、背景介紹 朋友的項目開發到一定程度之後,又遇到
多種排序算法的思路和簡單代碼的實現(一)
insert i++ 前後端 分享 size quicksort 執行 判斷 clas 就自己簡單的理解了一些排序算法(JAVA)思路和代碼分享給大家:歡迎大家進行交流。 直接插入排序,折半插入排序,冒泡排序,快速排序 1 public class Sort { 2
Dji Mobile SDK 基礎實現(一)
n-1 app lba ger print ttl touch事件 釋放 bsp Dji Mobile SDK 基礎實現(一) 本文簡要介紹如何通過調用DJI Mobile SDK,實現獲取和釋放無人機的控制權限、模擬遙控器按鈕控制無人機的飛行、獲取無人機的回傳視頻、獲取無
實現自定義查詢的數據庫設計及實現(一)
bre 名稱 審批流程 work 數據庫名 需要 自定義查詢 perm 枚舉 需求 先說一下需求:實現用戶自定義的查詢,用戶可以自定義要查詢的列、自定義條件條件、自定義排序。除了查詢使用外,還可以使用於各個需要根據條件進行約束的業務,如權限; 本設計和實現,很大部分是通過數
視頻流GPU解碼的實現(一)-基本概念
bsp 視頻流 class 概念 logs log 視頻 .com 認識 這段時間在實現Gpu的視頻流解碼,遇到了很多的問題。 要想實現ffempg的GPU化,必須要要對ffempg的解碼cou流程有基本的認識才能改造 我在http://www.cnblogs.com/
MVVM模式解析和在WPF中的實現(一)
開發 特點 還需 如果 情況下 依次 顯示 尋找 這也 MVVM模式簡介 MVVM是Model、View、ViewModel的簡寫,這種模式的引入就是使用ViewModel來降低View和Model的耦合,說是降低View和Model的耦合。也可以說是是降低界面和邏輯的耦合
hadoop雲盤client的設計與實現(一)
white 下一跳 -c 文件 。。 edi track ++ ava 近期在hadoop雲盤client項目。在做這個項目曾經對hadoop是一點都不了解呀,在網
基於樹莓派(Raspberry Pi)平臺的MQ-2煙霧報警系統以及結合Zabbix監控的實現(一)
Raspberry Pi Zabbix和嵌入式系統的結合 Python3 樹莓派和MQ-2氣體檢測 一、前期準備 達成目標: 利用Rapberry Pi 驅動MQ-2煙霧報警模塊,對信息進行采集和提取,而後Zabbix監控系統來收集和處理信息采集到的信息。
基於樹莓派(Raspberry Pi)平臺的智能家居實現(一)----繼電器模塊,DHT11模塊
Raspberry 繼電器模塊 DHT11溫濕度模塊 智能家居 前言: ??其實做這個智能家居系統我還是因為學校的畢業設計,距離上篇文章發布已經過去了20多天了,之前想著只是做一個煙霧報警,然後通過Zabbix進行報警,但是通過這20多天的設計,我發現實現報警的功能其
Android項目實戰(十六):QQ空間實現(一)—— 展示說說中的評論內容並有相應點擊事件
con toast short demo append 集合 obj parent 自帶 原文:Android項目實戰(十六):QQ空間實現(一)—— 展示說說中的評論內容並有相應點擊事件大家都玩QQ空間客戶端,對於每一個說說,我們都可以評論,那麽,對於某一條評論:
KVM虛擬化的四種簡單網絡模型介紹及實現(一)
_for only 應該 code eth tun x86_64 信息 dock KVM中的四種簡單網絡模型,分別如下:1、隔離模型:虛擬機之間組建網絡,該模式無法與宿主機通信,無法與其他網絡通信,相當於虛擬機只是連接到一臺交換機上。2、路由模型:相當於虛擬機連接到一臺路由
Algorand算法實現(一)
span agreement 科學 anti 文章 技術 給定 節點 雲上 判斷節點是potential leader的條件: H(Sig(r, 1, Br-1)) <= 1 / size(PKr-k) size(PKr-k)為第r-k輪中網絡中參與區塊共識的
Android 滑動效果入門篇(一)—— ViewFlipper
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
資料結構實現(一):動態陣列(C++版)
資料結構實現(一):動態陣列(C++版) 1. 概念及基本框架 2. 基本操作程式實現 2.1 增加操作 2.2 刪除操作 2.3 修改操作 2.4 查詢操作 2.5 其他操作 3. 演算法複雜度分析
iOS研發助手DoraemonKit技術實現(一)
一、前言 一個比較成熟的App,經歷了多個版本的迭代之後,為了方便調式和測試,往往會積累一些工具來應付這些場景。最近我們組就開源了一款適用於iOS App線下開發、測試、驗收階段,內建在App中的工具集合。使用DoraemonKit,你無需連線電腦,就可以對於App的資訊進行快速的檢視。一鍵接入、使用方便,
Java關於傳統的excel匯出的實現(一)
匯出的excel如下: 如果沒有特殊的格式啥的要求,此方法已經滿足,如果遇到標題或者定製的那種內容,就需要改造此方法!注意一下,這個方法只適合簡單的匯出使用。如果那種定製模板的匯出,在下一篇文章中我會有給出方法! 第一,控制層(controller層)的程式碼如下: /** * 列
中小型園區網路的設計與實現 (一)
在職本科臨近畢業,論文是躲不掉的。 謹此來記錄畢業論文的完成過程。 論文大綱 部分雖然已經交上去很多日了。但是個人覺得有必要寫到第一篇裡,以示論文過程的完整。 先聊一下寫論文大綱的思路。 第一,首先論文是需要有專案背景的,就像做一件事情要有個目的。說仔細點就是某項工作需要交
五子棋專案的實現(一)
在這個學期裡花了一個學期的時間零零散散寫了一個五子棋專案,專案的實現主要還是根據華南理工大學裡面劉瑞的一篇碩士論文《五子棋人工智慧演算法設計與實現》。其實我也不認識他,但是在知網找相關資料時,就發現這篇的引用率最高也是最通俗