【淺墨Unity3D Shader編程】之一 夏威夷篇:游戲場(chǎng)景的創(chuàng)建 & 第一個(gè)Shader的書(shū)寫(xiě)

作為一個(gè)系統(tǒng)介紹Unity3D中Shader編寫(xiě)的系列文章的開(kāi)篇,本文的第一部分為系列文章的前言峭拘,然后第二部分介紹了這個(gè)系列文章中我們會(huì)使用的游戲場(chǎng)景創(chuàng)建方式俊庇,最后一部分講解了如何在Unity中創(chuàng)建和使用Shader,為后面專(zhuān)注于介紹如何在Unity中進(jìn)行Shader編程打好了基礎(chǔ)鸡挠。

因?yàn)楹竺嫱瞥龅南盗形恼聲?huì)著重介紹各種Shader的寫(xiě)法和實(shí)現(xiàn)辉饱,不會(huì)再具體講解如何創(chuàng)建場(chǎng)景和寫(xiě)出Shader代碼后如何使用,相信這篇文章作為本系列的開(kāi)篇拣展,發(fā)表出來(lái)肯定還是會(huì)對(duì)大家多少有些幫助的彭沼。大家以后閱讀稍后推出的Unity Shader系列文章的時(shí)候,有場(chǎng)景創(chuàng)建或者Shader代碼寫(xiě)好了如何使用方面的疑問(wèn)的話(huà)备埃,可以隨時(shí)回過(guò)頭來(lái)查閱這篇文章姓惑。

OK,就讓我們從這篇文章開(kāi)始一趟精彩萬(wàn)分的Shader游戲編程旅途按脚。
依舊國(guó)際慣例于毙,看幾張文章中實(shí)現(xiàn)的場(chǎng)景美圖先:

上圖中展示的文本配套Unity工程的可運(yùn)行exe淺墨也為大家準(zhǔn)備好了节沦,有興趣的朋友們可以點(diǎn)擊 這里進(jìn)行下載兜喻、運(yùn)行和探索:

PS:文章配套的三個(gè)unitypackage和最終的工程源碼在文章末尾處提供下載。

一忿檩、系列文章前言

在這個(gè)系列開(kāi)頭,淺墨想說(shuō)的是介蛉,其實(shí)這個(gè)系列文章中我們學(xué)的主要是著色器編程技術(shù)萌庆,重點(diǎn)不是學(xué)Unity。
甚至可以這樣說(shuō)币旧,我們學(xué)的是HLSL——沒(méi)錯(cuò)践险,就是DirectX中的那個(gè)HLSL。

為什么這樣講佳恬,讓我們來(lái)捋一捋捏境。
首先于游,Unity中編寫(xiě)Shader的語(yǔ)言叫做ShaderLab毁葱,而ShaderLab說(shuō)白了就是裹著一層皮的CG著色器語(yǔ)言而已。Cg贰剥,即C forgraphics倾剿,即用于圖形的C語(yǔ)言,是微軟Microsoft和英偉達(dá)NVIDIA相互協(xié)作在標(biāo)準(zhǔn)硬件光照語(yǔ)言的語(yǔ)法和語(yǔ)義上達(dá)成的一種一致性協(xié)議蚌成。
HLSL和CG其實(shí)是同一種語(yǔ)言(參見(jiàn)Cg教程_可編程實(shí)時(shí)圖形權(quán)威指南29頁(yè)的致謝部分)前痘。很多時(shí)候,我們會(huì)發(fā)現(xiàn)用HLSL寫(xiě)的代碼可以直接當(dāng)中Cg代碼使用担忧。
Microsoft和NVIDIA聯(lián)手推出CG語(yǔ)言芹缔,想在經(jīng)濟(jì)和技術(shù)上實(shí)現(xiàn)雙贏(yíng),從而通過(guò)這種方式聯(lián)手打擊他們共同的對(duì)手GLSL瓶盛。

既然Unity主打Shader編程的語(yǔ)言ShaderLab是CG語(yǔ)言披上一層皮最欠,而CG語(yǔ)言又約等于HLSL。這就是說(shuō)惩猫,在Unity中寫(xiě)Shader約等于用HLSL寫(xiě)Shader芝硬,也就約等于給DirectX寫(xiě)Shader。雖然有點(diǎn)繞orz轧房,最后總結(jié)一下也就是:
在Unity中寫(xiě)Shader約等于給DirectX寫(xiě)Shader

而Unity又是這樣一個(gè)集萬(wàn)千寵愛(ài)于一身的可見(jiàn)即所得的目前在移動(dòng)互聯(lián)網(wǎng)時(shí)代火到不行的游戲引擎拌阴。可以說(shuō)奶镶,Unity可見(jiàn)即所得的開(kāi)發(fā)環(huán)境非常適合Shader的學(xué)習(xí)迟赃,而且在Unity分分鐘可以創(chuàng)建出來(lái)一個(gè)漂亮的場(chǎng)景里面寫(xiě)寫(xiě)Shader,心情都會(huì)好很多厂镇。不再是苦逼地面朝代碼背朝天了纤壁。
所以淺墨決定開(kāi)始在Unity中進(jìn)行這個(gè)shader學(xué)習(xí)系列,畢竟Unity剪撬、CG摄乒、HLSL不分家。
另外促成這個(gè)系列文章的成型的一個(gè)原因是Unity的Asset store。淺墨很喜歡Unity引領(lǐng)的Asset store這樣一站式的游戲素材商店馍佑,里面有數(shù)不盡的游戲插件斋否、素材、資源拭荤、腳本代碼參考茵臭、shader資料等等,甚至現(xiàn)成的游戲工程范例舅世。Asset store給開(kāi)發(fā)者們帶來(lái)了很大的便利旦委,簡(jiǎn)直就是游戲開(kāi)發(fā)界的大寶庫(kù)。
但需要說(shuō)明的是雏亚,Unity這款引擎的缺點(diǎn)也是很明顯的缨硝。比如Unity不對(duì)外開(kāi)源,我們看不到源碼罢低,坑比較多查辩,遇到坑了看不到
源碼給解決帶來(lái)了很大難度。執(zhí)行效率還是不太行网持,渲染大場(chǎng)景和做高級(jí)渲染的時(shí)候還是顯得力不從心宜岛。比如渲染大片的近乎
真實(shí)的動(dòng)態(tài)水面時(shí),幀數(shù)就立馬降下來(lái)了功舀,需要后期做很多的優(yōu)化萍倡。就畫(huà)面質(zhì)量來(lái)說(shuō)和Unreal Engine和cryEngine等次時(shí)代
引擎比還是有差距。是最近幾年互聯(lián)網(wǎng)的浪潮和得屌絲者的天下商業(yè)模式促進(jìn)了其最近幾年如此的成功辟汰。

