文章目录

  • About URasterizer
    • 包含哪些内容
    • 关于本系列总结文章
  • 框架搭建
    • 关于Unity版本
    • 渲染目标和RawImage
    • CameraRenderer和RenderingObject
    • RenderingObject和RenderObjectData
  • 矩阵计算
    • 视图和投影矩阵计算
      • 视图矩阵计算
      • 平行投影矩阵
      • 透视投影矩阵
    • 模型变换矩阵的计算
  • 模型数据的手向性转换
    • 模型坐标和法线转换
    • 三角形环绕方向修改
  • 小结

About URasterizer

去年学习完GAMES101之后,一直想着自己动手把GAMES101的理论知识再实际操练一遍。恰好又了解到Unreal5的Nanite使用了软光栅提高微小三角形的光栅化性能,那么学习研究光栅化这个古老的幕后技术貌似在新时代又有了新的意义。所以从今年1月份开始,利用了春节假期的大部分时间以及两个月来的夜晚和周末,在Unity上实现了一套实验性的软光栅渲染器:URasterizer。

为什么要在Unity上实现呢?有几个原因。

  • 首先,Unity提供了很多基础设施,如载入模型贴图材质,相机、灯光以及Game Object的控制,甚至还有动画什么的,都可以直接利用,大大节省了自己写框架的时间。
  • 第二,可以和Unity直接渲染的效果以及性能做对比。通过Unity检验光栅化的正确性。
  • 第三,Unity使用Compute Shader很方便,使用Compute Shader实现软光栅也是计划之一,正好可以顺便练习一下CS的使用。
  • 第四,Unity尽管使用C#代码写光栅化没有C++效率高,但是Unity的Job System和Burst Compiler很好用,正好借这个机会体验一下CPU多核计算的威力。

包含哪些内容

URasterizer有三个独立的光栅化渲染器:

  • CPU Rasterizer :完全使用C#编写,在CPU上实现的单线程软件光栅化
  • CPU Job Rasterizer: 使用Unity的Job System和Burst Compiler,使用多个处理器核心执行经过编译优化的光栅化代码,性能大幅提升。
  • GPU Driven Rasterizer: 使用Compute shader实现整个光栅化流水线。GPU Driven模式的性能和直接使用Unity渲染相当。

具体支持的特性见项目github仓库的README

关于本系列总结文章

  • 首先,我们要建立框架,让软光栅能跑起来并输出最终结果。我们需要通过相机参数和Transform构建我们自己的矩阵,并且要从Unity获取到各种数据。Unity的坐标系约定和GAMES101并不一致,因此需要转换数据。本篇中会总结这部分内容,让Unity为我们所用。
  • 第二篇中,总结光栅化以及相关算法的实现,以及使用C#实现时一些需要注意的优化。
  • 第三篇中,介绍使用Job System和Burst Compiler加速我们的光栅化代码。
  • 第四篇中,介绍使用Compute Shader在GPU上执行整个软光栅流水线。
    好了,下面开始正文。

框架搭建

关于Unity版本

我使用的是Unity 2020.3.25f1,使用了内置的流水线。实际上,并没有使用特别新的Unity功能。因此更老或更新的Unity版本都可以。

渲染目标和RawImage

为了软光栅最终的渲染结果能显示出来,首先要解决的是渲染目标是什么。对于CPURasterizer,我使用一个普通的Texture2D作为渲染目标,将整个颜色缓冲区通过SetPixels方法设置到Texture2D中。对于GPURasterizer,最终会渲染到一个RenderTexture中。为了展示这些Texture,我使用了一个RawImage,这是个UI对象,放到Canvas下面。RawImage包含了一张Texture,我只要给它设置一个Texture就可以显示Texture的内容。这就是我们的渲染目标,兼容Texture2D和RenderTexture,完美。

CameraRenderer和RenderingObject

