<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我的音視頻技術(shù)路線

          共 16305字,需瀏覽 33分鐘

           ·

          2022-02-09 17:34

          ? 目錄


          抖音/快手等短視頻APP的風(fēng)靡,讓音視頻成為當(dāng)下最火熱的技術(shù),越來(lái)越多的人想要進(jìn)入到這個(gè)領(lǐng)域,我自己也是從圖形方向剛剛踏入這領(lǐng)域不久,音視頻方向所包含的技術(shù)棧非常復(fù)雜,我自己也在一點(diǎn)一點(diǎn)慢慢鉆研,這里面每一個(gè)方向都值得深入研究,而且隨著5G時(shí)代的到來(lái),音視頻方向的應(yīng)用會(huì)更加廣泛,所以希望自己能掌握更多的關(guān)于音視頻方向的技能,未來(lái)可以探索更多的音視頻玩法。然后這篇博客主要是想梳理一下我自己關(guān)于音視頻這個(gè)方向的學(xué)習(xí)路線,分享出來(lái)的同時(shí)也能鼓勵(lì)自己朝著這個(gè)方向繼續(xù)深耕下去。

          ? 關(guān)于音視頻方向的基礎(chǔ)技能分支,先來(lái)看一張圖(圖片來(lái)自網(wǎng)上)



          采集:音視頻數(shù)據(jù)來(lái)源,比如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接口過(guò)于簡(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)
              }

          關(guān)于Camera2的介紹

          Camera 2.0: New computing platforms for computational photography

          ? Camera API2對(duì)比API 1改動(dòng)非常大,主要配合HAL3進(jìn)行使用,功能和接口都更加齊全,同時(shí)使用起來(lái)也會(huì)更加復(fù)雜,但如果熟悉之后,也能拍攝出更加豐富的效果。下圖關(guān)于Camera API 2的幾個(gè)使用場(chǎng)景


          ? 然后結(jié)合具體代碼講幾個(gè)Camera2的主要接口

          1. 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ù)量越來(lái)越多,組合也越來(lái)越豐富,不同的Camera負(fù)責(zé)不同的能力,比如我們需要更大的拍攝范圍會(huì)選擇Ultra Wide,比如小米一億像素的Wide,還有Macro等等

          2. 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();
                  }
              };      

          3. CaptureRequest

          通過(guò)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());

          4. Surface

          Surface主要是用來(lái)接相機(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);

          5. CaptureSession

          通過(guò)上面的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ī)制最早是諾基亞提出來(lái)的,后面蘋果的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);

          然后還可以通過(guò)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連接起來(lái)。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 是可選插件,通過(guò)該插件,您可以在支持的設(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ī)功能越來(lái)越強(qiáng)大,現(xiàn)在已經(jīng)基本可以取代卡片機(jī),手機(jī)相機(jī)已經(jīng)作為手機(jī)廠商最重要的一個(gè)賣點(diǎn),尤其是DXO刷榜的行為,之后讓廠商投入更多的研發(fā)相機(jī)上面,在可以預(yù)見的未來(lái),手機(jī)相機(jī)發(fā)展勢(shì)必會(huì)更加激進(jìn),后面進(jìn)一步取代部分單反也是有可能的,所以相機(jī)未來(lái)應(yīng)該更多的體現(xiàn)起作為生產(chǎn)力工具的一部分,尤其是現(xiàn)在視頻作為人們記錄生活的方式,越來(lái)越得到普及。

          ? 如果說(shuō)到視頻和相機(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形渲染方面的工作,也寫過(guò)自己的渲染引擎,鏈接如下

          ? github.com/LukiYLS/Simp

          以及關(guān)于這個(gè)引擎的介紹

          ? yanglusheng.com/

          ? 所以可以大概分享一下自己的學(xué)習(xí)過(guò)程,以及關(guān)于Android GLES相關(guān)總結(jié)

          2.1 圖形學(xué)基礎(chǔ)

          ? 我覺(jué)圖形方面基礎(chǔ)的應(yīng)該需要掌握如下:

          1. 整個(gè)圖形矩陣變換過(guò)程

          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)系

          這部分想要理解就要自己用筆手推一遍,非常管用

          2. OpenGL API

          熟悉API,比如紋理貼圖方式,繪制點(diǎn)線面,VAO/VBO創(chuàng)建等等

          3. 光照

          首先需要理解傳統(tǒng)的Phong光照,然后要看PBR,理解BRDF模型和公式推導(dǎo)

          還有就是Shadow這快,理解shadowmap的原理,不同的光照怎樣生成深度圖,然后shadow acne,處理邊緣鋸齒等很多細(xì)節(jié),還有陰影體這塊

          4. 模版測(cè)試/深度測(cè)試/Alpha混合

          深度測(cè)試實(shí)現(xiàn)遮擋

          模版測(cè)試也是非常有用,比如我有篇博客里面講到的 陰影體結(jié)合模版測(cè)試實(shí)現(xiàn)矢量緊貼地形的效果

          5. 地形

          怎用利用perlin noise生成地形頂點(diǎn)數(shù)據(jù)

          LOD的地形:規(guī)則四叉樹劃分(Google Earth地形),以及不規(guī)則的CLOD(自適應(yīng)三角網(wǎng))

          6. 粒子系統(tǒng)

          主要就是怎么控制粒子發(fā)射器,粒子加速的,粒子運(yùn)動(dòng)軌跡等等

          7. 場(chǎng)景管理

          如何利用四叉樹、八叉樹管理場(chǎng)景節(jié)點(diǎn),做視錐體裁剪

          ........

          之前寫的渲染引擎,都包含這些基本模塊,大概持續(xù)完善了半年左右,對(duì)我圖形渲染方面的提升非常大,包括自己也寫過(guò)軟光柵器

          總結(jié):

          學(xué)習(xí)圖形學(xué)最好的方式就是造輪子造輪子造輪子,自己寫引擎,自己寫軟光柵器

          學(xué)習(xí)資料分享:

          learnopengl-cn.github.io

          scratchapixel.com/

          閱讀源碼:OGRE/OSG/THREEJS

          工具: unity3d processing

          2.2 OpenGL/GLES

          1. EGL環(huán)境創(chuàng)建

          eglGetDisplay //獲取display信息

          eglChooseConfig //設(shè)置RGBA bit depth

          eglCreatePbufferSurface // 創(chuàng)建離屏surface, 也可以eglCreateWindowSurface

          eglCreateContext //創(chuàng)建上下文

          2. GL多線程
          shareContext 方式

          這個(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ò)

          3. 渲染優(yōu)化

          ? 幀率如何優(yōu)化,涉及到很多方面,這方面游戲引擎有很多技巧,比如提前視錐體裁剪,遮擋剔除等等,我這里只是簡(jiǎn)單講一下在不涉及到大場(chǎng)景的優(yōu)化有哪些

          個(gè)人關(guān)于效率優(yōu)化的總結(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出來(lái)就能看到,里面是同lua腳本去調(diào)用底層的渲染引擎,lua腳本負(fù)責(zé)下發(fā)一些參數(shù),主要是AI的一些識(shí)別結(jié)果以及用戶的交互事件。

          ? 雖然沒(méi)有游戲引擎那么強(qiáng)大,但基本模塊應(yīng)該都會(huì)包含模型、資源管理、相機(jī)控制、裁剪、渲染等等,畢竟寫這套引擎的基本都是以前搞游戲的那撥人,算是降維來(lái)寫音視頻渲染引擎。

          ? 下面通過(guò)拆解抖音內(nèi)部的資源文件,探究?jī)?nèi)部關(guān)于音視頻渲染引擎的模塊組成,對(duì)比短視頻引擎和游戲引擎的區(qū)別。

          一起來(lái)看看抖音最近很火的一個(gè)游戲:潛水艇,通過(guò)移動(dòng)鼻子控制潛水艇



          如下是download 的資源包



          ? 這里面的核心控制邏輯就是那個(gè)lua基本,里面會(huì)負(fù)責(zé)把人臉關(guān)鍵點(diǎn)信息傳給底層引擎,實(shí)時(shí)更新潛水艇的位置,腳本實(shí)現(xiàn)了兩個(gè)碰撞檢測(cè)函數(shù),用于潛水艇和柱子之間做碰撞檢測(cè)。Collision玩過(guò)游戲引擎的都很熟悉,有些游戲引擎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秒走過(guò)0.5個(gè)屏幕,最終速度為1秒走過(guò)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ù)覽和錄制,游戲過(guò)程的邏輯,比如柱子隨著時(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è)類似游戲引擎的這樣的渲染框架,然后通過(guò)腳本進(jìn)行控制。功能也應(yīng)該是挺齊全的,可能是縮小版的游戲引擎,就是麻雀雖小,五臟俱全。

          ? 對(duì)于熟悉游戲引擎的人來(lái)說(shuō),這部分應(yīng)該不難,基本都是那些

          ? 另外,如果想學(xué)習(xí)引擎這快,我的思路是多去看看一些流行的開源引擎,一開始模仿別人怎么寫,然后不斷的思考總結(jié),慢慢的你就知道一個(gè)引擎應(yīng)該包含哪些,以及怎么控制各個(gè)模塊之間的交互。

          ? 比如我之前深入研究過(guò)OGRE,看過(guò)OSG,以及深入研究過(guò)THREEJS,同時(shí)我自己寫的渲染引擎,有把這幾個(gè)引擎比較好的模塊模仿過(guò)來(lái),逐漸內(nèi)化成自己的引擎。

          ? 后面有機(jī)會(huì)把拆解一下抖音比較復(fù)雜的特效吧,最好找一個(gè)和AI或者AR結(jié)合的特效(挖坑2)

          3. 關(guān)于音視頻圖形部分

          ? 這里主要會(huì)大概整理一下比較基礎(chǔ)的幾個(gè)部分,包括濾鏡、美顏、特效、轉(zhuǎn)場(chǎng)這些,因?yàn)槲夷壳昂孟裰蛔鲞^(guò)這些基礎(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

          //這沒(méi)啥說(shuō)的,都是很基本的
          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看起來(lái)更加和諧一點(diǎn),因?yàn)?D LUT 的RGB映射關(guān)系是關(guān)聯(lián)在一起,1D LUT則是獨(dú)立的,不過(guò)1D LUT可以節(jié)省空間,有些只需要單通道就可以搞定的,就可以用1D LUT

          ? LUT得益于其簡(jiǎn)單的生產(chǎn)流程,基本設(shè)計(jì)師那邊用PhotoShop調(diào)好一張查找表,這邊就應(yīng)用上去就OK了

          相關(guān)資料:

          affinityspotlight.com/a

          zhuanlan.zhihu.com/p/37

          zhuanlan.zhihu.com/p/60

          3.2 美顏篇

          1. 磨皮去燥

          最簡(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)硬,


          2. 美白

          HighPass高亮加一點(diǎn)紅暈


          3. 美型

          美型主要是需要和人臉檢測(cè)結(jié)合起來(lái),對(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,然后通過(guò)progress控制進(jìn)度,我理解的轉(zhuǎn)場(chǎng)主要包括以下這些:

          1. 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)吧

          2. Blend轉(zhuǎn)場(chǎng)

          首先熟悉一下PhotoShop里面的混合模式alphe混合、濾色、加深、減淡、高亮度等等

          zhuanlan.zhihu.com/p/23

          然后就可以慢慢玩了

          3. 模糊轉(zhuǎn)場(chǎng)

          主要也是Photoshop里面的幾種模糊方式,旋轉(zhuǎn)模糊、高斯模糊、均值模糊等等

          需要注意一下旋轉(zhuǎn)模糊,我實(shí)現(xiàn)過(guò)一種旋轉(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)站,可以研究一下

          gl-transitions.com/

          3.4 特效篇

          基礎(chǔ)的特效包括:抖動(dòng)、靈魂出竅、故障風(fēng)、光暈、老電影、粒子特效等等

          比如old film


          old_film


          比如glitch風(fēng)格(動(dòng)態(tài)的會(huì)好一點(diǎn))


          glitch

          ? ? 這些特效都不難,網(wǎng)上Shader到處都是,基本copy下來(lái)調(diào)調(diào)參數(shù)就行,可能也就粒子特效需要調(diào)很多細(xì)節(jié),但如果你有圖形引擎的基礎(chǔ),這些都很簡(jiǎn)單,到時(shí)候我可以建個(gè)倉(cāng)庫(kù),share一些我自己實(shí)現(xiàn)的shader

          一般特效的話可以先去網(wǎng)上找,比如shadertoy

          shadertoy.com/

          如果找不到的話,需要自己去想怎么實(shí)現(xiàn),我一般是會(huì)按如下的方式去思考:

          1. 善于利用各種卷積濾波器(邊緣檢測(cè)、模糊),有時(shí)候需要和隨機(jī)采樣結(jié)合
          2. 熟悉各種顏色空間,熟悉飽和度、銳度、亮度、色度等基礎(chǔ)調(diào)節(jié)
          3. UV變換多寫幾個(gè)有經(jīng)驗(yàn)了,然后理解一些曲線函數(shù),基本都能慢慢調(diào)出來(lái)
          4. 可以試著看看相關(guān)論文,或者OpenCV的實(shí)現(xiàn)方式

          簡(jiǎn)單的特效實(shí)現(xiàn)起來(lái)并不難,多去寫,慢慢總結(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通過(guò)FBO串起來(lái)。這里主要想推薦大家看下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)生成,他里面是通過(guò)宏define來(lái)控制,可以把一些列串聯(lián)特效組裝到一起,非常高效,后面可以單獨(dú)講講這塊。

          只需要看下他組裝shader的過(guò)程:

          //xxxx
          

          完了之后可以生成類似如下的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);
          }

          ? 直接通過(guò)一個(gè)#define FUNCNAME就可以串起來(lái),我以前寫的那個(gè)渲染引擎,也是動(dòng)態(tài)組裝shader,跟這個(gè)有點(diǎn)類似,不過(guò)比這個(gè)復(fù)雜,需要更多的宏來(lái)控制,有興趣可以去看看

          Movit源碼:git.sesse.net/?

          4. 關(guān)于音視頻處理

          4.1 音視頻理論知識(shí)

          1. H264/H265編碼原理,宏快怎么劃分
          2. I、P、B幀壓縮方式
          3. SPS/PPS 信息
          4. 音頻的采樣率
          5. 封裝格式(MP4, FLV),MP4的Box形式存儲(chǔ)
          6. YUV數(shù)據(jù)

          4.2 編解碼部分

          音視頻解碼基本流程



          參考:

          blog.csdn.net/leixiaohu

          1. 硬解部分(Android MediaCodec)

          在低端平臺(tái)更多的需要依賴硬件解碼,效率會(huì)更高,Android MediaCodec



          developer.android.com/r

          大概的代碼邏輯

          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);
                  }
              }

          解碼過(guò)程網(wǎng)上到處都是,解碼主要就是配合MediaExtrator,直接說(shuō)一下需要注意的點(diǎn):

          • getOutputBuffer得到的數(shù)據(jù)格式主要是YUV420P和YUV420SP,UV通道排列不一樣
          • seekTo完之后要Flush
          • configure如果傳入surface則是直接decode到suface上,不同通過(guò)getOutputBuffer獲取數(shù)據(jù)

          2. 軟解部分(FFMPEG)

          ? 其實(shí)現(xiàn)在絕大部分音視頻APP編解碼都是用的FFMPEG,這里門也包含了硬解MediaCodec部分,所以只需要用FFMPEG就可以做硬解和軟解的切換。關(guān)于FFMPEG的學(xué)習(xí),首推肯定是雷神的博客啦,

          blog.csdn.net/leixiaohu

          主要包括兩個(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ù),
          //還可以通過(guò)swscaleFrame進(jìn)行轉(zhuǎn)碼,轉(zhuǎn)成你想要的格式
          
          //然后就是copy AVFrame里面的數(shù)據(jù)啦
          

          關(guān)于FFMPEG有太多可以去學(xué)習(xí)的了,比如:

          • 學(xué)會(huì)用ffmpeg ffprobe ffplay一些命令,真的非常強(qiáng)大,方便分析問(wèn)題(ffprobe -i xxxx)
          • 怎么去切換軟解和硬碼
          • 熟悉API返回的狀態(tài)

          然后就是兼容性的問(wèn)題

          ? 由于Android平臺(tái)太復(fù)雜了,廠商很多,所以會(huì)有無(wú)窮無(wú)盡的兼容性問(wèn)題需要去解決,總體來(lái)說(shuō)兼容性問(wèn)題主要包括:

          • 數(shù)據(jù)源的兼容性,YUV格式非常多樣,420p/420sp是常見的,還有yuv420p10le 這種10bit,賊坑
          • 硬件平臺(tái)的兼容性,這里坑就更多了,MediaCodec支持程度,尤其是低端機(jī)非常需要注意
          • 分辨率問(wèn)題,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í)間軸來(lái)對(duì)齊視頻。

          ? 比如當(dāng)前時(shí)間軸如果大于當(dāng)前decode出來(lái)的幀的pts,就直接丟掉,繼續(xù)找下一幀,如果當(dāng)前時(shí)間軸小于decode出來(lái)的幀的pts,超過(guò)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)介紹:

          github.com/mltframework

          5. 關(guān)于傳輸

          這塊沒(méi)做過(guò),對(duì)傳輸協(xié)議不太了解,只知道一些理論知識(shí),比如RTMP的分塊傳輸,后面有機(jī)會(huì)再補(bǔ)齊

          5.1 RTMP

          6. 分析音視頻APP

          ? 在開發(fā)過(guò)程中,對(duì)于一個(gè)小白來(lái)說(shuō)可能會(huì)經(jīng)常遇到一籌莫展的時(shí)候,這時(shí)候要學(xué)會(huì)向同類優(yōu)秀的應(yīng)用學(xué)習(xí),比如做短視頻相關(guān)的可以去看抖音/快手怎么做的,做音視頻剪輯相關(guān)的可以看看見剪映/快影/小影怎么做的,你可以去把他們的包pull出來(lái),比如我在做資源的接入的時(shí)候,就對(duì)比過(guò)快手/抖音/大疆 這幾家的資源,看他們的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,通過(guò)他們的sticker資源可以看到他們支持alpha通道視頻的原理,后面有類似需求也可以拿過(guò)來(lái)用

          7. 總結(jié)

          現(xiàn)在是凌晨3點(diǎn)多,從周日從早上開始寫到晚上3點(diǎn),差不多用了將近18個(gè)小時(shí)來(lái)整理[吐血]。想想上一次博客更新差不多是兩年前了,那時(shí)候是做圖形渲染相關(guān)的事情,也試著自己寫了一個(gè)渲染引擎,結(jié)合了不錯(cuò)的開源引擎,寫完之后對(duì)我圖形方向的理解有很大的進(jìn)步,于是試著開始寫了幾篇博客,繼續(xù)維護(hù)自己關(guān)于圖形方向的研究,后面由于各種原因,沒(méi)有繼續(xù)在維護(hù)之前的圖形引擎,自己也不在更新博客了。

          ? 今天以音視頻作為新的方向,重新回來(lái),把自己這兩年的一些積累整理出來(lái),希望能對(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í)代的到來(lái),音視頻的應(yīng)用將會(huì)更加普及,這方面的人才也會(huì)更加緊缺,同時(shí)對(duì)于這里面的技術(shù)要求也會(huì)越來(lái)越高,從現(xiàn)在開始就慢慢積累,完善里面的技能棧,也可以選擇一個(gè)分支去深入下去,這里面有很多的方向都值得研究。也希望以這篇文章為起點(diǎn),尋找一些志同道合之人,一起去探索一些音視頻方向的玩法,當(dāng)然還有相機(jī)這塊,因?yàn)槲抑耙恢笔亲鱿鄼C(jī)相關(guān)的,也思考過(guò)相機(jī)有什么好的方向可以去探索。

          ? 總之,我會(huì)一直做音視頻這個(gè)方向,抖音和快手在視頻玩法上做到了極致,手機(jī)廠商不斷的升級(jí)Camera,也越來(lái)越重視視頻的采集,讓手機(jī)作為拍攝工具可以拍出更加震撼的效果,還有大疆在無(wú)人機(jī)領(lǐng)域視角去采集的視頻數(shù)據(jù),也可以做出很多大片效果,還有Insta360在全景方向的玩法,做的也很棒,未來(lái)隨著硬件設(shè)備的升級(jí),還會(huì)出現(xiàn)更多有趣的玩法,比如和AR/VR結(jié)合在一起,又會(huì)有怎樣的火花呢,我們拭目以待。

          Blog:

          yanglusheng.com/
          人生只有一次,做自己喜歡的事吧
          瀏覽 27
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美三级片中文字幕在线观看 | 青青久草| 一级做a爰久久 | 男人天堂TV | 91超碰在线 |