說(shuō)了這么多列敲,總結(jié)一下。
Unity只是我們學(xué)習(xí)CG莉擒、HLSL編程可見(jiàn)即所得的好幫手好工具而已酿炸。我們主要還是利用它來(lái)更好的學(xué)計(jì)算機(jī)圖形學(xué)和Shader編程,順便掌握目前熱門(mén)的Unity引擎的基本使用和研發(fā)思路涨冀。
我們還是忘不了C++和DirectX填硕,我們還是渴望通過(guò)自己的努力,最終有能力用C/C++一句一句寫(xiě)出自己的游戲引擎鹿鳖,我們還是想從零開(kāi)始造輪子扁眯,畢竟那樣一句一句寫(xiě)出來(lái)的代碼都是自己的,而不是那些別人為我們準(zhǔn)備好的現(xiàn)成的API函數(shù)翅帜。
謹(jǐn)以此前言姻檀,與諸君共勉。

二涝滴、用Unity創(chuàng)建第一個(gè)美麗的游戲場(chǎng)景

首先要說(shuō)明的是绣版,作為一個(gè)從入門(mén)內(nèi)容開(kāi)始逐漸深入介紹的系列教程胶台,這一部分在Unity下創(chuàng)建場(chǎng)景的內(nèi)容是為還不太熟悉Unity的朋友們準(zhǔn)備的,如果你已經(jīng)熟悉了Unity的使用杂抽,這部分可以快速跳讀诈唬。

OK,正式開(kāi)始吧缩麸。

2.1 【第一步】當(dāng)然是新建一個(gè)項(xiàng)目

大家肯定都知道铸磅,每次新建項(xiàng)目后或者新建場(chǎng)景后,會(huì)得到一片Unity中默認(rèn)為全藍(lán)的場(chǎng)景杭朱。想要場(chǎng)景變得有生機(jī)阅仔,一般都是去菜單欄的GameObject里面新建Terrain(地形)然后進(jìn)行編輯。Terrain的制作很耗時(shí)而且水很深弧械,想要精通也是得花一些功夫八酒。
甚至Unity Asset Store中有各種可以輔助快速生成AAA級(jí)大作風(fēng)格的真實(shí)場(chǎng)景的插件,如Terrain Composer梦谜,配合著Unity中有名的Relief Terrain Pack v3地形輔助著色渲染工具丘跌,可以生成近乎以假亂真的三維自然風(fēng)光出來(lái)。
漂亮地形的創(chuàng)建當(dāng)然不屬于我們講解的重點(diǎn)唁桩,網(wǎng)絡(luò)上有數(shù)不清的文章和視頻講這方面的內(nèi)容,有需要的話(huà)耸棒,大家去學(xué)一些基礎(chǔ)荒澡,或者直接用Asset Store中現(xiàn)成的各種漂亮場(chǎng)景。反正淺墨是各種在網(wǎng)上下載AssetStore中美工大牛們做場(chǎng)景与殃,然后簡(jiǎn)單的修改单山,為自己測(cè)試和平常調(diào)數(shù)值所用。
這不幅疼,淺墨為了寫(xiě)這篇博客米奸,就為大家再加工“創(chuàng)作“了一個(gè)夏威夷風(fēng)格的場(chǎng)景來(lái):)

2.2 【第二步】導(dǎo)入Hawaii Environment.unitypackage場(chǎng)景包

上文講到,為了節(jié)約時(shí)間爽篷,淺墨提前為大家修改好了一個(gè)場(chǎng)景悴晰,然后這個(gè)場(chǎng)景已經(jīng)打包,叫做“HawaiiEnvironment.unitypackage “逐工,在文章末尾提供下載铡溪。(限于Unity對(duì)中文的支持拙計(jì),無(wú)奈只能取英文名泪喊,不然直接導(dǎo)入就報(bào)錯(cuò))棕硫。

HawaiiEnvironment.unitypackage單獨(dú)下載請(qǐng)點(diǎn)我】

雙擊這個(gè)包,導(dǎo)入到我們空空如也的工程中袒啼,經(jīng)過(guò)一段時(shí)間的讀條哈扮,就導(dǎo)入完畢了纬纪。然后我們雙擊打開(kāi)出現(xiàn)在Project面板中Assets文件夾下的場(chǎng)景文件Hawaii Environment.unity



接著便打開(kāi)了場(chǎng)景,如果打開(kāi)成功滑肉,Scene面板中應(yīng)該便出現(xiàn)了如下類(lèi)似的場(chǎng)景畫(huà)面:


因?yàn)槁匀チ藞?chǎng)景編輯部分育八,直接導(dǎo)入,所以過(guò)程是非常簡(jiǎn)單的赦邻。但是髓棋,這還完全不夠。讓我們?cè)趫?chǎng)景中添加一個(gè)可以自由控制的攝像機(jī)吧惶洲。