虽然我们要实现的是光栅化渲染器,但是首先要让渲染器的代码能跑起来。CameraRenderer就是驱动渲染器的核心部分。
首先是Init方法。

     void Init(){_camera = GetComponent<Camera>();var rootObjs = this.gameObject.scene.GetRootGameObjects();renderingObjects.Clear();foreach(var o in rootObjs){renderingObjects.AddRange(o.GetComponentsInChildren<RenderingObject>());}Debug.Log($"Find rendering objs count:{renderingObjects.Count}");RectTransform rect = rawImg.GetComponent<RectTransform>();rect.sizeDelta = new Vector2(Screen.width, Screen.height);int w = Mathf.FloorToInt(rect.rect.width);int h = Mathf.FloorToInt(rect.rect.height);Debug.Log($"screen size: {w}x{h}");_cpuRasterizer = new CPURasterizer(w, h, _config);_jobRasterizer = new JobRasterizer(w, h, _config);_gpuRasterizer = new GPURasterizer(w, h, _config);_lastRasterizer = null;_statsPanel = this.GetComponent<StatsPanel>();if (_statsPanel != null) {_cpuRasterizer.StatDelegate += _statsPanel.StatDelegate;_jobRasterizer.StatDelegate += _statsPanel.StatDelegate;_gpuRasterizer.StatDelegate += _statsPanel.StatDelegate;}}
  • 顾名思义,CameraRenderer需要获取到一个Camera才能渲染。
  • 然后会收集场景中所有的可渲染物体(RenderingObject)。是的,为了能让Unity的GameObject可以被我们渲染,使用RenderingObject脚本进行标记,并且RenderingObject还会管理渲染器所需要的数据。
  • 之后我们根据屏幕的尺寸去设置RawImage的大小,这样就可以让RawImage和Unity显示窗口一致,便于我们比较渲染结果。
  • 这之后初始化我们的3个渲染器:为了能在运行时动态切换,会将所有的渲染器实例化出来

然后是Render()方法,它被放置到OnPostRender系统回调中执行。这个方法就是驱动渲染器的核心了。

     void Render(){ProfileManager.BeginSample("CameraRenderer.Render");switch(_config.RasterizerType){case RasterizerType.CPU:_rasterizer = _cpuRasterizer;break;case RasterizerType.CPUJobs:_rasterizer = _jobRasterizer;break;case RasterizerType.GPUDriven:_rasterizer = _gpuRasterizer;break;}            if(_rasterizer != _lastRasterizer){Debug.Log($"Change Rasterizer to {_rasterizer.Name}");_lastRasterizer = _rasterizer;rawImg.texture = _rasterizer.ColorTexture;_statsPanel.SetRasterizerType(_rasterizer.Name);   }var r = _rasterizer;r.Clear(BufferMask.Color | BufferMask.Depth);r.SetupUniforms(_camera, _mainLight);for (int i=0; i<renderingObjects.Count; ++i){if (renderingObjects[i].gameObject.activeInHierarchy){                    r.DrawObject(renderingObjects[i]);}}    r.UpdateFrame();            ProfileManager.EndSample();}}

为了参考,同时看一下渲染器接口:

 public interface IRasterizer{string Name { get; }void Clear(BufferMask mask);void SetupUniforms(Camera camera, Light mainLight);void DrawObject(RenderingObject ro);Texture ColorTexture { get; }void UpdateFrame();  void Release();      }
  • 首先,会在渲染器切换(或第一次设置)时,将渲染器的ColorTexture设置给RawImage.
  • 然后再渲染开始,执行Clear
  • 之后我们设置渲染器需要的uniform参数,主要是camera和灯光。
  • 之后我们遍历所有的Rendring Object,对于所有场景中可见的物体进行绘制。
  • 最后我们调用UpdateFrame,刷新我们的Texture。对于GPU Rasterizer,因为是直接绘制到RenderTexture上的,所以这个函数基本没做什么。

RenderingObject和RenderObjectData

对于不同的Rasterizer,我们需要缓存不同的数据,有的是数组的形式,有的是NativeArray,有的是ComputeBuffer。这些数据放到各自的RenderObjectData中。而为了动态切换渲染器,RenderingObject包含了所有三种RenderObjectData。此外,由于RenderingObject是挂在GameObject身上的,因此它知道GameObject的transform,模型和材质, 所以它还负责计算GameObject的模型变换矩阵以及获取模型和材质信息。目前材质部分仅仅获取了一张diffuse贴图,因为最终渲染的shader是我们自定义的,目前只有BlinnPhong材质,理论上可以实现各种材质,只要你愿意,实现PBR也没问题,只要通过RenderingObject从Unity材质中获取各种贴图和参数,然后在自己的渲染器Shader函数中使用即可。由于Shader和渲染效果不是本项目的重点,因此只实现了一个简单的BlinnPhong。

