目錄如下:
音視頻基礎(chǔ)技術(shù)棧
? 抖音/快手等短視頻APP的風(fēng)靡盯孙,讓音視頻成為當(dāng)下最火熱的技術(shù)锋勺,越來越多的人想要進(jìn)入到這個(gè)領(lǐng)域植捎,我自己也是從圖形方向剛剛踏入這領(lǐng)域不久,音視頻方向所包含的技術(shù)棧非常復(fù)雜喉前,我自己也在一點(diǎn)一點(diǎn)慢慢鉆研没酣,這里面每一個(gè)方向都值得深入研究,而且隨著5G時(shí)代的到來卵迂,音視頻方向的應(yīng)用會(huì)更加廣泛裕便,所以希望自己能掌握更多的關(guān)于音視頻方向的技能,未來可以探索更多的音視頻玩法见咒。然后這篇博客主要是想梳理一下我自己關(guān)于音視頻這個(gè)方向的學(xué)習(xí)路線偿衰,分享出來的同時(shí)也能鼓勵(lì)自己朝著這個(gè)方向繼續(xù)深耕下去。
? 關(guān)于音視頻方向的基礎(chǔ)技能分支改览,先來看一張圖(圖片來自網(wǎng)上)
采集:音視頻數(shù)據(jù)來源下翎,比如Android Camera數(shù)據(jù)采集
渲染:將采集得到的數(shù)據(jù)展示到Surface上宝当,并添加一些圖形效果
處理:對(duì)源數(shù)據(jù)的加工视事,比如添加濾鏡、特效庆揩,還有多視頻剪輯俐东、變速、轉(zhuǎn)場(chǎng)等等
編解碼:對(duì)音視頻數(shù)據(jù)進(jìn)行壓縮封裝订晌,減少數(shù)據(jù)量虏辫,方便傳輸
傳輸:對(duì)采集加工完成的數(shù)據(jù)傳輸至客戶端,比如直播推流锈拨、拉流
1. 關(guān)于音視頻數(shù)據(jù)采集(Android)
? 因?yàn)樽约褐饕菍?duì)Android Camera比較熟悉乒裆,所以我主要梳理一下這方面的知識(shí)點(diǎn),我會(huì)從最基礎(chǔ)的Android Camera API的接口以及基本流程開始梳理推励,后面進(jìn)階部分主要是結(jié)合Camera HAL高通架構(gòu)進(jìn)一步詳細(xì)梳理底層的Camera原理鹤耍,最后是我對(duì)于Camera專業(yè)視頻方向的一些探索。
1.1 Android Camera API
? Android 5.0之后Camera接口升級(jí)成API2验辞,因?yàn)镃amera API 1接口過于簡(jiǎn)單稿黄,根本體現(xiàn)不出硬件能力,用戶能控制的不多跌造,比如拿不到RAW數(shù)據(jù)杆怕,控制不了相機(jī)參數(shù)的下發(fā)等等,如下代碼看下他內(nèi)部的基本接口
? Camera API 1
try {
mCamera = Camera.open(mCurrentCamera);
Camera.Parameters params = mCamera.getParameters();
List<Camera.Size> previewSizes = params.getSupportedPreviewSizes();
Camera.Size preViewSize = previewSizes.get(previewSizes.size() > 4 ? previewSizes.size() - 4 : 0);
params.setPreviewSize(preViewSize.width, preViewSize.height);
mPreviewHeight = preViewSize.height;
mPreviewWidth = preViewSize.width;
Log.e(TAG, "preViewSize->width: " + preViewSize.width + ", preViewSize->height: " + preViewSize.height);
params.setPictureFormat(ImageFormat.JPEG);
params.setJpegQuality(100);
//是否開啟閃光
List<String> flashModes = params.getSupportedFlashModes();
if (flashModes != null && flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) {
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
}
mCamera.setPreviewCallback(this);
mCamera.setDisplayOrientation(PORTRAIT_MODE);
mCamera.setParameters(params);
if(!Consts.SHOW_CAMERA) {
LogUtils.log_main_step("相機(jī)開始preview");
mCamera.startPreview();
}
LogUtils.logd(TAG, "camera init finish.....");
} catch (Exception e) {
LogUtils.loge(TAG, e.getMessage());
e.printStackTrace();
}
然后在CameraPreview callback里面就可以處理相機(jī)數(shù)據(jù)了
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Camera.CameraInfo mCameraInfo = new Camera.CameraInfo();
//如果使用前置攝像頭壳贪,請(qǐng)注意顯示的圖像與幀圖像左右對(duì)稱陵珍,需處理坐標(biāo)
boolean frontCamera = (mCurrentCamera == Camera.CameraInfo.CAMERA_FACING_FRONT);
//獲取重力傳感器返回的方向
int dir = getDirection();
int rotate = (dir ^ 1);
//Log.e("stRotateCamera ", (dir ^ 1) + " rotate result");
//在使用后置攝像頭,且傳感器方向?yàn)?或2時(shí)违施,后置攝像頭與前置orentation相反
if (!frontCamera && dir == 0) {
dir = 2;
} else if (!frontCamera && dir == 2) {
dir = 0;
}
dir = (dir ^ 2);
//.....
process(data)
}
Camera 2.0: New computing platforms for computational photography
? Camera API2對(duì)比API 1改動(dòng)非常大互纯,主要配合HAL3進(jìn)行使用,功能和接口都更加齊全磕蒲,同時(shí)使用起來也會(huì)更加復(fù)雜留潦,但如果熟悉之后只盹,也能拍攝出更加豐富的效果。下圖關(guān)于Camera API 2的幾個(gè)使用場(chǎng)景
? 然后結(jié)合具體代碼講幾個(gè)Camera2的主要接口
- CameraManager
關(guān)于硬件能力的統(tǒng)一封裝接口
CameraManager cameraManager = (CameraManager)mContext.getSystemService(Context.CAMERA_SERVICE);
//可以拿到所以的相機(jī)列表兔院,比如Wide殖卑,Tele,Macro坊萝,Tele2x孵稽,Tele4x等等
String[] cameraIdList = cameraManager.getCameraIdList();
//根據(jù)Camera ID拿到對(duì)應(yīng)設(shè)備支持的能力
CameraCharacteristics cameraCharacteristics =
cameraManager.getCameraCharacteristics(cameraIdStr);
//比如用這個(gè)去判斷支持的最小Focus distance
Float focusDistance = mCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);
? 現(xiàn)如今機(jī)型的攝像頭的數(shù)量越來越多,組合也越來越豐富十偶,不同的Camera負(fù)責(zé)不同的能力肛冶,比如我們需要更大的拍攝范圍會(huì)選擇Ultra Wide,比如小米一億像素的Wide扯键,還有Macro等等睦袖。
- CaptureDevice
我們可以在OpenCamera Callback拿到CameraDevice
String cameraIdStr = String.valueOf(mCameraId);
cameraManager.openCamera(cameraIdStr, mCameraStateCallback, mMainHandler);
//
private CameraDevice.StateCallback mCameraStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
synchronized (SnapCamera.this) {
mCameraDevice = camera;
}
if (mStatusListener != null) {
mStatusListener.onCameraOpened();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
Log.w(TAG, "onDisconnected");
// fail-safe: make sure resources get released
release();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
Log.e(TAG, "onError: " + error);
// fail-safe: make sure resources get released
release();
}
};
- CaptureRequest
? 通過CameraDevice可以創(chuàng)建CaptureRequest,類型主要有以下幾種荣刑,1-6分布用于預(yù)覽馅笙、拍照、錄制厉亏、錄制中拍照董习、ZSL、手動(dòng)爱只。
public static final int TEMPLATE_MANUAL = 6;
public static final int TEMPLATE_PREVIEW = 1;
public static final int TEMPLATE_RECORD = 3;
public static final int TEMPLATE_STILL_CAPTURE = 2;
public static final int TEMPLATE_VIDEO_SNAPSHOT = 4;
public static final int TEMPLATE_ZERO_SHUTTER_LAG = 5;
//創(chuàng)建的代碼
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//這里是更加專業(yè)一點(diǎn)的用法酬姆,可以不看寞肖,就是拿到RequestBuilder之后我們可以去下發(fā)各種TAG渔彰,如下設(shè)置AE/AWB鎖
CaptureRequestBuilder.applyAELock(request, mConfigs.isAELocked());
CaptureRequestBuilder.applyAWBLock(request, mConfigs.isAWBLocked());
- Surface
Surface主要是用來接相機(jī)返回的數(shù)據(jù)吏够,可以支持不同的數(shù)據(jù)類型和分辨率
//SurfaceTexture的方式,用于預(yù)覽
mSurfaceTexture = new SurfaceTexture(false);
mSurfaceTexture.setDefaultBufferSize(optimalSize.width, optimalSize.height);
mPreviewSurface = new Surface(mSurfaceTexture);
//ImageReader 的方式, Format可以是YUV训柴、RAW哑舒,DEPTH等
mPhotoImageReader = ImageReader.newInstance(size.getWidth(), size.getHeight(),
ImageFormat.JPEG, /* maxImages */ 2);
mPhotoImageReader.setOnImageAvailableListener(mPhotoAvailableListener, mCameraHandler);
//創(chuàng)建session
List<Surface> surfaces = Arrays.asList(mPreviewSurface, mPhotoImageReader.getSurface());
mCameraDevice.createCaptureSession(surfaces, mSessionCallback, mCameraHandler);
- CaptureSession
通過上面的CameraDevice創(chuàng)建Session,在Callback里面拿到Session
mCameraDevice.createCaptureSession(surfaces, mSessionCallback, mCameraHandler);
private CameraCaptureSession.StateCallback
mSessionCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
synchronized (SnapCamera.this) {
if (mCameraDevice == null) {
Log.e(TAG, "onConfigured: CameraDevice was already closed.");
session.close();
return;
}
mCaptureSession = session;
}
startPreview();
capture();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Log.e(TAG, "sessionCb: onConfigureFailed");
}
};
? 后面所有的采集工作都是基于Session幻馁,關(guān)于Session機(jī)制最早是諾基亞提出來的洗鸵,后面蘋果的IOS也采取Session作為相機(jī)拍照,錄制的會(huì)話單元仗嗦。
/*發(fā)起請(qǐng)求膘滨,后續(xù)相機(jī)開始往surface輸出數(shù)據(jù),這個(gè)可以在onConfigured里面調(diào)用稀拐,如上面的代碼 startPreview();
capture();*/
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
mCaptureCallback, mCameraHandler);
mCaptureSession.capture(requestBuilder.build(), mCaptureCallback, mCameraHandler);
// 這個(gè)是針對(duì)120/240/960Fps recording
mCaptureSession.setRepeatingBurst(requestList, mCaptureCallback, mCameraHandler);
mCaptureSession.captureBurst(requestList, listener, handler);
然后還可以通過Session控制結(jié)束動(dòng)作
mCaptureSession.abortCaptures();
mCaptureSession.stopRepeating();
? 對(duì)象更加專業(yè)一點(diǎn)的系統(tǒng)相機(jī)而言火邓,在setRepeatingRequest之前可以還需要做很多工作,比如拍照要等Focus finish,還需要Apply一些列參數(shù)贡翘,比如FocusMode, Zoom, AE/F Lock, Video FPS.....
總結(jié):上面只是一個(gè)大概流程,至于每個(gè)流程你需要去控制什么可能就會(huì)更加復(fù)雜砰逻,但其實(shí)對(duì)于第三方APP鸣驱,需要控制的不多,基本就是OpenCamera -> 配置Surface -> 創(chuàng)建Session -> 設(shè)置參數(shù) -> RepeatingRequest ->ImageReader或者GLSurafce回調(diào)接數(shù)據(jù)做后續(xù)處理蝠咆∮欢可能有些更加專業(yè)一點(diǎn)的相機(jī)應(yīng)用,可能會(huì)涉及的一些專業(yè)參數(shù)的下發(fā)刚操,比如IOS, FocusDistance, Shutter Time, EV....
1.2 Camera HAL
Android Camera硬件抽象層(HAL闸翅,Hardware Abstraction Layer)主要用于把底層camera drive與硬件和位于android.hardware中的framework APIs連接起來。Camera子系統(tǒng)主要包含了camera pipeline components 的各種實(shí)現(xiàn)菊霜,而camera HAL提供了這些組件的使用接口
官網(wǎng)的系統(tǒng)架構(gòu)圖:
? 做相機(jī)開發(fā)除了第一部分講到的APP層面的相機(jī)控制坚冀,然后就是HAL層的開發(fā)了,HAL層由芯片廠商定制(比如高通等),手機(jī)廠商可以再其上增加一些內(nèi)容鉴逞。Camera HAL 經(jīng)歷1-3的版本迭代记某,不同的硬件支持的Camera2程度不一樣,主要有以下等級(jí)
//每一個(gè)等級(jí)啥意思自己搜吧
INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
INFO_SUPPORTED_HARDWARE_LEVEL_FULL
INFO_SUPPORTED_HARDWARE_LEVEL_3
INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
想了一下這一塊比較復(fù)雜构捡,我自己也不是非常的清楚液南,而且好像第三方APP一般不會(huì)涉及,后面有機(jī)會(huì)單獨(dú)整理吧(挖坑1)
1.3 CameraX
? 如上看到的Camera API2非常復(fù)雜勾徽,為了簡(jiǎn)化流程滑凉,Google推出CameraX,借助 CameraX喘帚,開發(fā)者只需兩行代碼就能利用與預(yù)安裝的相機(jī)應(yīng)用相同的相機(jī)體驗(yàn)和功能畅姊。 CameraX Extensions 是可選插件,通過該插件吹由,您可以在支持的設(shè)備上向自己的應(yīng)用中添加人像涡匀、HDR、夜間模式和美顏等效果溉知。如下預(yù)覽和拍照的代碼:
//預(yù)覽
PreviewConfig config = new PreviewConfig.Builder().build();
Preview preview = new Preview(config);
preview.setOnPreviewOutputUpdateListener(
new Preview.OnPreviewOutputUpdateListener() {
@Override
public void onUpdated(Preview.PreviewOutput previewOutput) {
// Your code here. For example, use previewOutput.getSurfaceTexture()
// and post to a GL renderer.
};
});
CameraX.bindToLifecycle((LifecycleOwner) this, preview);
//拍照
ImageCaptureConfig config =
new ImageCaptureConfig.Builder()
.setTargetRotation(getWindowManager().getDefaultDisplay().getRotation())
.build();
ImagesCapture imageCapture = new ImageCapture(config);
CameraX.bindToLifecycle((LifecycleOwner) this, imageCapture, imageAnalysis, preview);
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
metadata.isReversedHorizontal = mCameraLensFacing == LensFacing.FRONT;
imageCapture .takePicture(saveLocation, metadata, executor, OnImageSavedListener);
1.4 Camera專業(yè)視頻(進(jìn)階)
? 現(xiàn)如今手機(jī)相機(jī)功能越來越強(qiáng)大陨瘩,現(xiàn)在已經(jīng)基本可以取代卡片機(jī),手機(jī)相機(jī)已經(jīng)作為手機(jī)廠商最重要的一個(gè)賣點(diǎn)级乍,尤其是DXO刷榜的行為舌劳,之后讓廠商投入更多的研發(fā)相機(jī)上面,在可以預(yù)見的未來玫荣,手機(jī)相機(jī)發(fā)展勢(shì)必會(huì)更加激進(jìn)甚淡,后面進(jìn)一步取代部分單反也是有可能的,所以相機(jī)未來應(yīng)該更多的體現(xiàn)起作為生產(chǎn)力工具的一部分捅厂,尤其是現(xiàn)在視頻作為人們記錄生活的方式贯卦,越來越得到普及资柔。
? 如果說到視頻和相機(jī),我的想法是如何去拍攝更加專業(yè)的視頻撵割,借助手機(jī)相機(jī)強(qiáng)大的功能贿堰,能否讓一部分專業(yè)人士用他作為生產(chǎn)力工具,部分取代非常不便攜的單反設(shè)備啡彬,比如借助Camera2參數(shù)調(diào)節(jié)功能去實(shí)現(xiàn)長(zhǎng)曝光羹与、延時(shí)、流光快門等效果庶灿。
? 相機(jī)更加專業(yè)的方向纵搁,主要體現(xiàn)手機(jī)廠商的系統(tǒng)相機(jī),尤其是Android端往踢,很多硬件能力只能由手機(jī)廠商自己控制腾誉,比如系統(tǒng)相機(jī)里面的專業(yè)模式,還有就是類似大疆這種峻呕,也是自己做相機(jī)硬件妄辩,然后能夠結(jié)合硬件能力,去實(shí)現(xiàn)一些更加專業(yè)的拍攝效果山上。目前軟件這塊做的比較專業(yè)的就是Fimic Pro眼耀。
?
? 支持各種調(diào)參:ISO ,Exposure time佩憾,WB哮伟,EV,F(xiàn)ocus distance
? 支持平滑變焦妄帘,光焦分離
? 支持曲線調(diào)整:亮度楞黄、飽和度、陰影抡驼、Gamma曲線等
? 支持LOG格式鬼廓,還有專業(yè)的音頻采集
? 有非常多的輔助信息,比如峰值對(duì)焦致盟、曝光反饋碎税、RGB直方圖信息等
? 音視頻各種格式自定義:畫幅比例、FPS馏锡、分辨率雷蹂、音頻采樣率、編碼格式等等
?
真的是一個(gè)非常強(qiáng)大的生產(chǎn)力工具
2. 關(guān)于圖形渲染
? 音視頻第二部分肯定是圖形渲染方向啦杯道,因?yàn)橹耙恢庇凶鰣D形渲染方面的工作匪煌,也寫過自己的渲染引擎,鏈接如下
? https://github.com/LukiYLS/SimpleRenderer
以及關(guān)于這個(gè)引擎的介紹
? 所以可以大概分享一下自己的學(xué)習(xí)過程,以及關(guān)于Android GLES相關(guān)總結(jié)
2.1 圖形學(xué)基礎(chǔ)
? 我覺圖形方面基礎(chǔ)的應(yīng)該需要掌握如下:
- 整個(gè)圖形矩陣變換過程
World Matrix:將場(chǎng)景中所有對(duì)象統(tǒng)一到一個(gè)坐標(biāo)系下
ViewMatrix:World Matrix 變換的相機(jī)坐標(biāo)系萎庭,根據(jù)相機(jī)的三個(gè)參數(shù)生成
ProjectionMatrix:3D世界投影的2D平面
NDC:轉(zhuǎn)換到[-1 1]
Screen Matrix:轉(zhuǎn)換的屏幕坐標(biāo)系
這部分想要理解就要自己用筆手推一遍霜医,非常管用
- OpenGL API
熟悉API,比如紋理貼圖方式驳规,繪制點(diǎn)線面肴敛,VAO/VBO創(chuàng)建等等
- 光照
首先需要理解傳統(tǒng)的Phong光照,然后要看PBR达舒,理解BRDF模型和公式推導(dǎo)
還有就是Shadow這快值朋,理解shadowmap的原理叹侄,不同的光照怎樣生成深度圖巩搏,然后shadow acne,處理邊緣鋸齒等很多細(xì)節(jié)趾代,還有陰影體這塊
- 模版測(cè)試/深度測(cè)試
深度測(cè)試實(shí)現(xiàn)遮擋
模版測(cè)試也是非常有用贯底,比如我有篇博客里面講到的 陰影體結(jié)合模版測(cè)試實(shí)現(xiàn)矢量緊貼地形的效果
- 地形
怎用利用perlin noise生成地形頂點(diǎn)數(shù)據(jù)
LOD的地形:規(guī)則四叉樹劃分(Google Earth地形),以及不規(guī)則的CLOD(自適應(yīng)三角網(wǎng))
- 粒子系統(tǒng)
主要就是怎么控制粒子發(fā)射器撒强,粒子加速的禽捆,粒子運(yùn)動(dòng)軌跡等等
- 場(chǎng)景管理
如何利用四叉樹、八叉樹管理場(chǎng)景節(jié)點(diǎn)飘哨,做視錐體裁剪
........
之前寫的渲染引擎胚想,都包含這些基本模塊,大概持續(xù)完善了半年左右芽隆,對(duì)我圖形渲染方面的提升非常大浊服,包括自己也寫過軟光柵器
總結(jié):
學(xué)習(xí)圖形學(xué)最好的方式就是造輪子造輪子造輪子,自己寫引擎胚吁,自己寫軟光柵器
學(xué)習(xí)資料分享:
https://learnopengl-cn.github.io/
https://www.scratchapixel.com/
閱讀源碼:OGRE/OSG/THREEJS
工具: unity3d processing
2.2 OpenGL/GLES
-
EGL環(huán)境創(chuàng)建
eglGetDisplay //獲取display信息
eglChooseConfig //設(shè)置RGBA bit depth
eglCreatePbufferSurface // 創(chuàng)建離屏surface, 也可以eglCreateWindowSurface
eglCreateContext //創(chuàng)建上下文
GL多線程
-
SharedContext多線程
這個(gè)網(wǎng)上也有很多資料牙躺,eglCreateContext 的時(shí)候傳入其它線程的Context,既可以共享一些GPU Buffer腕扶,比如:MediaCodec錄制的用的Surface孽拷,和預(yù)覽去sharedContext
-
Render pass如何做同步glFenceSync
如果用glFinish可能會(huì)在某個(gè)點(diǎn)等的時(shí)間很長(zhǎng),可以用glFenceSync做同步半抱,非常不錯(cuò)
- 渲染優(yōu)化
? 幀率如何優(yōu)化脓恕,涉及到很多方面,這方面游戲引擎有很多技巧窿侈,比如提前視錐體裁剪进肯,遮擋剔除等等,我這里只是簡(jiǎn)單講一下在不涉及到大場(chǎng)景的優(yōu)化有哪些
個(gè)人關(guān)于效率優(yōu)化的總結(jié):(這里有部分來自組內(nèi)大佬的總結(jié))
? 盡量在渲染之前先做視錐體裁剪工作棉磨,減少不必要的IO
? 盡量少使用一些同步阻塞操作江掩,比如glReadPixel、glFinish、glTeximage2D等
? Shader里面少使用if/for這種操作
? 必要時(shí)做下采樣
? 利用好內(nèi)存對(duì)齊环形,會(huì)有意想不到的效率提升
? 渲染之前做好一些列準(zhǔn)備工作策泣,比如編譯Shader
2.3 音視頻渲染引擎(扒抖音)
? 現(xiàn)在短視頻應(yīng)用關(guān)于特效部分底層都有會(huì)有一套渲染引擎,不管的抖音還是快手抬吟,你把抖音的的APP package pull出來就能看到萨咕,里面是同lua腳本去調(diào)用底層的渲染引擎,lua腳本負(fù)責(zé)下發(fā)一些參數(shù)火本,主要是AI的一些識(shí)別結(jié)果以及用戶的交互事件危队。
? 雖然沒有游戲引擎那么強(qiáng)大,但基本模塊應(yīng)該都會(huì)包含模型钙畔、資源管理茫陆、相機(jī)控制、裁剪擎析、渲染等等簿盅,畢竟寫這套引擎的基本都是以前搞游戲的那撥人,算是降維來寫音視頻渲染引擎揍魂。
? 下面通過拆解抖音內(nèi)部的資源文件桨醋,探究?jī)?nèi)部關(guān)于音視頻渲染引擎的模塊組成,對(duì)比短視頻引擎和游戲引擎的區(qū)別现斋。
一起來看看抖音最近很火的一個(gè)游戲:潛水艇喜最,通過移動(dòng)鼻子控制潛水艇
如下是download 的資源包
? 這里面的核心控制邏輯就是那個(gè)lua基本,里面會(huì)負(fù)責(zé)把人臉關(guān)鍵點(diǎn)信息傳給底層引擎庄蹋,實(shí)時(shí)更新潛水艇的位置瞬内,腳本實(shí)現(xiàn)了兩個(gè)碰撞檢測(cè)函數(shù),用于潛水艇和柱子之間做碰撞檢測(cè)蔓肯。Collision玩過游戲引擎的都很熟悉遂鹊,有些游戲引擎Collision detect做的不好的會(huì)出現(xiàn)穿模的現(xiàn)象。
? rectCollision: 潛水艇中心坐標(biāo)和柱子底部坐標(biāo)之間的距離蔗包,與潛水艇半徑比較秉扑,小于0.9*R,就是碰撞了
? circleCollision: 兩個(gè)圓心坐標(biāo)之間的距離和他們各自半徑之和比較调限,小于半徑和舟陆,就是碰撞了
local intpx = 220 --柱子左右間隔
local intpy = 550 --柱子中間縫隙寬度
local range_y = {0.3, 0.7} --柱子縫隙中央隨機(jī)范圍,{0.3,0.7}代表縫隙可能在屏幕高度30%-70%的位置隨機(jī)出現(xiàn)
local sp = {0.5,1.0} --速度初始和最終速度耻矮,{0.5,1.0}代表初始速度為1秒走過0.5個(gè)屏幕秦躯,最終速度為1秒走過1個(gè)屏幕
local ptime = 3 --準(zhǔn)備時(shí)間3s
local range_s = {5,10} --分?jǐn)?shù)分段,{5,10}代表0-5第一段裆装,6-10第二段踱承,10以上第三段倡缠;10以上未碰撞第四段
local smul = 0.8 --字號(hào)倍率
//.....此次省略N行
//兩個(gè)碰撞檢測(cè)函數(shù)
//u=1,2 代碼上面和下面柱子
local function rectCollision(i,u)
local K_s_x = sub.x
local K_s_y = sub.y * ratio
local K_rect_x = pillar[i].x
local K_rect_y = 0
local l = K_rect_x - r
local r = K_rect_x + r
local t = 0
local b = 0
if u == 1 then
t = -1
b = (pillar[i].y - interval_y / 2) * ratio
else
t = (pillar[i].y + interval_y / 2) * ratio
b = 2
end
local closestP_x = 0.0
local closestP_y = 0.0
closestP_x = clamp(K_s_x, l , r)
closestP_y = clamp(K_s_y, t , b)
local dist = distance(closestP_x, closestP_y, K_s_x, K_s_y)
if dist <= sub.r * 0.9 then
return true
else
return false
end
return false
end
local function circleCollision(i,u)
local K_s_x = sub.x
local K_s_y = sub.y * ratio
local K_cir_x = pillar[i].x
local K_cir_y = 0
if u == 1 then
K_cir_y = (pillar[i].y - interval_y / 2) * ratio
else
K_cir_y = (pillar[i].y + interval_y / 2) * ratio
end
local dist = distance(K_cir_x, K_cir_y, K_s_x, K_s_y)
if dist <= sub.r * 0.9 + r then
return true
else
return false
end
return false
end
還有兩個(gè)重要的函數(shù),分別處理預(yù)覽和錄制茎活,游戲過程的邏輯昙沦,比如柱子隨著時(shí)間線一直在移動(dòng),speed在加快
handleTimerEvent = function(this, timerId, milliSeconds)
if timerId == timer_ID_Fast and gaming then
if init_state ~= 0 then
return true
end
timeThis = getTime(this)
timeDelta = getDiffTime(timeLast, timeThis)
timeCumu = timeCumu + timeDelta
timeLast = timeThis
local speed = sp[1] + clamp(timeCumu / 14, 0,1) * (sp[2] - sp[1])
if timeCumu > 14 then
gaming = false
Sticker2DV3.playClip(this, feature_0.folder, feature_0.entity[1], feature_0.clip[1], 0)
CommonFunc.setFeatureEnabled(this, feature_t.folder, true)
if score >= 10 then
realTimeFunc.initRealTime(this, feature_t.folder, true, feature_t.realTimeParams[1])
else
realTimeFunc.initRealTime(this, feature_t.folder, true, feature_t.realTimeParams[2])
end
realTimeFunc.setRealTime(this, feature_t.folder, "u_data", score)
end
for i = 1,5 do
pillar[i].x = pillar[i].x - speed * timeDelta
if pillar[i].x < -r then
pillar[i].x = pillar[i].x + interval_x * 5
pillar[i].y = range_y[1] + math.random() * (range_y[2] - range_y[1])
pillar[i].counted = false
end
if pillar[i].x < sub.x and not pillar[i].counted then
pillar[i].counted = true
score = score + 1
end
local cy = pillar[i].y - (interval_y / 2 - r / ratio) - 0.5
local cx = pillar[i].x
set4Vtx(this, feature_3.folder, feature_3.entity[1], feature_3.clip[2*i-1], cx , cy, 2 * r, 1.0 , 0.0, 1.0, ratio, 0.0, 0.0)
cy = pillar[i].y + (interval_y / 2 - r / ratio) + 0.5
//更新實(shí)體坐標(biāo)
set4Vtx(this, feature_3.folder, feature_3.entity[1], feature_3.clip[2*i], cx , cy, 2 * r, 1.0 , 0.0, 1.0, ratio, 0.0, 0.0)
end
if noseY ~= 0 then
sub.y = noseY
end
if (sub.y ~= 0 or lastY ~= 0) then
sub.a = sub.a * 0.8 + (sub.y - lastY) / timeDelta * 0.2
else
sub.a = sub.a * 0.8
end
set4Vtx(this, feature_2.folder, feature_2.entity[1], feature_2.clip[1], sub.x , sub.y, 2 * sub.r, 2 * sub.r / ratio / 1.1 , sub.a, 1.0, ratio, 0.0, 0.0)
//對(duì)所有柱子遍歷做碰撞檢測(cè)
for i = 1,5 do
if rectCollision(i,1) or rectCollision(i,2) or circleCollision(i,1) or circleCollision(i,2) then
gaming = false
if score <= range_s[1] then
Sticker2DV3.playClip(this, feature_0.folder, feature_0.entity[1], feature_0.clip[4], 0)
elseif score <= range_s[2] then
Sticker2DV3.playClip(this, feature_0.folder, feature_0.entity[1], feature_0.clip[3], 0)
else
Sticker2DV3.playClip(this, feature_0.folder, feature_0.entity[1], feature_0.clip[2], 0)
end
CommonFunc.setFeatureEnabled(this, feature_t.folder, true)
if score >= 10 then
realTimeFunc.initRealTime(this, feature_t.folder, true, feature_t.realTimeParams[1])
else
realTimeFunc.initRealTime(this, feature_t.folder, true, feature_t.realTimeParams[2])
end
realTimeFunc.setRealTime(this, feature_t.folder, "u_data", score)
end
end
lastY = sub.y
end
return true
end,
handleRecodeVedioEvent = function (this, eventCode)
if (init_state ~= 0) then
return true
end
if (eventCode == 1) then
timeLast = getTime(this)
timeCumu = 0
score = 0
gaming = true
pillar =
{
{
x = start_x,
y = range_y[1] + math.random() * (range_y[2] - range_y[1]),
counted = false
},
{
x = start_x + interval_x,
y = range_y[1] + math.random() * (range_y[2] - range_y[1]),
counted = false
},
{
x = start_x + interval_x * 2,
y = range_y[1] + math.random() * (range_y[2] - range_y[1]),
counted = false
},
{
x = start_x + interval_x * 3,
y = range_y[1] + math.random() * (range_y[2] - range_y[1]),
counted = false
},
{
x = start_x + interval_x * 4,
y = range_y[1] + math.random() * (range_y[2] - range_y[1]),
counted = false
},
}
sub =
{
r = 0.105,
x = 0.24,
y = 0.5,
sy = 0.0,
a = 0
}
lastY = noseY
for i = 1,10 do
Sticker2DV3.playClip(this, feature_3.folder, feature_3.entity[1], feature_3.clip[i], 0)
set4Vtx(this, feature_3.folder, feature_3.entity[1], feature_3.clip[i], -2 , -2, 0, 0 , 0.0, 0.0, ratio, 0.0, 0.0)
end
for i = 1,4 do
Sticker2DV3.stopClip(this, feature_0.folder, feature_0.entity[1], feature_0.clip[i])
end
set4Vtx(this, feature_2.folder, feature_2.entity[1], feature_2.clip[1], -2 , -2, 0, 0 , 0, 0, ratio, 0.0, 0.0)
CommonFunc.setFeatureEnabled(this, feature_t.folder, false)
end
return true
end,
}
? 然后有幾個(gè)stcker文件载荔,可以看到有一個(gè)是柱子的entity信息盾饮,有一個(gè)是潛水艇的entity信息,后面那個(gè)游戲失敗時(shí)彈出的那個(gè)框框模型懒熙。然后每一個(gè)stcker文件夾都有兩個(gè)json文件丘损,其中主要是clip.json,里面主要包含實(shí)體的基本信息工扎,還有transform參數(shù)徘钥。
"clipname1": {
"alphaFactor": 1.0,
"blendmode": 0,
"fps": 16,
"height": 801,
"textureIdx": {
"idx": [
0
],
"type": "image"
},
"transformParams": {
"position": {
"point0": {
"anchor": [
0.0,
0.5
],
"point": [
{
"idx": "topright",
"relationRef": 0,
"relationType": "foreground",
"weight": 0.5635420937542709
},
{
"idx": "bottomleft",
"relationRef": 0,
"relationType": "foreground",
"weight": -0.0793309886223863
}
]
},
"point1": {
"anchor": [
1.0,
0.5
],
"point": [
{
"idx": "topright",
"relationRef": 0,
"relationType": "foreground",
"weight": 0.7854868849874516
},
{
"idx": "bottomleft",
"relationRef": 0,
"relationType": "foreground",
"weight": -0.0793309886223863
}
]
}
},
"relation": {
"foreground": 1
},
"relationIndex": [
0
],
"relationRefOrder": 0,
"rotationtype": 1,
"scale": {
"scaleY": {
"factor": 1.0
}
}
},
....
總結(jié):
? 從effect資源可以看出,抖音底層有一個(gè)類似游戲引擎的這樣的渲染框架定庵,然后通過腳本進(jìn)行控制吏饿。功能也應(yīng)該是挺齊全的踪危,可能是縮小版的游戲引擎蔬浙,就是麻雀雖小,五臟俱全贞远。
? 對(duì)于熟悉游戲引擎的人來說畴博,這部分應(yīng)該不難,基本都是那些
? 另外蓝仲,如果想學(xué)習(xí)引擎這快俱病,我的思路是多去看看一些流行的開源引擎,一開始模仿別人怎么寫袱结,然后不斷的思考總結(jié)亮隙,慢慢的你就知道一個(gè)引擎應(yīng)該包含哪些,以及怎么控制各個(gè)模塊之間的交互垢夹。
? 比如我之前深入研究過OGRE溢吻,看過OSG,以及深入研究過THREEJS果元,同時(shí)我自己寫的渲染引擎促王,有把這幾個(gè)引擎比較好的模塊模仿過來,逐漸內(nèi)化成自己的引擎而晒。
? 后面有機(jī)會(huì)把拆解一下抖音比較復(fù)雜的特效吧蝇狼,最好找一個(gè)和AI或者AR結(jié)合的特效(挖坑2)
3. 關(guān)于音視頻圖形部分
? 這里主要會(huì)大概整理一下比較基礎(chǔ)的幾個(gè)部分,包括濾鏡倡怎、美顏迅耘、特效贱枣、轉(zhuǎn)場(chǎng)這些,因?yàn)槲夷壳昂孟裰蛔鲞^這些基礎(chǔ)的玩法颤专,更加復(fù)雜的就涉及到渲染引擎冯事,還有AI圖像方面,后面會(huì)繼續(xù)學(xué)習(xí)研究
3.1 濾鏡篇
現(xiàn)在的濾鏡主要還是查找表的形式包括1D/3D LUT血公,可能有些也會(huì)用AI去做昵仅,比如風(fēng)格化遷移等
1D LUT:
調(diào)節(jié)亮度,對(duì)比度累魔,黑白等級(jí)256 x 1摔笤,只影響Gamma曲線
RGB曲線調(diào)節(jié)256 x 3
Instagram 里面有很多1D的LUT應(yīng)用,比如amaro, lomo, Hudson, Sierra.....
void main() {
vec2 uv = gl_FragCoord.st/u_resolution;
uv.y = 1.0 - uv.y;
vec4 originColor = texture2D(u_texture, uv);
vec4 texel = texture2D(u_texture, uv);
vec3 bbTexel = texture2D(u_blowoutTex, uv).rgb;
//256x1
texel.r = texture2D(u_overlayTex, vec2(bbTexel.r, texel.r)).r;
texel.g = texture2D(u_overlayTex, vec2(bbTexel.g, texel.g)).g;
texel.b = clamp(texture2D(u_overlayTex, vec2(bbTexel.b, texel.b)).b, 0.1, 0.9);
//256x3
vec4 mapped;
mapped.r = texture2D(u_mapTex, vec2(texel.r, .25)).r;
mapped.g = texture2D(u_mapTex, vec2(texel.g, .5)).g;
mapped.b = texture2D(u_mapTex, vec2(texel.b, 0.1)).b;
mapped.a = 1.0;
mapped.rgb = mix(originColor.rgb, mapped.rgb, 1.0);
gl_FragColor = mapped;
}
Amaro 風(fēng)格
Lomo 風(fēng)格
......
3D LUT:
Lookup table : 64x64 512x512
//這沒啥說的垦写,都是很基本的
void main() {
vec2 uv = gl_FragCoord.st/u_resolution;
uv.y = 1.0 - uv.y;
lowp vec3 textureColor = texture2D(u_texture, uv).rgb;
textureColor = clamp((textureColor - vec3(u_levelBlack, u_levelBlack, u_levelBlack)) * u_levelRangeInv, 0.0, 1.0);
textureColor.r = texture2D(u_grayTexture, vec2(textureColor.r, 0.5)).r;
textureColor.g = texture2D(u_grayTexture, vec2(textureColor.g, 0.5)).g;
textureColor.b = texture2D(u_grayTexture, vec2(textureColor.b, 0.5)).b;
mediump float blueColor = textureColor.b * 15.0;
mediump vec2 quad1;
quad1.y = floor(blueColor / 4.0);
quad1.x = floor(blueColor) - (quad1.y * 4.0);
mediump vec2 quad2;
quad2.y = floor(ceil(blueColor) / 4.0);
quad2.x = ceil(blueColor) - (quad2.y * 4.0);
highp vec2 texPos1;
texPos1.x = (quad1.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
texPos1.y = (quad1.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);
highp vec2 texPos2;
texPos2.x = (quad2.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
texPos2.y = (quad2.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);
lowp vec4 newColor1 = texture2D(u_lookupTexture, texPos1);
lowp vec4 newColor2 = texture2D(u_lookupTexture, texPos2);
lowp vec3 newColor = mix(newColor1.rgb, newColor2.rgb, fract(blueColor));
textureColor = mix(textureColor, newColor, u_strength);
gl_FragColor = vec4(textureColor, 1.0);
}
Fairytale風(fēng)格
? 可以看到3D LUT看起來更加和諧一點(diǎn)吕世,因?yàn)?D LUT 的RGB映射關(guān)系是關(guān)聯(lián)在一起,1D LUT則是獨(dú)立的梯投,不過1D LUT可以節(jié)省空間命辖,有些只需要單通道就可以搞定的,就可以用1D LUT
? LUT得益于其簡(jiǎn)單的生產(chǎn)流程分蓖,基本設(shè)計(jì)師那邊用PhotoShop調(diào)好一張查找表尔艇,這邊就應(yīng)用上去就OK了
相關(guān)資料:
https://affinityspotlight.com/article/1d-vs-3d-luts/
https://zhuanlan.zhihu.com/p/37147849
https://zhuanlan.zhihu.com/p/60702944
3.2 美顏篇
-
磨皮去燥
最簡(jiǎn)單的方式,就是利用各種降噪濾波器么鹤,去掉高頻噪聲部分
均值模糊 權(quán)重分布平均
高斯模糊 權(quán)重高斯分布
表面模糊 權(quán)重分布只跟顏色空間有關(guān)系(與膚色檢測(cè)配合)
雙邊濾波 權(quán)重分布跟距離和顏色空間分布有關(guān)系终娃,
中值模糊 卷積核范圍內(nèi)去中值
導(dǎo)向?yàn)V波 參考圖像
vec4 BilateralFilter(vec2 uv) { float i = uv.x; float j = uv.y; float sigmaSSquare = 2.0 * SigmaS * SigmaS; float sigmaRSquare = 2.0 * SigmaR * SigmaR; vec3 centerColor = texture2D(u_texture, uv).rgb; float centerGray = Luminance(centerColor); vec3 sum_up, sum_down; for(int k = -u_radius; k <= u_radius; k++) { for(int l = -u_radius; l <= u_radius; l++) { vec2 uv_new = uv + vec2(k, l)/u_resolution; vec3 curColor = texture2D(u_texture, uv_new).rgb; float curGray = Luminance(curColor); vec3 deltaColor = curyolor - centerColor; float len = dot(deltaColor, deltaColor); float exponent = -((i-k)*(i-k)+(j-l)*(j-l))/sigmaSSquare - len/sigmaRSquare; float weight = exp(exponent); sum_up += curColor * weight; sum_down += weight; } } vec3 color = sum_up / sum_down; return vec4(color, 1.0); } vec4 SurfaceFilter(vec2 uv) { vec3 centerColor = texture2D(u_texture, uv).rgb; vec3 sum_up, sum_down; for(int k = -u_radius; k <= u_radius; k++) { for(int l = -u_radius; l <= u_radius; l++) { vec2 uv_new = uv + vec2(k, l)/u_resolution; vec3 curColor = texture2D(u_texture, uv_new).rgb; vec3 weight = CalculateWeight(curColor, centerColor); sum_up += weight * curColor; sum_down += weight; } } return vec4(sum_up / sum_down, 1.0); } vec4 gaussian(vec2 uv, bool horizontalPass) { float numBlurPixelsPerSide = float(blurSize / 2); vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0); //高斯函數(shù) vec3 incrementalGaussian; incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma); incrementalGaussian.y = exp(-0.5 / (sigma * sigma)); incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y; vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0); float coefficientSum = 0.0; avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x; coefficientSum += incrementalGaussian.x; incrementalGaussian.xy *= incrementalGaussian.yz; for (float i = 1.0; i <= numBlurPixelsPerSide; i++) { avgValue += texture2D(texture, vertTexCoord.st - i * texOffset * blurMultiplyVec) * incrementalGaussian.x; avgValue += texture2D(texture, vertTexCoord.st + i * texOffset * blurMultiplyVec) * incrementalGaussian.x; coefficientSum += 2.0 * incrementalGaussian.x; incrementalGaussian.xy *= incrementalGaussian.yz; } return avgValue / coefficientSum;
比如雙邊濾波的效果
高斯效果,分開縱橫兩個(gè)pass處理蒸甜,復(fù)雜度從WxHx(2R +1)x(2R +1) 降至 WxHx(2R + 1)
suface blur 配合膚色檢測(cè)棠耕,可以看出邊緣部分有點(diǎn)硬,
- 美白
HighPass高亮加一點(diǎn)紅暈
- 美型
美型主要是需要和人臉檢測(cè)結(jié)合起來柠新,對(duì)人臉各個(gè)部位進(jìn)行微調(diào)窍荧,比如:
- 廋臉,根據(jù)AI找到的固定臉型的那幾個(gè)關(guān)鍵點(diǎn)恨憎,在各自方向做曲線變形處理
- 大眼蕊退,找到中心點(diǎn),放大一定的半徑
- 下巴框咙,也是做曲線變形處理
類似如下的曲線變形處理
// 曲線形變處理
vec2 curveWarp(vec2 textureCoord, vec2 originPosition, vec2 targetPosition, float radius)
{
vec2 offset = vec2(0.0);
vec2 result = vec2(0.0);
vec2 direction = targetPosition - originPosition;
float infect = distance(textureCoord, originPosition)/radius;
infect = 1.0 - infect;
infect = clamp(infect, 0.0, 1.0);
offset = direction * infect;
result = textureCoord - offset;
return result;
}
......
? 這一塊主要是需要準(zhǔn)確的關(guān)鍵點(diǎn)處理咕痛,然后就是怎么調(diào)整曲線了,可以自己注冊(cè)一個(gè)Face++的FaceDetect自己玩一下喇嘱,包括怎么和貼紙配合茉贡,這一塊還在總結(jié)中,后續(xù)會(huì)自己研究(挖坑3)
3.3 轉(zhuǎn)場(chǎng)篇
? 轉(zhuǎn)場(chǎng)主要是涉及到兩段視頻之間者铜,Shader會(huì)有兩個(gè)Input腔丧,然后通過progress控制進(jìn)度放椰,我理解的轉(zhuǎn)場(chǎng)主要包括以下這些:
- UV變換
轉(zhuǎn)場(chǎng)很多其實(shí)都是UV變化,UV坐標(biāo)圍繞某個(gè)點(diǎn)旋轉(zhuǎn)愉粤、縮放砾医、平移
//scale center
vec2 scale_uv = vec2(0.5 + (tc.x - 0.5) / scaleU , 0.5 + (tc.y - 0.5) / scaleV );
//rotate
vec2 rotateUV(vec2 uv, float rotation, vec2 mid)
{
float ratio = (resolution.x / resolution.y);
float s = sin ( rotation );
float c = cos ( rotation );
mat2 rotationMatrix = mat2( c, -s, s, c);
vec2 coord = vec2((uv.x - mid.x) * ratio ,(uv.y -mid.y)*1.0);
vec2 scaled = rotationMatrix * coord;
return vec2(scaled.x / ratio + mid.x,scaled.y + mid.y);
}
//translate
vec2 translate_uv = vec2(0.5 + (tc.x - 0.5) / scaleU , 0.5 + (tc.y - 0.5) / scaleV );
怎么控制進(jìn)度就慢慢調(diào)吧
- Blend轉(zhuǎn)場(chǎng)
首先熟悉一下PhotoShop里面的混合模式alphe混合、濾色衣厘、加深如蚜、減淡、高亮度等等
https://zhuanlan.zhihu.com/p/23905865
然后就可以慢慢玩了
- 模糊轉(zhuǎn)場(chǎng)
主要也是Photoshop里面的幾種模糊方式影暴,旋轉(zhuǎn)模糊错邦、高斯模糊、均值模糊等等
需要注意一下旋轉(zhuǎn)模糊型宙,我實(shí)現(xiàn)過一種旋轉(zhuǎn)模糊的轉(zhuǎn)場(chǎng)撬呢,需要配合隨機(jī)采樣
float rand(vec2 uv){
return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
vec4 rotation_blur(vec2 tc) {
angle = angle * PI_ROTATION / 180.0;
vec2 uv = tc;
float uv_random = rand(uv);
vec4 sum_color = vec4(0.0);
for(float i = 0.0; i < samples; i++) {
float percent = (i + uv_random) / samples;
float real_angle = angle + percent * strength;
real_angle = mod(real_angle, PI_ROTATION);
vec2 uv_rotation = rotateUV(uv, real_angle, center);
sum_color += INPUT(fract(uv_rotation));
}
return sum_color / samples;
}
And 這里有個(gè)轉(zhuǎn)場(chǎng)的網(wǎng)站,可以研究一下
3.4 特效篇
基礎(chǔ)的特效包括:抖動(dòng)妆兑、靈魂出竅魂拦、故障風(fēng)、光暈搁嗓、老電影芯勘、粒子特效等等
比如old film
<img src="https://i.loli.net/2020/03/30/GutfwzVTk2LlC1h.png" alt="old_film.png" style="zoom:50%;" />
比如glitch風(fēng)格(動(dòng)態(tài)的會(huì)好一點(diǎn))
<img src="https://i.loli.net/2020/03/30/sPNvIylF56gkYzQ.png" alt="glitch.png" style="zoom:50%;" />
?
? 不一而足...
?
? 這些特效都不難,網(wǎng)上Shader到處都是谱姓,基本copy下來調(diào)調(diào)參數(shù)就行借尿,可能也就粒子特效需要調(diào)很多細(xì)節(jié)刨晴,但如果你有圖形引擎的基礎(chǔ)屉来,這些都很簡(jiǎn)單,到時(shí)候我可以建個(gè)倉庫狈癞,share一些我自己實(shí)現(xiàn)的shader
一般特效的話可以先去網(wǎng)上找茄靠,比如shadertoy
如果找不到的話,需要自己去想怎么實(shí)現(xiàn)蝶桶,我一般是會(huì)按如下的方式去思考:
- 善于利用各種卷積濾波器(邊緣檢測(cè)慨绳、模糊),有時(shí)候需要和隨機(jī)采樣結(jié)合
- 熟悉各種顏色空間真竖,熟悉飽和度脐雪、銳度、亮度恢共、色度等基礎(chǔ)調(diào)節(jié)
- UV變換多寫幾個(gè)有經(jīng)驗(yàn)了战秋,然后理解一些曲線函數(shù),基本都能慢慢調(diào)出來
- 可以試著看看相關(guān)論文讨韭,或者OpenCV的實(shí)現(xiàn)方式
簡(jiǎn)單的特效實(shí)現(xiàn)起來并不難脂信,多去寫癣蟋,慢慢總結(jié)經(jīng)驗(yàn)的套路,如果更深入一點(diǎn)可能需要圖形圖像和數(shù)學(xué)的知識(shí)
3.5 串聯(lián)這些效果
? 可能大家比較熟悉的就是GPUImage了狰闪,基本都是用這個(gè)去串聯(lián)這些特效疯搅、濾鏡之類,關(guān)于GPUImage網(wǎng)上也有很多資料埋泵,這里就不具體講了幔欧,也比較簡(jiǎn)單,內(nèi)部就是一個(gè)Input一個(gè)output通過FBO串起來丽声。這里主要想推薦大家看下movit這個(gè)框架琐馆,基本跟GPUImage類似,支持單個(gè)input和多個(gè)input恒序,可以打斷中間節(jié)點(diǎn)瘦麸,也可以有FrameBufferCache機(jī)制,但他還有一個(gè)優(yōu)化的點(diǎn)就是歧胁,Shader動(dòng)態(tài)組裝機(jī)制滋饲,熟悉游戲引擎應(yīng)該都知道這個(gè),Shader是可以在最后動(dòng)態(tài)生成喊巍,他里面是通過宏define來控制屠缭,可以把一些列串聯(lián)特效組裝到一起,非常高效崭参,后面可以單獨(dú)講講這塊呵曹。
只需要看下他組裝shader的過程:
for (unsigned i = 0; i < phase->effects.size(); ++i) {
Node *node = phase->effects[i];
const string effect_id = phase->effect_ids[make_pair(node, IN_SAME_PHASE)];
for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
if (node->incoming_links.size() == 1) {
frag_shader += "#define INPUT";
} else {
char buf[256];
sprintf(buf, "#define INPUT%d", j + 1);
frag_shader += buf;
}
Node *input = node->incoming_links[j];
NodeLinkType link_type = node->incoming_link_type[j];
if (i != 0 &&
input->effect->is_compute_shader() &&
node->incoming_link_type[j] == IN_SAME_PHASE) {
// First effect after the compute shader reads the value
// that cs_output() wrote to a global variable,
// ignoring the tc (since all such effects have to be
// strong one-to-one).
frag_shader += "(tc) CS_OUTPUT_VAL\n";
} else {
assert(phase->effect_ids.count(make_pair(input, link_type)));
frag_shader += string(" ") + phase->effect_ids[make_pair(input, link_type)] + "\n";
}
}
frag_shader += "\n";
frag_shader += string("#define FUNCNAME ") + effect_id + "\n";
if (node->effect->is_compute_shader()) {
frag_shader += string("#define NORMALIZE_TEXTURE_COORDS(tc) ((tc) * ") + effect_id + "_inv_output_size + " + effect_id + "_output_texcoord_adjust)\n";
}
frag_shader += replace_prefix(node->effect->output_fragment_shader(), effect_id);
frag_shader += "#undef FUNCNAME\n";
if (node->incoming_links.size() == 1) {
frag_shader += "#undef INPUT\n";
} else {
for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
char buf[256];
sprintf(buf, "#undef INPUT%d\n", j + 1);
frag_shader += buf;
}
}
frag_shader += "\n";
}
完了之后可以生成類似如下的shader:
precision highp float;
varying vec2 tc;
#define FUNCNAME eff0
uniform sampler2D eff0_tex;
vec4 FUNCNAME(vec2 tc) {
return texture2D(eff0_tex, vec2(tc.x,1.0-tc.y));
}
#undef PREFIX
#undef FUNCNAME
#define INPUT eff0
#define FUNCNAME eff1
uniform float eff1_strength;
uniform sampler2D eff1_lut;
vec4 FUNCNAME(vec2 tc) {
float strength = eff1_strength;
lowp vec4 textureColor = INPUT(tc);
mediump float blueColor = textureColor.b * 63.0;
mediump vec2 quad1; quad1.y = floor(floor(blueColor) / 8.0);
quad1.x = floor(blueColor) - (quad1.y * 8.0);
mediump vec2 quad2;
quad2.y = floor(ceil(blueColor) / 8.0);
quad2.x = ceil(blueColor) - (quad2.y * 8.0);
highp vec2 texPos1;
texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
highp vec2 texPos2;
texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
lowp vec4 newColor1 = texture2D(eff1_lut, texPos1);
lowp vec4 newColor2 = texture2D(eff1_lut, texPos2);
lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
return mix(textureColor, vec4(newColor.rgb, textureColor.w), strength);
}
#undef PREFIX
#undef FUNCNAME
#undef INPUT
#define INPUT eff1
void main()
{
gl_FragColor = INPUT(tc);
}
? 直接通過一個(gè)#define FUNCNAME就可以串起來,我以前寫的那個(gè)渲染引擎何暮,也是動(dòng)態(tài)組裝shader奄喂,跟這個(gè)有點(diǎn)類似,不過比這個(gè)復(fù)雜海洼,需要更多的宏來控制跨新,有興趣可以去看看
Movit源碼:https://git.sesse.net/?p=movit;a=summary
4. 關(guān)于音視頻處理
4.1 音視頻理論知識(shí)
- H264/H265編碼原理,宏快怎么劃分
- I坏逢、P域帐、B幀壓縮方式
- SPS/PPS 信息
- 音頻的采樣率
- 封裝格式(MP4, FLV),MP4的Box形式存儲(chǔ)
- YUV數(shù)據(jù)
4.2 編解碼部分
音視頻解碼基本流程
參考:
https://blog.csdn.net/leixiaohua1020/article/details/18893769
- 硬解部分(Android MediaCodec)
在低端平臺(tái)更多的需要依賴硬件解碼是整,效率會(huì)更高肖揣,Android MediaCodec
https://developer.android.com/reference/android/media/MediaCodec?hl=en
大概的代碼邏輯
while (!m_sawOutputEOS) {
if (!m_sawInputEOS) {
// Feed more data to the decoder
final int inputBufIndex = m_decoder.dequeueInputBuffer(TIMEOUT_USEC);
if (inputBufIndex >= 0) {
ByteBuffer inputBuf = m_decoderInputBuffers[inputBufIndex];
// Read the sample data into the ByteBuffer. This neither
// respects nor
// updates inputBuf's position, limit, etc.
final int chunkSize = m_extractor.readSampleData(inputBuf, 0);
if (m_verbose)
Log.d(TAG, "input packet length: " + chunkSize + " time stamp: " + m_extractor.getSampleTime());
if (chunkSize < 0) {
// End of stream -- send empty frame with EOS flag set.
m_decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
m_sawInputEOS = true;
if (m_verbose)
Log.d(TAG, "Sent input EOS");
} else {
if (m_extractor.getSampleTrackIndex() != m_videoTrackIndex) {
Log.w(TAG, "WEIRD: got sample from track " + m_extractor.getSampleTrackIndex()
+ ", expected " + m_videoTrackIndex);
}
long presentationTimeUs = m_extractor.getSampleTime();
m_decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, presentationTimeUs, 0);
if (m_verbose)
Log.d(TAG,
"Submitted frame to decoder input buffer " + inputBufIndex + ", size=" + chunkSize);
m_inputBufferQueued = true;
++m_pendingInputFrameCount;
if (m_verbose)
Log.d(TAG, "Pending input frame count increased: " + m_pendingInputFrameCount);
m_extractor.advance();
m_extractorInOriginalState = false;
}
} else {
if (m_verbose)
Log.d(TAG, "Input buffer not available");
}
}
final int decoderStatus = m_decoder.dequeueOutputBuffer(m_bufferInfo, dequeueTimeoutUs);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// No output available yet
if (m_verbose)
Log.d(TAG, "No output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// Not important for us, since we're using Surface
if (m_verbose)
Log.d(TAG, "Decoder output buffers changed");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = m_decoder.getOutputFormat();
if (m_verbose)
Log.d(TAG, "Decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
Log.e(TAG, "Unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
return ERROR_FAIL;
} else {
///....
m_decoder.releaseOutputBuffer(decoderStatus, doRender);
}
}
解碼過程網(wǎng)上到處都是,解碼主要就是配合MediaExtrator浮入,直接說一下需要注意的點(diǎn):
- getOutputBuffer得到的數(shù)據(jù)格式主要是YUV420P和YUV420SP龙优,UV通道排列不一樣
- seekTo完之后要Flush
- configure如果傳入surface則是直接decode到suface上,不同通過getOutputBuffer獲取數(shù)據(jù)
- 軟解部分(FFMPEG)
? 其實(shí)現(xiàn)在絕大部分音視頻APP編解碼都是用的FFMPEG舵盈,這里門也包含了硬解MediaCodec部分陋率,所以只需要用FFMPEG就可以做硬解和軟解的切換球化。關(guān)于FFMPEG的學(xué)習(xí),首推肯定是雷神的博客啦瓦糟,
https://blog.csdn.net/leixiaohua1020/article/details/15811977
主要包括兩個(gè)部分筒愚,首先是prepare部分,
if (avformat_open_input(&mFormatContext, path.c_str(), NULL, NULL) != 0) {
mlt_log_error(mProducer, "Could not open input file: %s", path.c_str());
goto error;
}
if (avformat_find_stream_info(mFormatContext, NULL) < 0) {
mlt_log_error(mProducer, "Could not find stream information");
goto error;
}
mVideoStreamIndex = findPreferedVideoStream();//遍歷track找到視頻軌
mVideoStream = mFormatContext->streams[mVideoStreamIndex];
//然后就可以得到一些列參數(shù)菩浙,width巢掺,height,fps劲蜻,fromat
//如果是硬解
//根據(jù)不同的編碼器陆淀,找到對(duì)應(yīng)的解碼器,如H264先嬉,H265等
mVideoCodec = avcodec_find_decoder_by_name("h264_mediacodec");;
//如果是軟解
mVideoCodec = avcodec_find_decoder(mVideoStream->codecpar->codec_id);
//open
avcodec_open2(mVideoCodecContext, mVideoCodec, NULL)
//就可以開始解碼了
解碼部分
do {
if (!mPacket) {
ret = getVideoPacket();
if (ret < 0 && ret != AVERROR_EOF) {
break;
}
}
//這里面送包的時(shí)候有很多細(xì)節(jié)
int ret = avcodec_send_packet(mVideoCodecContext, flush ? NULL : mPacket);
if (ret >= 0)
freeVideoPacket();
else if (ret < 0 && ret != AVERROR_EOF && ret != AVERROR(EAGAIN)) {
break;
}
ret = avcodec_receive_frame(mVideoCodecContext, mFrame);
if (ret >= 0) {
ret = DECODER_FFMPEG_SUCCESS;
//計(jì)算pts
mCurrentPos = getTime(mFrame->pts == AV_NOPTS_VALUE ? mFrame->pkt_dts : mFrame->pts);
pts = mCurrentPos;
break;
}
++count;
} while (ret < DECODER_FFMPEG_SUCCESS && count < DECODER_TRY_ATTEMPTS);
//后面就是針對(duì)不同的格式轧苫,接入數(shù)據(jù),
//還可以通過swscaleFrame進(jìn)行轉(zhuǎn)碼疫蔓,轉(zhuǎn)成你想要的格式
//然后就是copy AVFrame里面的數(shù)據(jù)啦
關(guān)于FFMPEG有太多可以去學(xué)習(xí)的了含懊,比如:
- 學(xué)會(huì)用ffmpeg ffprobe ffplay一些命令,真的非常強(qiáng)大衅胀,方便分析問題(ffprobe -i xxxx)
- 怎么去切換軟解和硬碼
- 熟悉API返回的狀態(tài)
然后就是兼容性的問題:
? 由于Android平臺(tái)太復(fù)雜了岔乔,廠商很多,所以會(huì)有無窮無盡的兼容性問題需要去解決滚躯,總體來說兼容性問題主要包括:
- 數(shù)據(jù)源的兼容性雏门,YUV格式非常多樣,420p/420sp是常見的掸掏,還有yuv420p10le 這種10bit茁影,賊坑
- 硬件平臺(tái)的兼容性,這里坑就更多了,MediaCodec支持程度呜叫,尤其是低端機(jī)非常需要注意
- 分辨率問題,4K/8K,高通平臺(tái)的支持程度
這個(gè)后續(xù)單獨(dú)去整理吧......(挖坑4)
4.3 音視頻同步
視音頻同步的實(shí)現(xiàn)方式其實(shí)有三種顶滩,分別是:
- 以音頻為主時(shí)間軸作為同步源;
- 以視頻為主時(shí)間軸作為同步源;
- 以外部時(shí)鐘為主時(shí)間軸作為同步源;
? 具體用哪一種需要根據(jù)場(chǎng)景,比如在線視頻播放器苦始,一般會(huì)以第一種音頻作為主時(shí)間軸去對(duì)齊視頻幀數(shù)據(jù)坎匿,做丟幀和用當(dāng)前幀處理,比如我之前寫的音視頻編輯SDK蚁廓,因?yàn)槲覀冇幸粭l固定幀率的時(shí)間線访圃,所以我們的對(duì)齊方式是以這條固定時(shí)間軸來對(duì)齊視頻。
? 比如當(dāng)前時(shí)間軸如果大于當(dāng)前decode出來的幀的pts相嵌,就直接丟掉腿时,繼續(xù)找下一幀况脆,如果當(dāng)前時(shí)間軸小于decode出來的幀的pts,超過1一幀的時(shí)間就繼續(xù)用上一幀渲染即可批糟。
4.4 多視頻多軌道(進(jìn)階)
? 視頻編輯SDK肯定是需要支持多視頻多軌道編輯格了,如何高效的管理,方便編輯和預(yù)覽徽鼎,這里面主要是需要一個(gè)好的Multi track的框架支撐盛末,在這里推薦一個(gè)MLT框架,這個(gè)框架對(duì)于多視頻多軌道處理真的非常強(qiáng)大否淤,內(nèi)部有精確的時(shí)間戳對(duì)齊邏輯悄但,只需要關(guān)注多軌道數(shù)據(jù)直接的排列,包括每一個(gè)視頻片段生產(chǎn)數(shù)據(jù)石抡,其內(nèi)部會(huì)幫我們做好時(shí)間戳對(duì)齊檐嚣,擔(dān)任也可以和很多插件配合使用,比如movit啰扛,ffmpeg等净嘀。
? 基本框架
+--------+ +------+ +--------+
|Producer|-->|Filter|-->|Consumer|
+--------+ +------+ +--------+
具體的介紹后面單獨(dú)寫一篇博客整理,如下是官網(wǎng)介紹:
https://github.com/mltframework/mlt
5. 關(guān)于傳輸
這塊沒做過侠讯,對(duì)傳輸協(xié)議不太了解挖藏,只知道一些理論知識(shí),比如RTMP的分塊傳輸厢漩,后面有機(jī)會(huì)再補(bǔ)齊
5.1 RTMP
6. 分析音視頻APP
? 在開發(fā)過程中膜眠,對(duì)于一個(gè)小白來說可能會(huì)經(jīng)常遇到一籌莫展的時(shí)候,這時(shí)候要學(xué)會(huì)向同類優(yōu)秀的應(yīng)用學(xué)習(xí)溜嗜,比如做短視頻相關(guān)的可以去看抖音/快手怎么做的宵膨,做音視頻剪輯相關(guān)的可以看看見剪映/快影/小影怎么做的,你可以去把他們的包pull出來炸宵,比如我在做資源的接入的時(shí)候辟躏,就對(duì)比過快手/抖音/大疆 這幾家的資源,看他們的sticker怎樣接入土全,有png序列捎琐,有MP4,有自定義GIF格式的裹匙。所以從這些APP的packge內(nèi)部還是能找到一些線索瑞凑,跟著這些線索再慢慢找到其內(nèi)部的邏輯,就比如我上面分析抖音潛水艇那個(gè)游戲一樣概页,當(dāng)然也可以去分析其它的一些文件籽御,給你提供思路。
1. 抖音
Package name: com.ss.android.ugc.aweme
adb pull /data/data/com.ss.android.ugc.aweme
主要是分析了一下里面的Effect資源,還有LOG信息, 后面可以有機(jī)會(huì)繼續(xù)拆解一下(挖坑5)
2. 快手
Package name: com.smile.gifmaker
同樣的方式
3. 大疆
Package name: dji.mimo
? 主要是DJI mimo技掏,但是做的比較爛铃将,所以沒有太多可分析的,不過通過他們的sticker資源可以看到他們支持alpha通道視頻的原理哑梳,后面有類似需求也可以拿過來用
7. 總結(jié)
現(xiàn)在是凌晨3點(diǎn)多劲阎,從周日從早上開始寫到晚上3點(diǎn),差不多用了將近18個(gè)小時(shí)來整理[吐血]涧衙。想想上一次博客更新差不多是兩年前了哪工,那時(shí)候是做圖形渲染相關(guān)的事情,也試著自己寫了一個(gè)渲染引擎弧哎,結(jié)合了不錯(cuò)的開源引擎雁比,寫完之后對(duì)我圖形方向的理解有很大的進(jìn)步,于是試著開始寫了幾篇博客撤嫩,繼續(xù)維護(hù)自己關(guān)于圖形方向的研究偎捎,后面由于各種原因,沒有繼續(xù)在維護(hù)之前的圖形引擎序攘,自己也不在更新博客了茴她。
? 今天以音視頻作為新的方向,重新回來程奠,把自己這兩年的一些積累整理出來丈牢,希望能對(duì)后面的學(xué)習(xí)者有所幫助,我也會(huì)盡力去打磨好每一篇博客瞄沙,今天這遍博客只是總結(jié)一個(gè)大的框架己沛,里面有非常非常多的細(xì)節(jié),都可單獨(dú)用一篇文章去講距境,比如關(guān)于Camera申尼,音視頻特效,編解碼垫桂,兼容性這些师幕,都需要花時(shí)間去一點(diǎn)一點(diǎn)研究。我也會(huì)對(duì)自己每一篇文章有嚴(yán)格的要求诬滩,要么不寫霹粥,要寫就盡量按照論文的形式把每一個(gè)點(diǎn)講清楚,結(jié)合流程圖和效果圖碱呼,最后還需要給出Demo復(fù)現(xiàn)效果蒙挑。
? 還是開頭那句話,隨著5G是時(shí)代的到來愚臀,音視頻的應(yīng)用將會(huì)更加普及,這方面的人才也會(huì)更加緊缺,同時(shí)對(duì)于這里面的技術(shù)要求也會(huì)越來越高姑裂,從現(xiàn)在開始就慢慢積累馋袜,完善里面的技能棧,也可以選擇一個(gè)分支去深入下去舶斧,這里面有很多的方向都值得研究欣鳖。也希望以這篇文章為起點(diǎn),尋找一些志同道合之人茴厉,一起去探索一些音視頻方向的玩法泽台,當(dāng)然還有相機(jī)這塊,因?yàn)槲抑耙恢笔亲鱿鄼C(jī)相關(guān)的矾缓,也思考過相機(jī)有什么好的方向可以去探索怀酷。
? 總之,我會(huì)一直做音視頻這個(gè)方向嗜闻,抖音和快手在視頻玩法上做到了極致蜕依,手機(jī)廠商不斷的升級(jí)Camera,也越來越重視視頻的采集琉雳,讓手機(jī)作為拍攝工具可以拍出更加震撼的效果样眠,還有大疆在無人機(jī)領(lǐng)域視角去采集的視頻數(shù)據(jù),也可以做出很多大片效果翠肘,還有Insta360在全景方向的玩法檐束,做的也很棒,未來隨著硬件設(shè)備的升級(jí)束倍,還會(huì)出現(xiàn)更多有趣的玩法被丧,比如和AR/VR結(jié)合在一起,又會(huì)有怎樣的火花呢肌幽,我們拭目以待晚碾。
BLOG: http://yanglusheng.com/
人生只有一次,做自己喜歡的事吧