2.3 【第三步】添加第一人稱(chēng)攝像機(jī)

淺墨準(zhǔn)備的這個(gè)場(chǎng)景包是沒(méi)有攝像機(jī)的按声,單單就是場(chǎng)景,所以我們還需要在場(chǎng)景中添加一個(gè)攝像機(jī)恬吕。

大家應(yīng)該清楚签则,比較常見(jiàn)添加攝像機(jī)的做法是通過(guò)菜單欄中的GameObject->CreateOther->Camera來(lái)添加。但這種方式的攝像機(jī)是固定的铐料,不合我們的要求渐裂。我們想添加的是一個(gè)在游戲運(yùn)行時(shí)可以自由移動(dòng)視角的第一人稱(chēng)攝像機(jī)。其實(shí)Unity自帶的資源包中剛好可以滿(mǎn)足我們的要求钠惩。于是我們?cè)赑roject面板中右鍵【Import Package】柒凉,或者菜單欄中Assets->ImportPackage->Character Controller來(lái)導(dǎo)入官網(wǎng)為我們準(zhǔn)備的的角色控制資源包。如下圖:


點(diǎn)擊之后篓跛,會(huì)彈出如下的資源導(dǎo)入確認(rèn)窗口膝捞,我們直接點(diǎn)確定就行了:


因?yàn)檫@個(gè)包很小,所以很快就導(dǎo)入完成愧沟,這時(shí)Assets根文件夾下會(huì)出現(xiàn)一個(gè)名為Standard Assets的文件夾蔬咬,展開(kāi)它或者點(diǎn)進(jìn)去,就是名為【CharacterControllers】的文件夾沐寺,繼續(xù)點(diǎn)進(jìn)去林艘,發(fā)現(xiàn)了一個(gè)膠囊狀的叫【First PersonController】的家伙,這就是我們需要的了混坞。
然后我們先在Scene面板中利用【右鍵+鍵盤(pán)W狐援、A、S拔第、D】以及滾輪等操作調(diào)整好場(chǎng)景咕村,然后在我們剛才的【CharacterControllers】下點(diǎn)擊這個(gè)膠囊裝的【First Person Controller】按住不放,拖動(dòng)到Scene場(chǎng)景中蚊俺,選到合適的地方(如草坪上)后就放手懈涛,操作如下:

Unity會(huì)自動(dòng)將這個(gè)【CharacterControllers】的中心位置依附到地形表面。

這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn)之前是黑屏的Game面板中也有了畫(huà)面:


這時(shí)我們還要將這個(gè) First Person Controller的底部向上拖動(dòng)一點(diǎn)泳猬,不然運(yùn)行游戲時(shí)我們會(huì)不停的往下掉批钠,因?yàn)樵赨nity默認(rèn)情況下First Person Controller的中心位于中部宇植,這會(huì)照成它的底部已經(jīng)穿透地形,懸空位于地形的下方埋心,自然一運(yùn)行就往下掉指郁。

我們Hierarchy面板中選中First Person Controller,工具欄中選擇【移動(dòng)】工具

然后對(duì)著場(chǎng)景中膠囊上的代表Y軸的綠色箭頭向上適當(dāng)拖動(dòng)拷呆,讓膠囊的底部確保位于草地之上就行了闲坎。

這時(shí)候我們點(diǎn)擊unity中間三角尖的【運(yùn)行】按鈕,就可以自由地在場(chǎng)景中觀(guān)察和移動(dòng)了~


Unity第一人稱(chēng)控制器默認(rèn)操作方式類(lèi)似CS茬斧,CF一類(lèi)的FPS游戲腰懂。W、A项秉、S绣溜、D前后左右移動(dòng),空格跳躍娄蔼,鼠標(biāo)移動(dòng)調(diào)整視角怖喻,非常有親切感有木有~
這就很好地體現(xiàn)了Unity的入門(mén)容易的特點(diǎn),只用點(diǎn)點(diǎn)鼠標(biāo)一個(gè)漂亮的場(chǎng)景就展現(xiàn)在眼前岁诉。

2.4 【第四步】在游戲場(chǎng)景中加入背景音樂(lè)

話(huà)說(shuō)這么美麗的場(chǎng)景怎么能沒(méi)有音樂(lè)锚沸?
不妨就讓我們添加一段優(yōu)美的鋼琴曲吧。曲子淺墨都為大家準(zhǔn)備好了唉侄,上文已經(jīng)導(dǎo)入的HawaiiEnvironment.unitypackage包中咒吐,在A(yíng)ssets根目錄下包含了一首林海的《日光告別》。

我們要做的只要是把這個(gè)音樂(lè)文件拖拽到第一人稱(chēng)攝像機(jī)First PersonController上就可以了属划,就像箭頭中指的這樣:


拖拽完成后,F(xiàn)irst Person Controller的Inspector面板中就應(yīng)該會(huì)多了一個(gè)Audio Source組件候生。


運(yùn)行場(chǎng)景同眯,伴隨著美麗的場(chǎng)景,“吹著海風(fēng)”唯鸭,優(yōu)美的鋼琴曲入耳须蜗,非常怡人。

先放兩張測(cè)試過(guò)程中的場(chǎng)景美圖目溉,再繼續(xù)我們下一部分的講解吧:

非常逼真的水效明肮,采用大名鼎鼎的NGUI工作室Tasharen Entertainment出品的水面插件:


三、導(dǎo)入QianMo’s Toolkit并使用

3.1 認(rèn)識(shí)QianMo's Toolkit

所謂的QianMo's Toolkit缭付,其實(shí)就是淺墨為場(chǎng)景測(cè)試寫(xiě)的一個(gè)小腳本工具集柿估,打包成一個(gè)unitypackage方便多次使用而已。若有需要陷猫,淺墨會(huì)在其中添加更多的功能秫舌。
以后我們每次新建工程的時(shí)候的妖,只要導(dǎo)入這個(gè)小工具就可以使用我們之前已經(jīng)寫(xiě)好的各種特性,非常便捷足陨。