矩阵计算

URasterizer自己计算了模型,视图和投影矩阵,基于GAMES101的约定,但是从Unity获取了camera和transform参数。由于Unity的本地和世界坐标都是左手系,所以必然在获取参数时要进行一些转换。基本就是对于坐标和向量,将z坐标取反。对于旋转,也要将欧拉角的值取反。

视图和投影矩阵计算

矩阵计算相关的算法都放到TransformTool这个类中,其中计算视图和投影矩阵的函数如下:

     public static void SetupViewProjectionMatrix(Camera camera, float aspect, out Matrix4x4 ViewMatrix, out Matrix4x4 ProjectionMatrix){//左手坐标系转右手坐标系,以下坐标和向量z取反var camPos = camera.transform.position;camPos.z *= -1; var lookAt = camera.transform.forward;lookAt.z *= -1;var up = camera.transform.up;up.z *= -1;ViewMatrix = TransformTool.GetViewMatrix(camPos, lookAt, up);if (camera.orthographic){float halfOrthHeight = camera.orthographicSize;float halfOrthWidth = halfOrthHeight * aspect;float f = -camera.farClipPlane;float n = -camera.nearClipPlane;ProjectionMatrix = GetOrthographicProjectionMatrix(-halfOrthWidth, halfOrthWidth, -halfOrthHeight, halfOrthHeight, f, n);}else{ProjectionMatrix = GetPerspectiveProjectionMatrix(camera.fieldOfView, aspect, camera.nearClipPlane, camera.farClipPlane);}}

视图矩阵计算

URasterizer是基于GAME101的约定的,因此各坐标空间都是右手系(包括NDC),且视图空间中camera是看向-z轴。
而Unity的本地和世界坐标都是左手系,因此我们从camera.transform获取到的坐标和方向都要进行转换。转换的方法很简单,就是将z轴取反。然后使用GetViewMatrix计算视图矩阵。代码如下:

     public static Matrix4x4 GetViewMatrix(Vector3 eye_pos, Vector3 lookAtDir, Vector3 upDir){//这儿lookAtDir取反是因为我们使用的view space默认camrea看向(0,0,-1),因此lookAt会被对应到(0,0,-1)//那么-lookAt就对应到(0,0,1)//我们构造的旋转矩阵是将(0,0,1)变换到-lookAt,其逆矩阵就是将-lookAt变换到(0,0,1)Vector3 camZ = -lookAtDir.normalized;Vector3 camY = upDir.normalized;Vector3 camX = Vector3.Cross(camY, camZ);camY = Vector3.Cross(camZ, camX);Matrix4x4 matRot = Matrix4x4.identity;matRot.SetColumn(0, camX);matRot.SetColumn(1, camY);matRot.SetColumn(2, camZ);Matrix4x4 translate = Matrix4x4.identity;translate.SetColumn(3, new Vector4(-eye_pos.x, -eye_pos.y, -eye_pos.z, 1f));Matrix4x4 view = matRot.transpose * translate;return view;}

URasterizer中直接使用了Unity的Matrix4x4,但是并没有使用Unity的方法构造矩阵,只是将Matrix4x4作为一个容器。 根据我们使用的约定,矩阵乘以向量时,向量在乘法右边。这儿计算的matRot是将标准坐标轴变换到camera的朝向的旋转矩阵,所以只要将变换后的3个轴填入矩阵的3个列中即可。然后使用其逆矩阵连接一个平移矩阵,就得到了视图矩阵。注意这儿传入的坐标和方向,已经在上一步中转换到了右手系。
然后我们看投影矩阵的计算。

平行投影矩阵

从Unity的camera中可以很方便的获取到参数,比如 orthographicSize,意义为投影面高度的一半(对于平行投影,前后剪裁面是一样大的),根据aspect可计算出宽度的一半,然后使用GetOrthographicProjectionMatrix计算矩阵:

     //根据ViewSpace下的frustum剪裁面坐标计算正交投影矩阵。//ViewSpace使用右手坐标系,camera看向-Z轴。//所有参数都是坐标值。因此f和n都是负数,且 f < npublic static Matrix4x4 GetOrthographicProjectionMatrix(float l, float r, float b, float t, float f, float n){Matrix4x4 translate = Matrix4x4.identity;translate.SetColumn(3, new Vector4(-(r + l) * 0.5f, -(t + b) * 0.5f, -(n + f) * 0.5f, 1f));Matrix4x4 scale = Matrix4x4.identity;scale.m00 = 2f / (r - l);scale.m11 = 2f / (t - b);scale.m22 = 2f / (n - f);return scale * translate;}

由于这个函数的约定中,参数都是坐标值,而camera是看向-z轴的,因此传入的f和n要将camera的farClipPlane和nearClipPlane值取负。而l,r,b,t则根据前面获取到的半宽和半高计算。

透视投影矩阵

透视投影矩阵使用fov加aspect的方式接受参数,正好可以从camera获取到这些参数:

     //根据FOV等参数计算透视投影矩阵。fov为fov y, aspect_ratio为宽/高,zNear,zFar为距离值(正数)public static Matrix4x4 GetPerspectiveProjectionMatrix(float eye_fov, float aspect_ratio, float zNear, float zFar){float t = zNear * Mathf.Tan(eye_fov * D2R * 0.5f);float b = -t;float r = t * aspect_ratio;float l = -r;float n = -zNear;float f = -zFar;return GetPerspectiveProjectionMatrix(l, r, b, t, f, n);}//根据ViewSpace下的frustum剪裁面坐标计算透视投影矩阵。//ViewSpace使用右手坐标系,camera看向-Z轴。//所有参数都是坐标值。因此f和n都是负数,且 f < n        public static Matrix4x4 GetPerspectiveProjectionMatrix(float l, float r, float b, float t, float f, float n){Matrix4x4 perspToOrtho = Matrix4x4.identity;perspToOrtho.m00 = n;perspToOrtho.m11 = n;perspToOrtho.m22 = n + f;perspToOrtho.m23 = -n * f;perspToOrtho.m32 = 1;perspToOrtho.m33 = 0;var orthoProj = GetOrthographicProjectionMatrix(l, r, b, t, f, n);return orthoProj * perspToOrtho;}

透视投影计算使用了GAMES101介绍的挤压法。其实这里为了效率可以将矩阵计算好了直接填入,不过为了展示还是保留了过程。

模型变换矩阵的计算

如上所述,模型变换矩阵在RenderingObject类中计算,因为RenderingObject可以获取到所在GameObject的transform。

     // TRSpublic Matrix4x4 GetModelMatrix(){if(transform == null){return TransformTool.GetRotZMatrix(0);}var matScale = TransformTool.GetScaleMatrix(transform.lossyScale);var rotation = transform.rotation.eulerAngles;var rotX = TransformTool.GetRotationMatrix(Vector3.right, -rotation.x);var rotY = TransformTool.GetRotationMatrix(Vector3.up, -rotation.y);var rotZ = TransformTool.GetRotationMatrix(Vector3.forward, rotation.z);var matRot = rotY * rotX * rotZ; // rotation apply order: z(roll), x(pitch), y(yaw) var matTranslation = TransformTool.GetTranslationMatrix(transform.position);return matTranslation * matRot * matScale;}

计算模型矩阵,我们必须遵守Unity的约定,使用TRS。并且旋转应用的顺序为 ZXY。因为只有这样才能使用Unity的动画和编辑器Gizmos来移动旋转缩放物体。构造缩放矩阵时需要使用transform.lossyScale才是全局缩放。构造旋转矩阵时要注意,由于手向性的差异,我们仍然要处理旋转的方向,因为左手系和右手系的旋转方向是相反的,另外由于z坐标本身也要取反,所以最终旋转欧拉角的x和y值取反,z不变。构造平移矩阵时,是在内部处理z的取反的。(总结的时候我感觉应该还是统一在外部处理)
其实Unity在内部是使用四元数保存旋转的,这里将四元数转换成欧拉角,然后再构造成旋转矩阵似乎有点麻烦了。应该直接从四元数构造出旋转矩阵。这个留着以后再改吧。主要GAMES101没讲四元数啊。另外旋转矩阵的实现是绕任意轴旋转矩阵,但是目前使用的情况,其实只要分别构造沿3个坐标轴旋转的矩阵即可。我在某引擎代码里看到绕任意轴矩阵的实现时会判断是否是坐标轴,如果是就直接构造,这也是一种优化。同样构造旋转矩阵的函数也是保留了过程,没有直接写结果。

模型数据的手向性转换

我们知道Unity在世界空间使用的是左手坐标系,而GAMES101在所有空间都使用右手坐标系。所以我们从Unity中获取到的Mesh的坐标法线等都要做转换才能使用,并且三角形的环绕方向也要做转换。转换的方法都一样,但转换的位置不同,为了使用数据更方便,有时转换会在Shader函数中进行。这里我仅以CPURasterizer为例。

模型坐标和法线转换

很简单,z值取反,例如CPURasterizer是在做vertex shader时进行处理:

             //Unity模型本地坐标系也是左手系,需要转成我们使用的右手系//1. z轴反转//2. 三角形顶点环绕方向从顺时针改成逆时针// Vertex shader相关代码(由于是CPU上的软件渲染,这儿都是C#代码)var vert = ro.cpuData.MeshVertices[i];        var objVert = new Vector4(vert.x, vert.y, -vert.z, 1); //注意这儿反转了z坐标vsOutput[i].clipPos = mvp * objVert;vsOutput[i].worldPos = _matModel * objVert;var normal = ro.cpuData.MeshNormals[i];var objNormal = new Vector3(normal.x, normal.y, -normal.z);//同样法线也反转了z坐标vsOutput[i].objectNormal = objNormal;vsOutput[i].worldNormal = normalMat * objNormal;

三角形环绕方向修改

由于三角形是使用3个索引值表示的,而Unity模型本地坐标是左手系,因此正面是顺时针环绕,我们这儿要改成逆时针。
CPURasterizer是在图元集成阶段做的。对于一个三角形的3个顶点v0,v1,v2,只要对调了v0和v1的索引即可。即原来的 0,1,2是顺时针的,对调后是 1,0,2是逆时针的。

         var indices = ro.cpuData.MeshTriangles;for(int i=0; i< indices.Length; i+=3){         /// -------------- Primitive Assembly -----------------//注意这儿对调了v0和v1的索引,因为原来的 0,1,2是顺时针的,对调后是 1,0,2是逆时针的//Unity Quard模型的两个三角形索引分别是 0,3,1,3,0,2 转换后为 3,0,1,0,3,2int idx0 = indices[i+1];int idx1 = indices[i]; int idx2 = indices[i+2];  //使用调整后的索引获取三角形三个顶点                                      v[0] = vsOutput[idx0].clipPos;v[1] = vsOutput[idx1].clipPos;v[2] = vsOutput[idx2].clipPos;  }                                

GPURasterizer中是在构造RenderingObject数据时做的调整,可能更清晰些:

         //初始化三角形数组,每个三角形包含3个索引值//注意这儿对调了v0和v1的索引,因为原来的 0,1,2是顺时针的,对调后是 1,0,2是逆时针的//Unity Quard模型的两个三角形索引分别是 0,3,1,3,0,2 转换后为 3,0,1,0,3,2var mesh_triangles = mesh.triangles;int triCnt = mesh_triangles.Length/3;Vector3Int[] triangles = new Vector3Int[triCnt];for(int i=0; i < triCnt; ++i){int j = i * 3;triangles[i].x = mesh_triangles[j+1];triangles[i].y = mesh_triangles[j];triangles[i].z = mesh_triangles[j+2];}

小结

本篇中介绍了URasterizer的框架,他支持多个渲染器,以及渲染器的实时切换。介绍了如何使用Unity提供的数据构造矩阵,以及对Unity模型进行手向性转换。下一篇中将介绍纯C#实现的软件光栅化渲染器,基本就是对GAMES101光栅化相关理论的复现。由于是c#实现,还要注意性能的优化。

基于Unity的软光栅实现(1):框架搭建和矩阵构造相关推荐

  1. 基于Unity的软光栅实现(3):基于Job system的多核加速光栅化

    文章目录 系列文章导航 拥抱CPU多核计算 Job System简介 ParallelFor Job JobRasterizer 数据准备:JobRenderObjectData 缓冲区表示和Clea ...

  2. 基于深度学习的中文语音识别系统框架搭建

    基于深度学习的中文语音识别系统框架 转自@https://blog.csdn.net/chinatelecom08/article/details/82557715 本文搭建一个完整的中文语音识别系统 ...

  3. python ui自动化测试框架_基于python语言下的UI自动化测试框架搭建(一)

    最近在搭一个UI自动化测试框架,想把整个搭建过程分享出来,如果有不对的地方,希望大家能够指正,首先创建一个名称为,antomation_framework_demo的工程文件, pycharm中工程及 ...

  4. 基于python语言下的UI自动化测试框架搭建(四)

    testsuits:案例执行 创建baidu_search1.py文件,这里会展示两种执行方式,一种是直接调用base_page中封装好的常用操作方法,另外一种是先调用baidu_homepage.p ...

  5. 简易版GameFramework游戏框架搭建教程(一)前言

    GameFramework(以下简称GF),是由Ellan开发的一款开源Unity游戏框架,以下是来自官网的简介: Game Framework 是一个基于 Unity 5.3+ 引擎的游戏框架,主要 ...

  6. 软光栅个人项目介绍,编写思路及后期整理

    软光栅项目主要是在学习了计算机图形学相关知识之后,主要是Games101,202,tiny shader等教程,然后借鉴了很多前辈们的思路和框架逻辑编写的. 渲染帧率来说,blinn-phong渲染维 ...

  7. 携程基于云的软呼叫中心及客服平台架构实践

    https://www.infoq.cn/article/Q2fTa5JvY0XuI-RG9RMU 背景及设计理念 自携程创立以来,呼叫中心就一直伴随着公司业务一同发展壮大.经过近 20 年的迭代,目 ...

  8. class unity 定义类_Unity 游戏框架搭建 2019 (二十五) 类的第一个作用 与 Obselete 属性...

    在上一篇我们整理到了第七个示例,我们今天再接着往下整理.我们来看第八个示例: #if UNITY_EDITORusing UnityEditor; #endif using UnityEngine; ...

  9. Unity 游戏框架搭建 (七) 减少加班利器-QApp类

    本来这周想介绍一些框架中自认为比较好用的小工具的,但是发现很多小工具都依赖一个类----App. App类的职责: 1.接收Unity的生命周期事件. 2.做为游戏的入口. 3.一些框架级别的组件初始 ...

  10. Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

    在本系列的第一篇随笔<Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)>中介绍了Entity Framework 实体框架的一些基础知识,以及构建 ...

最新文章

  1. oracle中“ORA-00060: 等待资源时检测到死锁” 或存储过程编译卡死 解决方法
  2. axios封装_VUE.JS请求工具Axios的封装
  3. KMP算法详解及各种应用
  4. BlockJUnit4ClassRunner
  5. 《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一1.2.2 内存...
  6. vuedraggable示例_vuedraggable快速入门
  7. 看下资深架构师平时需要解决的问题,对比你离资深架构师还有多少距离——再论技术架构的升级之路...
  8. 知道草根如何逆袭吗?
  9. sql组合索引和独立索引_SQL索引概述和策略
  10. LINUX mysql 源码安装
  11. UIKit Dynamic主题学习笔记
  12. 有什么低价好用的电容笔推荐?ipad可以用的手写笔分享
  13. 【技术】jquery暂无图片替换
  14. 微信浏览器打开APP
  15. 一个STAF的RC21的问题的解决和思考
  16. Linux CFS调度算法核心解析
  17. Android 万能遥控 开源,快速实现WIFI红外遥控器(ESP8266 SoC模式)
  18. 【多线程】多线程基础知识
  19. 「Adobe国际认证」平面设计师的,终极排版术语综合指南,都包含了哪些设计要点?
  20. Typora markdown语法基础教程

热门文章

  1. 计算机课怎样制作ppt 课件,如何制作PPT课件视频
  2. ENVI5.2裁剪遥感图像指定区域
  3. VoLTE SIP代码意义及流程图解
  4. VolTE注册流程0001 融合HLR HSS
  5. 三款常用IP发包工具介绍
  6. #define c# 报错_天轰穿C#教程之#define和#undef【原创】
  7. 电阻式触摸屏和电容式触摸屏区别
  8. STC 51单片机仿真总结
  9. matlab程序代码 伪码捕获_GNSS_SDR_a 实现北斗卫星的伪随机码产生和捕获跟踪,其中主函数为initial 。 matlab 262万源代码下载- www.pudn.com...
  10. android7+预装+卸载,国内安卓用户救星:7月1日起,手机预装App必须支持卸载!