QianMo's Toolkit v1.0.unitypackage單獨(dú)下載請(qǐng)點(diǎn)我】

QianMo's Toolkit v1.0版的內(nèi)容如下:


也就是包含了五個(gè)腳本文件嫂粟,兩張圖片。這五個(gè)腳本文件的功能分別為:

ShowFPS:在游戲運(yùn)行時(shí)顯示幀率相關(guān)信息
ShowObjectInfo:在場(chǎng)景中和游戲窗口中分別顯示添加給任意物體文字標(biāo)簽信息墨缘。隱藏和顯示可選星虹,基于公告板技術(shù)實(shí)現(xiàn)。
ShowGameInfo:在游戲運(yùn)行時(shí)顯示GUI相關(guān)說(shuō)明
ShowLogo:在游戲運(yùn)行時(shí)顯示Logo
ShowUI:在游戲運(yùn)行時(shí)顯示簡(jiǎn)單的鑲邊UI镊讼。

這個(gè)五個(gè)腳本的代碼淺墨都已經(jīng)詳細(xì)注釋?zhuān)诤罄m(xù)文章中有機(jī)會(huì)我們會(huì)介紹其具體寫(xiě)法宽涌。這篇文章中就先簡(jiǎn)單的認(rèn)識(shí)一下他們就好。PS:下文第四節(jié)中貼出了ShowGameInfo腳本的全部代碼狠毯。

3.2 使用QianMo's Toolkit

上文已經(jīng)說(shuō)了护糖,既然這是一個(gè)unitypackage,那么只用雙擊它導(dǎo)入到我們當(dāng)前的項(xiàng)目中就行了嚼松。導(dǎo)入完成之后嫡良。Assets文件夾下就又多了一個(gè)名為” QianMo's Toolkit v1.0“的文件夾,內(nèi)容就是我們剛才介紹的5個(gè)腳本文件兩張圖:

暫時(shí)我們要使用的是ShowGameInfo献酗、ShowLogo寝受、ShowUI這三個(gè)腳本文件,把它們一起拖到我們之前創(chuàng)建的第一人稱(chēng)攝像機(jī)First Person Controller上就行了:


拖動(dòng)完成后罕偎,我們?cè)贔irst Person Controller的Inspector面板中發(fā)現(xiàn)其多了三個(gè)組件很澄,就是我們給他添加的這個(gè)三個(gè)腳本:


其實(shí)Show Logo無(wú)關(guān)緊要,就是顯示了淺墨自己的logo而已,當(dāng)然你可以換成自己的logo颜及。show UI也無(wú)關(guān)緊要甩苛,就是顯示了一個(gè)頂部的鑲邊png。主要的是這個(gè)ShowGameInfo俏站,它是用于顯示幀率等相關(guān)文字消息的讯蒲。(其實(shí)簡(jiǎn)約黨會(huì)覺(jué)得三個(gè)都無(wú)關(guān)緊要,orz)

拖動(dòng)完成之后肄扎,再次運(yùn)行墨林,我們來(lái)看一看效果:


可以發(fā)現(xiàn),游戲窗口的邊邊角角多了一些說(shuō)明和圖片犯祠,以及有了幀率的顯示旭等。

四、書(shū)寫(xiě)和使用第一個(gè)Shader

上文講解的都是一般的場(chǎng)景構(gòu)建過(guò)程衡载,接下來(lái)就要正式開(kāi)始我們的核心部分搔耕,書(shū)寫(xiě)第一個(gè)Shader了。在完成上文中講解的創(chuàng)建好漂亮的場(chǎng)景之后月劈,我們首先可以在A(yíng)ssets根目錄下創(chuàng)建一個(gè)文件夾度迂,用于以后Shader和Material文件的存放藤乙。創(chuàng)建過(guò)程可以是在Project面板的空白處右鍵->Create->Folder,也可以是點(diǎn)擊Project面板中的Create下拉菜單->Folder


給新出來(lái)的這個(gè)文件夾取名Shaders惭墓,然后回車(chē)坛梁。然后再用同樣的方法在A(yíng)ssets根目錄下創(chuàng)建一個(gè)名為T(mén)extures的文件夾,用于稍后素材圖片的存放腊凶。那么划咐,如果你按照淺墨按照目前描述的步驟來(lái)的話(huà),Assets根目錄到現(xiàn)在就是這樣的5個(gè)文件夾:

接著钧萍,進(jìn)去到我們創(chuàng)建的Shader文件夾褐缠。同樣在空白處右鍵->Create->Shader,或者是直接點(diǎn)Create下拉菜單->Shader风瘦,創(chuàng)建一個(gè)Shader文件队魏,取名為 “0.TheFirstShader”。然后雙擊打開(kāi)它万搔,Unity會(huì)默認(rèn)使用名為MonoDevelop的編輯器打開(kāi)這個(gè)Shader文件胡桨。


小tips:可以在菜單欄中Edit->Preferences->ExternalTools中調(diào)成默認(rèn)用Visual Studio打開(kāi),但未經(jīng)修改配置文件的Visual Studio對(duì)Shader后綴的文件是不支持語(yǔ)法高亮的瞬雹,淺墨修改了部分配置文件才讓Visual Studio支持了Unity Shader書(shū)寫(xiě)的語(yǔ)法高亮昧谊。對(duì)于不太清楚如何修改的朋友,可以善用搜索引擎酗捌,或者過(guò)些天淺墨會(huì)單獨(dú)發(fā)一篇名為《Unity中使用Visual Studio編寫(xiě)shader并設(shè)置代碼高亮》的文章來(lái)專(zhuān)門(mén)講解呢诬。


作為初次寫(xiě)Shader,我們暫且先用MonoDevelop頂一頂胖缤,后面的文章再換用修改了配置文件的Visual Studio尚镰。

好了,用MonoDevelop打開(kāi)我們新建的這個(gè)Shader文件哪廓,發(fā)現(xiàn)Unity已經(jīng)為我們寫(xiě)好了很多代碼钓猬。
我們不妨自己重新寫(xiě)點(diǎn)不一樣的東西。刪掉原本的這些代碼撩独,拷貝淺墨寫(xiě)的如下代碼到編輯器中:

//-----------------------------------------------【Shader說(shuō)明】----------------------------------------------
//      Shader功能:   凹凸紋理顯示+自選邊緣顏色和強(qiáng)度
//     使用語(yǔ)言:   Shaderlab
//     開(kāi)發(fā)所用IDE版本:Unity4.5 06f 、Monodevelop   
//     2014年11月2日  Created by 淺墨    
//     更多內(nèi)容或交流請(qǐng)?jiān)L問(wèn)淺墨的博客:http://blog.csdn.net/poem_qianmo
//---------------------------------------------------------------------------------------------------------------------


Shader "淺墨Shader編程/0.TheFirstShader" 
{
    //-------------------------------【屬性】-----------------------------------------
    Properties 
    {
        _MainTex ("【紋理】Texture", 2D) = "white" {}
        _BumpMap ("【凹凸紋理】Bumpmap", 2D) = "bump" {}
        _RimColor ("【邊緣顏色】Rim Color", Color) = (0.17,0.36,0.81,0.0)
        _RimPower ("【邊緣顏色強(qiáng)度】Rim Power", Range(0.6,9.0)) = 1.0
    }

    //----------------------------【開(kāi)始一個(gè)子著色器】---------------------------
    SubShader 
    {
        //渲染類(lèi)型為Opaque账月,不透明
        Tags { "RenderType" = "Opaque" }

        //-------------------開(kāi)始CG著色器編程語(yǔ)言段-----------------
        CGPROGRAM

        //使用蘭伯特光照模式
        #pragma surface surf Lambert
        
        //輸入結(jié)構(gòu)
        struct Input 
        {
            float2 uv_MainTex;//紋理貼圖
            float2 uv_BumpMap;//法線(xiàn)貼圖
            float3 viewDir;//觀(guān)察方向
        };

        //變量聲明
        sampler2D _MainTex;//主紋理
        sampler2D _BumpMap;//凹凸紋理
        float4 _RimColor;//邊緣顏色
        float _RimPower;//邊緣顏色強(qiáng)度

        //表面著色函數(shù)的編寫(xiě)
        void surf (Input IN, inout SurfaceOutput o)
        {
            //表面反射顏色為紋理顏色
            o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
            //表面法線(xiàn)為凹凸紋理的顏色
            o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
            //邊緣顏色
            half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
            //邊緣顏色強(qiáng)度
            o.Emission = _RimColor.rgb * pow (rim, _RimPower);
        }

        //-------------------結(jié)束CG著色器編程語(yǔ)言段------------------
        ENDCG
    } 

    //“備胎”為普通漫反射
    Fallback "Diffuse"
}

由于這是第一篇Shader系列文章综膀,已經(jīng)涉及到很多內(nèi)容了,所以淺墨不可能展開(kāi)講解這段代碼的具體思路和寫(xiě)法局齿,不過(guò)已經(jīng)詳細(xì)注釋?zhuān)蠹覒?yīng)該會(huì)多少有點(diǎn)明白剧劝。隨著稍后文章的深入,這段代碼就顯得很簡(jiǎn)單易懂了抓歼。
拷貝完成讥此,保存一下這段代碼拢锹,unity會(huì)自動(dòng)檢測(cè)和編譯被保存的代碼,我只需返回Unity窗口萄喳,等待編譯完成即可卒稳。若沒(méi)有錯(cuò)誤,在“0.TheFirstShader”的inspector面板中得到的結(jié)果應(yīng)該是有紅色的錯(cuò)誤提示的他巨。

需要注意的是充坑,Shader想要使用到游戲物體上,一般得有個(gè)媒介染突,這個(gè)媒介就是我們的老朋友——材質(zhì)(Material)捻爷。我們把Shader作用于材質(zhì),接著再把材質(zhì)對(duì)應(yīng)地作用于給游戲物體份企,這樣寫(xiě)的Shader就間接地給物體表面使用了也榄。

而這一層關(guān)系,在Unity中完全可以通過(guò)點(diǎn)點(diǎn)鼠標(biāo)司志,拖動(dòng)來(lái)完成甜紫。下面我們就來(lái)講一講如何將一個(gè)著色程序的結(jié)果顯示到物體表面上。

知道以上原理了就很簡(jiǎn)單了俐芯,在“0.TheFirstShader.shader”的同一目錄下創(chuàng)建一個(gè)Material棵介。同樣是可以通過(guò)Create下拉菜單->Material或者空白處右鍵->create->Material來(lái)完成。


為了到時(shí)候方便對(duì)應(yīng)吧史,我們將這個(gè)材質(zhì)也取名為0.TheFirstShader邮辽。

接著,將0.TheFirstShader.shader拖動(dòng)到0.TheFirstShader材質(zhì)身上然后釋放贸营。


拖動(dòng)完成后吨述,我們單擊0.TheFirstShader材質(zhì),打開(kāi)他的面板钞脂,發(fā)現(xiàn)他已經(jīng)和一開(kāi)始不一樣了揣云,泛著藍(lán)光:


還沒(méi)完,接下來(lái)我們還得給這個(gè)材質(zhì)添加兩張紋理圖片冰啃。圖片淺墨也已經(jīng)提前準(zhǔn)備好了邓夕,在名為T(mén)extures01 by QianMo.unitypackage的Unity包中,同樣雙擊這個(gè)包然后打開(kāi)導(dǎo)入到項(xiàng)目中阎毅。

Textures01 by QianMo.unitypackage單獨(dú)下載請(qǐng)點(diǎn)我】

我們?cè)赥extures文件夾下找到這兩張紋理焚刚,接下來(lái)做的就是將他們拖動(dòng)到0.TheFirstShader材質(zhì)對(duì)應(yīng)的紋理區(qū)域中,如下:


或者點(diǎn)擊這里的Select分別選擇扇调,操作如下:


兩張紋理選擇完畢后矿咕,我們的材質(zhì)就準(zhǔn)備好了,最后的結(jié)果,有點(diǎn)黑科技碳柱,如各種科幻游戲和電影中發(fā)光的礦石捡絮,非常炫酷:

OK,那么就只剩下最后一步了莲镣,就是在場(chǎng)景中創(chuàng)建一個(gè)物體福稳,然后將我們做好的材質(zhì)拖拽到物體身上賦給這個(gè)物體就行了。

菜單欄【GameObject】->【Create Other】->【Capsule】或者【Create】下拉菜單->【Capsule】來(lái)在場(chǎng)景中創(chuàng)建一個(gè)膠囊裝的物體剥悟。把他拖動(dòng)到和我們的第一人稱(chēng)攝像機(jī)【First Person Controller】很近的地方灵寺,這樣方便觀(guān)察,接著就可以把我們的“0.TheFirstShader”材質(zhì)直接拖拽給場(chǎng)景中的這個(gè)膠囊区岗,或者Hierachy面板中【Capsule】名字上就行了略板,操作如下圖中的箭頭所示:


經(jīng)過(guò)拖拽,Capsule加上Material后的效果如下:


4.1 給使用Shader的物體加上文字說(shuō)明

為了以后介紹多個(gè)Shader寫(xiě)法時(shí)能更清晰更方便慈缔,淺墨專(zhuān)門(mén)在QianMo’s Toolkit中做了一個(gè)可以在場(chǎng)景中和游戲窗口中分別顯示附加給任意物體文字標(biāo)簽信息的工具腳本叮称,叫做ShowObjectInfo,其詳細(xì)注釋的代碼如下:


//-----------------------------------------------【腳本說(shuō)明】-------------------------------------------------------
//      腳本功能:    在場(chǎng)景中和游戲窗口中分別顯示給任意物體附加的文字標(biāo)簽信息
//      使用語(yǔ)言:   C#
//      開(kāi)發(fā)所用IDE版本:Unity4.5 06f 藐鹤、Visual Studio 2010    
//      2014年10月 Created by 淺墨    
//      更多內(nèi)容或交流瓤檐,請(qǐng)?jiān)L問(wèn)淺墨的博客:http://blog.csdn.net/poem_qianmo
//---------------------------------------------------------------------------------------------------------------------

//-----------------------------------------------【使用方法】-------------------------------------------------------
//      第一步:在Unity中拖拽此腳本到某物體之上,或在Inspector中[Add Component]->[淺墨's Toolkit v1.0]->[ShowObjectInfo]
//      第二步:在Inspector里,Show Object Info 欄中的TargetCamera參數(shù)中選擇需面向的攝像機(jī),如MainCamera
//      第三步:在text參數(shù)里填需要顯示輸出的文字娱节。
//      第四步:完成挠蛉。運(yùn)行游戲或在場(chǎng)景編輯器Scene中查看顯示效果。

//      PS:默認(rèn)情況下文本信息僅在游戲運(yùn)行時(shí)顯示肄满。
//      若需要在場(chǎng)景編輯時(shí)在Scene中顯示谴古,請(qǐng)勾選Show Object Info 欄中的[Show Info In Scene Editor]參數(shù)。
//      同理,勾選[Show Info In Game Play]參數(shù)也可以控制是否在游戲運(yùn)行時(shí)顯示文本信息
//---------------------------------------------------------------------------------------------------------------------


//預(yù)編譯指令稠歉,檢測(cè)到UNITY_EDITOR的定義掰担,則編譯后續(xù)代碼
#if UNITY_EDITOR    


//------------------------------------------【命名空間包含部分】----------------------------------------------------
//  說(shuō)明:命名空間包含
//----------------------------------------------------------------------------------------------------------------------
using UnityEngine;
using UnityEditor;
using System.Collections;

//添加組件菜單
[AddComponentMenu("淺墨's Toolkit v1.0/ShowObjectInfo")]


//開(kāi)始ShowObjectInfo類(lèi)
public class ShowObjectInfo : MonoBehaviour
{

    //------------------------------------------【變量聲明部分】----------------------------------------------------
    //  說(shuō)明:變量聲明部分
    //------------------------------------------------------------------------------------------------------------------
    public string text="鍵入你自己的內(nèi)容 by淺墨";//文本內(nèi)容
    public Camera TargetCamera;//面對(duì)的攝像機(jī)
    public bool ShowInfoInGamePlay = true;//是否在游戲運(yùn)行時(shí)顯示此信息框的標(biāo)識(shí)符
    public bool ShowInfoInSceneEditor = false;//是否在場(chǎng)景編輯時(shí)顯示此信息框的標(biāo)識(shí)符
    private static GUIStyle style;//GUI風(fēng)格



    //------------------------------------------【GUI 風(fēng)格的設(shè)置】--------------------------------------------------
    //  說(shuō)明:設(shè)定GUI風(fēng)格
    //------------------------------------------------------------------------------------------------------------------
    private static GUIStyle Style
    {
        get
        {
            if (style == null)
            {
                //新建一個(gè)largeLabel的GUI風(fēng)格
                style = new GUIStyle(EditorStyles.largeLabel);
                //設(shè)置文本居中對(duì)齊
                style.alignment = TextAnchor.MiddleCenter;
                //設(shè)置GUI的文本顏色
                style.normal.textColor = new Color(0.9f, 0.9f, 0.9f);
                //設(shè)置GUI的文本字體大小
                style.fontSize = 26;
            }
            return style;
        }

    }




    //-----------------------------------------【OnGUI()函數(shù)】-----------------------------------------------------
    // 說(shuō)明:游戲運(yùn)行時(shí)GUI的顯示
    //------------------------------------------------------------------------------------------------------------------
    void OnGUI( )
    {
        //ShowInfoInGamePlay為真時(shí),才進(jìn)行繪制
        if (ShowInfoInGamePlay)
        {
            //---------------------------------【1.光線(xiàn)投射判斷&計(jì)算位置坐標(biāo)】-------------------------------
            //定義一條射線(xiàn)
            Ray ray = new Ray(transform.position + TargetCamera.transform.up * 6f, -TargetCamera.transform.up);
            //定義光線(xiàn)投射碰撞
            RaycastHit raycastHit;
            //進(jìn)行光線(xiàn)投射操作,第一個(gè)參數(shù)為光線(xiàn)的開(kāi)始點(diǎn)和方向怒炸,第二個(gè)參數(shù)為光線(xiàn)碰撞器碰到哪里的輸出信息带饱,第三個(gè)參數(shù)為光線(xiàn)的長(zhǎng)度
            collider.Raycast(ray, out raycastHit, Mathf.Infinity);
            
            //計(jì)算距離,為當(dāng)前攝像機(jī)位置減去碰撞位置的長(zhǎng)度
            float distance = (TargetCamera.transform.position - raycastHit.point).magnitude;
            //設(shè)置字體大小阅羹,在26到12之間插值
            float fontSize = Mathf.Lerp(26, 12, distance / 10f);
            //將得到的字體大小賦給Style.fontSize
            Style.fontSize = (int)fontSize;
            //將文字位置取為得到的光線(xiàn)碰撞位置上方一點(diǎn)
            Vector3 worldPositon = raycastHit.point + TargetCamera.transform.up * distance * 0.03f;
            //世界坐標(biāo)轉(zhuǎn)屏幕坐標(biāo)
            Vector3 screenPosition = TargetCamera.WorldToScreenPoint(worldPositon);
            //z坐標(biāo)值的判斷勺疼,z值小于零就返回
            if (screenPosition.z <= 0){return;}
            //翻轉(zhuǎn)Y坐標(biāo)值
            screenPosition.y = Screen.height - screenPosition.y;
            
            //獲取文本尺寸
            Vector2 stringSize = Style.CalcSize(new GUIContent(text));
            //計(jì)算文本框坐標(biāo)
            Rect rect = new Rect(0f, 0f, stringSize.x + 6, stringSize.y + 4);
            //設(shè)定文本框中心坐標(biāo)
            rect.center = screenPosition - Vector3.up * rect.height * 0.5f;


            //----------------------------------【2.GUI繪制】---------------------------------------------
            //開(kāi)始繪制一個(gè)簡(jiǎn)單的文本框
            Handles.BeginGUI();
            //繪制灰底背景
            GUI.color = new Color(0f, 0f, 0f, 0.8f);
            GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
            //繪制文字
            GUI.color = new Color(1, 1, 1, 0.8f);
            GUI.Label(rect, text, Style);
            //結(jié)束繪制
            Handles.EndGUI();
        }
    }

    //-------------------------------------【OnDrawGizmos()函數(shù)】---------------------------------------------
    // 說(shuō)明:場(chǎng)景編輯器中GUI的顯示
    //------------------------------------------------------------------------------------------------------------------
    void OnDrawGizmos()
    {
        //ShowInfoInSeneEditor為真時(shí),才進(jìn)行繪制
        if (ShowInfoInSceneEditor)
        {
            //----------------------------------------【1.光線(xiàn)投射判斷&計(jì)算位置坐標(biāo)】----------------------------------
            //定義一條射線(xiàn)
            Ray ray = new Ray(transform.position + Camera.current.transform.up * 6f, -Camera.current.transform.up);
            //定義光線(xiàn)投射碰撞
            RaycastHit raycastHit;
            //進(jìn)行光線(xiàn)投射操作,第一個(gè)參數(shù)為光線(xiàn)的開(kāi)始點(diǎn)和方向捏鱼,第二個(gè)參數(shù)為光線(xiàn)碰撞器碰到哪里的輸出信息恢口,第三個(gè)參數(shù)為光線(xiàn)的長(zhǎng)度
            collider.Raycast(ray, out raycastHit, Mathf.Infinity);
            
            //計(jì)算距離,為當(dāng)前攝像機(jī)位置減去碰撞位置的長(zhǎng)度
            float distance = (Camera.current.transform.position - raycastHit.point).magnitude;
            //設(shè)置字體大小穷躁,在26到12之間插值
            float fontSize = Mathf.Lerp(26, 12, distance / 10f);
            //將得到的字體大小賦給Style.fontSize
            Style.fontSize = (int)fontSize;
            //將文字位置取為得到的光線(xiàn)碰撞位置上方一點(diǎn)
            Vector3 worldPositon = raycastHit.point + Camera.current.transform.up * distance * 0.03f;
            //世界坐標(biāo)轉(zhuǎn)屏幕坐標(biāo)
            Vector3 screenPosition = Camera.current.WorldToScreenPoint(worldPositon);
            //z坐標(biāo)值的判斷,z值小于零就返回
            if (screenPosition.z <= 0) { return; }
            //翻轉(zhuǎn)Y坐標(biāo)值
            screenPosition.y = Screen.height - screenPosition.y;
            
            //獲取文本尺寸
            Vector2 stringSize = Style.CalcSize(new GUIContent(text));
            //計(jì)算文本框坐標(biāo)
            Rect rect = new Rect(0f, 0f, stringSize.x + 6, stringSize.y + 4);
            //設(shè)定文本框中心坐標(biāo)
            rect.center = screenPosition - Vector3.up * rect.height * 0.5f;



            //----------------------------------【2.GUI繪制】---------------------------------------------
            //開(kāi)始繪制一個(gè)簡(jiǎn)單的文本框
            Handles.BeginGUI();
            //繪制灰底背景
            GUI.color = new Color(0f, 0f, 0f, 0.8f);
            GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
            //繪制文字
            GUI.color = new Color(1, 1, 1, 0.8f);
            GUI.Label(rect, text, Style);
            //結(jié)束繪制
            Handles.EndGUI();

        }

    }

}

//預(yù)編譯命令結(jié)束
#endif

這個(gè)腳本的用法倒是很簡(jiǎn)單,在代碼的說(shuō)明部分已經(jīng)詳細(xì)寫(xiě)出问潭,在這里我們?cè)倭谐鲆槐椋?/p>

第一步:在Unity中拖拽此腳本到某物體之上猿诸,或在Inspector中[Add Component]->[淺墨's Toolkit v1.0]->[ShowObjectInfo]
第二步:在Inspector里,ShowObject Info 欄中的TargetCamera參數(shù)中選擇需面向的攝像機(jī),如Main Camera,F(xiàn)irstPerson Controller等
第三步:在text參數(shù)里填需要顯示輸出的文字狡忙。
第四步:完成梳虽。運(yùn)行游戲或在場(chǎng)景編輯器Scene中查看顯示效果。

也就是拖拽ShowObjectInfo腳本或者直接添加組件給需要附加文字的物體灾茁,然后在Inspector中輸入需要顯示的文字窜觉,然后選擇其面對(duì)的攝像機(jī)就可以了。

我們將ShowObjectInfo腳本拖拽給上文中剛剛變得炫酷外形黑科技的Capsule:


那么他在Inspector就多了一個(gè)“ShowObject Info(Script)”組件北专,將該組件的Text項(xiàng)中填上“凹凸紋理+邊緣發(fā)光效果”禀挫,TargetCamera中填上First Person Controller的子物體Main Camera:


最后,得到的效果就是這樣:


五拓颓、總結(jié)语婴、配套資源&最終工程下載

好了,本篇的文章到這里就大概結(jié)束了驶睦。

今天講的內(nèi)容還是非常多的砰左,對(duì)于新接觸Unity的朋友們來(lái)說(shuō)或許還得好好消化消化,而熟悉Unity的朋友應(yīng)該很快就可以看懂场航,或者覺(jué)得淺墨講了一堆廢話(huà)缠导,orz。

這篇文章的內(nèi)容說(shuō)白了就非常簡(jiǎn)單溉痢,也就是新建工程僻造,然后導(dǎo)入三個(gè)淺墨提前準(zhǔn)備好的unitypackage游戲資源,點(diǎn)一點(diǎn)鼠標(biāo)拖動(dòng)拖動(dòng)腳本适室,新建一個(gè)Shader嫡意,寫(xiě)點(diǎn)代碼,再創(chuàng)建一個(gè)Material,Shader賦給這個(gè)Material捣辆,最后創(chuàng)建一個(gè)膠囊狀Capsule蔬螟,Material賦給這個(gè)Capsule,點(diǎn)運(yùn)行查看最終效果汽畴。一切旧巾,就是這么簡(jiǎn)單。:)

本文配套的三個(gè)unitypackage打包請(qǐng)點(diǎn)擊此處下載:

【淺墨Unity3D Shader編程】之一 配套的三個(gè)unitypackage打包下載

本文最終的Unity工程請(qǐng)點(diǎn)擊此處下載:

【淺墨Unity3D Shader編程】之一 配套Unity工程

最后放幾張最終的場(chǎng)景美圖吧忍些。

站在亭子上看世界:


逼真的光暈:


漂亮的天空:


亂真的水面:

藍(lán)天和草地樹(shù)木交相輝映:


OK鲁猩,全文到此結(jié)束。
新的游戲編程之旅已經(jīng)開(kāi)啟罢坝,下周一廓握,我們不見(jiàn)不散。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市隙券,隨后出現(xiàn)的幾起案子男应,更是在濱河造成了極大的恐慌,老刑警劉巖娱仔,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沐飘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡牲迫,警方通過(guò)查閱死者的電腦和手機(jī)耐朴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)盹憎,“玉大人筛峭,你說(shuō)我怎么就攤上這事〗畔纾” “怎么了蜒滩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)奶稠。 經(jīng)常有香客問(wèn)我俯艰,道長(zhǎng),這世上最難降的妖魔是什么锌订? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任竹握,我火速辦了婚禮,結(jié)果婚禮上辆飘,老公的妹妹穿的比我還像新娘啦辐。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著颂斜,像睡著了一般畏妖。 火紅的嫁衣襯著肌膚如雪棘利。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼轴总。 笑死,一個(gè)胖子當(dāng)著我的面吹牛博个,可吹牛的內(nèi)容都是我干的怀樟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盆佣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼往堡!你這毒婦竟也來(lái)了械荷?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤投蝉,失蹤者是張志新(化名)和其女友劉穎养葵,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瘩缆,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年佃蚜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了庸娱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谐算,死狀恐怖熟尉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洲脂,我是刑警寧澤斤儿,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站恐锦,受9級(jí)特大地震影響往果,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜一铅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一陕贮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧潘飘,春花似錦肮之、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至艰毒,卻和暖如春筐高,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背现喳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工凯傲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嗦篱。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓冰单,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親灸促。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诫欠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容