@TOC
代码仓库入口:
系列文章规划:
- (OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(1):从开发的视角看下CAD画出那些好看的图形们))
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(2):看似"老派"的 C++ 底层优化,恰恰是这些前沿领域最需要的基础设施)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(3):你的 CAD 终于能画标准零件了,但用户想要"弧面"、"流线型",怎么办?)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(4):GstarCAD / AutoCAD 客户端相关产品 ------ 深入骨髓的数据库哲学)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(5)番外篇:给 CAD 加上"控制台"------让用户能实时"调参数、看性能")
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(6)番外篇:让视图"活"起来------鼠标拖拽、缩放背后的数学魔法
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(7)-番外篇:点击的瞬间,发生了什么?
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(8)-番外篇:当你的 CAD 遇上"活"的零件)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(1)-当你的CAD想"联网"时:从单机绘图到多人实时协作)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(2)-当你的CAD需要处理"百万个螺栓"时:从内存爆炸到丝般顺滑)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-3-(1):你的 CAD 终于能联网协作了,但渲染的"内功心法"到底是什么?)
巨人的肩膀:
- deepseek
- gemini
当你的CAD学会"偷懒":从"一笔一画"到"一键生成"的OpenGL渲染进化史
故事续章:你的CAD能导入百万面片了,但一旋转就卡成PPT
你刚刚攻克了内存管理,CAD能加载500MB的STL文件了。你兴奋地向老板演示:一个精美的汽车外壳模型在屏幕上缓缓旋转。
"等等,"老板皱起眉头,"为什么转起来像幻灯片?你看隔壁Blender,同样是百万面片,人家转得跟德芙一样丝滑。"
你愣住了。你不是已经用了最先进的C++、最牛的数据结构了吗?为什么渲染还是卡?
你打开性能分析器,发现一个惊人的事实:你的CPU 70%的时间不是在计算几何,而是在等待和GPU说话。
每一帧,你都这样画图:
cpp
for(int i = 0; i < 1000000; i++) { // 一百万个三角形
glBegin(GL_TRIANGLES);
glVertex3f(...); // 传一个点给GPU
glVertex3f(...); // 再传一个点
glVertex3f(...); // 再传一个点
glEnd();
}
你每画一个三角形,就要和GPU打三次招呼。一百万个三角形,就是一千万次"喂饭"。CPU就像一个忙前忙后的保姆,GPU这个"大厨"却大部分时间在等菜下锅。
你意识到,你用的这套方法,是OpenGL "上古时代" 的遗产。要解决性能问题,你必须理解OpenGL这三十年的进化史------这不仅仅是一堆API的堆砌,而是一场关于 "数据如何从CPU搬到GPU,并最终变成像素" 的认知革命。
第一阶段:固定渲染管线------GPU是"黑盒管家"
时间 :1992年 ~ 2004年(OpenGL 1.x)
核心逻辑:管家式服务,你下命令,我干活。
回到你刚学图形学的日子。老师教你的第一行OpenGL代码大概长这样:
cpp
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f); // 告诉GPU:接下来用红色
glVertex3f(-0.5f, -0.5f, 0.0f); // 画第一个点
glVertex3f( 0.5f, -0.5f, 0.0f); // 画第二个点
glVertex3f( 0.0f, 0.5f, 0.0f); // 画第三个点
glEnd();
这种方式叫 "立即模式"(Immediate Mode)。OpenGL就像一个全能管家,你每下一个命令,它就立刻执行:
glColor3f→ "好的,我把笔换成红色。"glVertex3f→ "好的,我在这个位置点一个点。"glBegin/glEnd→ "好的,我帮你把这三个点连成一个三角形。"
为什么当时用这个?
因为简单。你不用管GPU内部怎么工作,不用写什么着色器代码,不用理解显存。就像你给餐厅服务员点菜:"来一盘红烧肉,多放糖。"你不需要知道厨房里怎么炒。
为什么你画STL模型时"不行了"?
你的汽车外壳有100万个三角形。如果用立即模式,每一帧你都要:
- CPU循环100万次。
- 每次循环调用3次
glVertex3f和若干状态设置函数。 - 每次函数调用都是一次 "CPU→GPU通信"。
总通信次数:100万 × 3 = 300万次函数调用,每帧。
而且这300万次调用发生在 每一帧 (60fps就是每秒1.8亿次调用)。CPU光忙着给GPU传话,根本没时间做别的事。GPU大部分时间在等CPU说完下一句话------这就是 "CPU瓶颈"。
固定管线的另一个问题:你没法"自定义"。
光照怎么算?OpenGL内置了Phong模型。阴影怎么画?对不起,固定管线不支持动态阴影。你想实现一个酷炫的描边效果?想根据法线方向动态改变颜色?固定管线说:"我只提供红烧肉和酸菜鱼,别的菜不会做。"
这就是为什么你发现,用老方法画出来的STL模型,永远是那种"塑料感"------因为光照算法是写死的。
深度扩展:固定渲染管线的技术全景
1. 固定管线的内部流程
OpenGL 1.x的渲染管线是一个严格顺序的黑盒:
顶点数据 → 变换与光照(T&L) → 图元装配 → 光栅化 → 纹理采样 → 雾效/混合 → 帧缓冲每一阶段都由硬件固定逻辑完成,开发者只能通过
glEnable/glDisable和少量参数控制。2. 关键函数剖析
函数 作用 性能代价 glBegin/glEnd标记一个图元批次的开始和结束 每次调用触发GPU状态机切换 glVertex3f提交一个顶点(立即被GPU消费) CPU-GPU总线传输开销极大 glNormal3f设置当前顶点法线(用于光照) 与顶点一一对应,带宽加倍 glTexCoord2f设置当前顶点纹理坐标 同上 3. 立即模式的内存模型
数据流是"推"模式:CPU主动将每个顶点的数据"推"给GPU。没有显存驻留概念,每一帧都重新推送。这在处理几百个顶点时无感,但工业级模型动辄百万顶点,立刻成为灾难。
4. 显示列表------早期优化尝试
为了缓解立即模式的性能问题,OpenGL 1.1引入了 显示列表(Display List):
cppGLuint list = glGenLists(1); glNewList(list, GL_COMPILE); // ... 在这里画图 ... glEndList(); // 渲染时:glCallList(list);显示列表将一组GL命令 录制 下来,存储在GPU驱动层(甚至部分在显存中)。后续渲染只需
glCallList,减少了函数调用开销。局限性:
- 录制后无法修改(静态数据)。
- 不同厂商实现差异大,某些驱动下反而更慢。
- 无法利用GPU并行处理的优势(因为数据还是一个个顶点传)。
5. 为什么固定管线最终被淘汰?
- 硬件发展:GPU从固定功能单元演进为通用并行处理器(SIMD架构),可编程性成为必然。
- 画质需求:电影级光照、阴影、后处理特效(HDR、Bloom、SSAO)无法用固定公式表达。
- 性能极限:CPU-GPU通信带宽成为瓶颈,必须将数据常驻显存。
6. 固定管线在今天的残余
OpenGL 3.0引入了 弃用机制(Deprecation) ,OpenGL 3.2+的 Core Profile 完全移除了
glBegin/glEnd、显示列表、矩阵堆栈(glMatrixMode、glTranslatef等)。但你依然可以通过 Compatibility Profile 使用它们,主要用于维护上古代码。7. 从固定管线到可编程管线的思维转变
概念 固定管线思维 可编程管线思维 数据 一帧一帧喂给GPU 初始化时上传到显存,常驻 计算 GPU内置公式 开发者编写Shader,完全可控 坐标系 内置模型视图/投影矩阵栈 手动管理矩阵,通过Uniform传递 光照 glLightfv设置参数在Shader中实现任意光照模型
第二阶段:可编程管线与着色器------GPU从"黑盒"变"白板"
时间 :2004年 ~ 2010年(OpenGL 2.x ~ 3.x早期)
核心逻辑:权力下放,我给你一个"白板",你自己写GPU程序。
"如果能自己控制每个顶点怎么变换、每个像素怎么上色,该多好?"------这是2000年代图形开发者的共同心声。
于是,GLSL(OpenGL Shading Language,着色器语言) 诞生了。从此,GPU不再是只能做固定几道菜的厨师,而是一个可以执行你编写的任意程序的 并行计算机。
顶点着色器:改变几何的魔法
你写了一个最简单的顶点着色器:
glsl
// vertex_shader.glsl
#version 120
attribute vec3 position; // 从CPU传来的顶点坐标
void main() {
gl_Position = gl_ModelViewProjectionMatrix * vec4(position, 1.0);
}
它告诉你:每个顶点都要经过这个程序处理,用内置的模型视图投影矩阵把它变换到屏幕上。
片段着色器:逐像素的绘画板
你又写了一个片段着色器:
glsl
// fragment_shader.glsl
#version 120
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 所有像素都是红色
}
现在,你可以控制每一个像素的颜色了。想根据光照计算颜色?把光源位置作为 uniform 变量传进来,在片段着色器里计算漫反射、高光。想加纹理?传一张图进来,用 texture2D 采样。
你尝试用这套新方法渲染你的STL模型:
- 你还是用
glVertexPointer把顶点数组地址告诉OpenGL。 - 你还是用
glDrawArrays一次性提交所有顶点。 - 但你现在可以通过Shader实现金属质感、边缘光、甚至卡通渲染。
然而,帧率还是没上去。
你百思不得其解:我已经用了Shader,为什么还是卡?
你用GPU性能分析工具一看,恍然大悟:虽然你一次提交了所有顶点(glDrawArrays),但这些顶点数据 还是存在CPU内存里。每一帧,GPU都要通过PCIe总线从CPU内存把数据拷过来。
100万个顶点 × 每个顶点(位置3个float + 法线3个float)= 24MB数据。
每帧拷24MB,60fps就是每秒1.44GB的PCIe带宽占用。
虽然比立即模式的"每次拷一个点"快了很多,但每一帧都重复拷贝 依然是巨大的浪费。你的STL模型在加载后从未改变,为什么不能让它 常驻在GPU显存里?
深度扩展:可编程管线的技术全景
1. 可编程管线的阶段划分
OpenGL 2.0(2004年)正式引入GLSL 1.10,标志着可编程管线的开始。但此时仍兼容固定管线。OpenGL 3.0(2008年)开始标记大量函数为"弃用",OpenGL 3.2(2009年)引入 Core Profile,彻底移除固定管线功能。
2. 着色器类型详解
着色器 作用 输入 输出 顶点着色器 处理每个顶点的空间变换、属性传递 顶点属性(位置、法线、UV等) gl_Position、out变量片段着色器 处理每个像素(片段)的最终颜色 插值后的 in变量、Uniform、纹理gl_FragColor(或out变量)几何着色器(OpenGL 3.2+) 在图元级别生成/销毁顶点 完整图元(点/线/三角形) 零个或多个新图元 曲面细分着色器(OpenGL 4.0+) 动态细分曲面,增加几何细节 面片(Patch) 细分后的顶点 计算着色器(OpenGL 4.3+) 通用GPU计算,不直接参与渲染 任意缓冲区/纹理 任意缓冲区/纹理 3. GLSL语言核心概念
- 存储限定符 :
attribute:顶点属性(仅顶点着色器,GLSL 1.30后改为in)。uniform:全局常量,所有顶点/片段共享(如变换矩阵、光源位置)。varying:顶点着色器输出,经插值后传给片段着色器(GLSL 1.30后改为out/in)。- 内置变量 :
gl_Position:顶点着色器必须写入的裁剪空间坐标。gl_FragCoord:片段着色器可读取的屏幕空间坐标。gl_FragDepth:可写入的自定义深度值。- 数据类型 :
vec2/3/4、mat2/3/4、sampler2D、samplerCube等。4. 客户端顶点数组------过渡方案
在VBO普及之前,开发者使用 客户端顶点数组(Client-side Vertex Arrays):
cppglEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vertexArray); glDrawArrays(GL_TRIANGLES, 0, vertexCount);这比立即模式快很多,因为只需一次函数调用。但数据仍存在CPU内存,每帧都被DMA到GPU。这是你第二阶段遇到的问题根源。
5. Uniform变量与状态管理
着色器通过Uniform变量接收外部数据。设置Uniform需要先获取其位置:
cppGLint loc = glGetUniformLocation(program, "modelMatrix"); glUniformMatrix4fv(loc, 1, GL_FALSE, &matrix[0][0]);Uniform值在多次Draw Call之间保持不变,直到被显式修改。这鼓励开发者按材质/模型分组渲染,减少状态切换。
6. 从固定管线迁移到可编程管线的典型陷阱
- 矩阵堆栈消失 :
glMatrixMode、glLoadIdentity、glTranslatef等全部失效,必须自己用数学库(如GLM)构建矩阵,并通过Uniform上传。- 光照状态失效 :
glLightfv、glMaterialfv不再有效,必须在Shader中实现光照方程。- 纹理环境消失 :
glTexEnvi的混合模式需在片段着色器中手动计算。7. 为什么可编程管线仍然不够现代?
核心问题:数据仍在CPU内存和GPU显存之间来回拷贝 。即使你用
glVertexPointer一次性提交,每次glDrawArrays都会触发一次DMA传输。对于静态模型(如STL),这是极大的浪费。解决方案是将数据 一次性上传并驻留显存------这就是VBO/VAO的使命。
第三阶段:现代OpenGL------GPU的"内存革命"
时间 :2008年至今(OpenGL 3.0+ Core Profile)
核心逻辑:内存管理至上,数据一次存储,多次使用。
你终于找到了病根:重复的CPU→GPU数据传输 。
处方也很明确:让数据"住"在GPU里。
这就需要引入现代OpenGL的三大核心概念:
VBO:GPU显存里的"数组"
VBO(Vertex Buffer Object,顶点缓冲对象) 就是OpenGL在显存里给你开辟的一块"数组"。你可以把STL模型的所有顶点、法线、纹理坐标一次性拷贝进去,然后它就 一直待在显存里,直到你告诉OpenGL"可以删了"。
cpp
// 1. 创建VBO
GLuint vbo;
glGenBuffers(1, &vbo);
// 2. 绑定VBO到GL_ARRAY_BUFFER目标
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 3. 把CPU内存里的顶点数据拷贝到GPU显存
glBufferData(GL_ARRAY_BUFFER, dataSize, vertexData, GL_STATIC_DRAW);
GL_STATIC_DRAW 告诉OpenGL:"这数据以后不会经常改,你尽管优化。"
从此,每次渲染时,CPU只需要说一句:
cpp
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
GPU直接从自己的显存里读取顶点数据,完全不占用PCIe总线。你的STL模型终于可以流畅旋转了!
VAO:数据"说明书"
但很快你又遇到了新麻烦。你的程序里有很多不同的模型:有的只有位置信息,有的还有法线、颜色、纹理坐标。每次渲染前,你都要用一大堆 glVertexAttribPointer 告诉OpenGL:"第一个VBO里存的是位置,偏移量是0,每个顶点3个float;第二个VBO里存的是法线,偏移量是......"。
这很繁琐,而且容易出错。
于是,VAO(Vertex Array Object,顶点数组对象) 登场了。它就像一个 "数据说明书",记录了:
- 哪些VBO被绑定了
- 每个VBO里的数据是怎么排布的(位置?法线?颜色?)
- 从哪里开始读,步长是多少
cpp
// 初始化时:
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao); // 开始"录制"
glBindBuffer(GL_ARRAY_BUFFER, vbo_position);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo_normal);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glBindVertexArray(0); // 结束"录制"
渲染时,只需要一行:
cpp
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
VAO帮你记住了一切配置,代码变得干净、高效。
你的Huhb3D-Viewer标准化流程
现在,你已经完全掌握了现代OpenGL的精髓。在你的STL查看器项目中,你建立了这样的标准流程:
初始化阶段(程序启动时,只执行一次):
- 解析STL文件,提取所有顶点坐标和法向量。
- 创建并绑定VAO。
- 创建VBO,将顶点数据上传至显存(
GL_STATIC_DRAW)。 - 配置顶点属性指针(
glVertexAttribPointer)。 - 解绑VAO。
渲染阶段(每一帧):
- 绑定Shader程序(
glUseProgram)。 - 设置Uniform(如MVP矩阵、光源位置)。
- 绑定VAO(
glBindVertexArray)。 - 调用
glDrawArrays绘制。 - 解绑VAO(可选)。
CPU和GPU各司其职,互不拖累。你的CAD终于可以像商业软件一样流畅处理百万级面片了。
深度扩展:现代OpenGL(Core Profile)技术全景
1. Core Profile vs Compatibility Profile
OpenGL 3.2引入了Profile机制:
- Core Profile:移除所有弃用功能,强制使用现代方式(VBO/VAO/Shader)。这是新项目的唯一正确选择。
- Compatibility Profile:保留固定管线函数,允许混用,但不保证性能最优,且未来可能被移除。
2. VBO深度剖析
2.1 内存类型与性能建议
glBufferData的最后一个参数usage是性能提示,驱动据此决定数据放在哪类显存:
提示值 含义 适用场景 显存位置建议 GL_STATIC_DRAW数据一次修改,多次使用 静态模型(如STL) 显存(VRAM) GL_DYNAMIC_DRAW数据多次修改,多次使用 动画顶点 驱动管理,可能用可写VRAM GL_STREAM_DRAW数据每次渲染都修改 粒子系统 系统内存,DMA到GPU 2.2 更新VBO数据的正确姿势
如果需要动态更新VBO内容(如变形动画),应使用:
cppglBufferSubData(GL_ARRAY_BUFFER, offset, size, newData);这比
glBufferData重新分配更高效(避免显存重分配)。更高级的用法是 双缓冲VBO + fence同步,避免CPU写入时GPU正在读取造成的停顿。
2.3 顶点数据的交错布局(Interleaved)vs 分离布局(Separate)
- 分离布局:位置VBO + 法线VBO + UV VBO。
- 交错布局 :一个VBO存储
[pos, normal, uv]交错排列。现代GPU更偏好交错布局,因为缓存局部性更好。配置时设置正确的
stride即可。3. VAO深度剖析
3.1 VAO内部状态机
VAO保存的内容包括:
glEnableVertexAttribArray/glDisableVertexAttribArrayglVertexAttribPointer的所有参数(包括绑定的VBO)glVertexAttribDivisor(实例化渲染用)- 索引缓冲(EBO)绑定状态
3.2 常见陷阱
- 忘记绑定VAO就配置属性:VAO必须在绑定状态下"录制"配置,否则配置将丢失或写入错误的VAO。
- 在VAO未绑定时调用
glEnableVertexAttribArray:这是全局状态,容易污染其他VAO。- 多个VAO共享同一个VBO:合法且高效。例如,多个子模型共享同一个顶点数据VBO,但用不同的VAO描述不同属性组合。
4. 索引缓冲与EBO
对于封闭网格,大部分顶点被多个三角形共享。如果直接用
glDrawArrays,共享顶点会重复存储。EBO解决这个问题:
cppGLuint ebo; glGenBuffers(1, &ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize, indices, GL_STATIC_DRAW); // 绘制时: glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);EBO也是VAO状态的一部分。绑定VAO后,EBO绑定状态也被记录下来。
5. DSA:直接状态访问(OpenGL 4.5+)
传统VBO/VAO操作需要先绑定到目标,再操作。这导致大量状态切换。OpenGL 4.5引入 DSA(Direct State Access),允许直接操作对象:
cppglCreateBuffers(1, &vbo); glNamedBufferStorage(vbo, size, data, GL_DYNAMIC_STORAGE_BIT); glVertexArrayVertexBuffer(vao, bindingIndex, vbo, offset, stride);DSA大幅简化代码,减少错误,是未来方向。但需要考虑兼容性(macOS最高支持OpenGL 4.1)。
6. 多VBO与实例化渲染
对于大量重复物体(如1000个螺栓),实例化渲染是必杀技:
cpp// 在VAO中设置每个实例的属性(如变换矩阵) glVertexAttribDivisor(instanceMatrixLoc, 1); // 绘制1000个实例 glDrawArraysInstanced(GL_TRIANGLES, 0, vertexCount, 1000);GPU自动为每个实例复制一份几何数据,并传入不同的实例属性。这正是你CAD中绘制大量相同零件时的最佳实践。
7. 同步与性能陷阱
- 隐式同步 :
glGet*系列函数会强制CPU等待GPU完成所有之前命令,造成流水线停顿。应完全避免在渲染循环中调用。- 状态切换开销 :
glBindVertexArray、glUseProgram、glBindTexture等状态切换有一定开销。应尽量按状态分组渲染(例如所有红色材质物体一起绘制)。- Draw Call数量 :每次
glDrawArrays/glDrawElements都是一次Draw Call。现代GPU能承受数千次,但上万次仍会瓶颈。应使用批处理(Batching)合并多个模型。8. 向前兼容与现代替代方案
OpenGL正逐渐被 Vulkan 和 Direct3D 12 取代。这些新API提供了更底层的显存控制、更少的驱动开销、更好的多线程支持。但学习曲线陡峭。对于大多数CAD应用,OpenGL Core Profile依然是最平衡的选择------性能足够,生态成熟,跨平台好。
9. 在Huhb3D-Viewer中的实践建议
- 加载阶段:STL文件不包含索引,顶点是独立的(每个三角形3个独立顶点)。你可以直接构建VBO,无需EBO(除非你做顶点去重优化)。
- 多模型管理 :每个STL文件对应一个VAO + VBO对。用
std::unordered_map<string, ModelData>管理。- Shader设计:至少需要简单的顶点着色器(MVP变换+法线传递)和片段着色器(基于法线的简单光照)。
- 性能验证:用RenderDoc抓帧,确认VBO数据只上传一次,Draw Call数量合理,没有隐式同步。
尾声:你现在站在了巨人的肩膀上
你从"每帧给GPU喂饭"的立即模式,一路走到"数据常驻显存"的现代OpenGL。你终于理解了,为什么你写的第一个STL查看器卡成PPT------你用了三十年前的设计模式去处理二十一世纪的数据规模。
现在,当你写 glGenBuffers、glBindVertexArray 时,你不再是盲目地复制粘贴代码,而是在做一次精心设计的数据编排:
- 哪些数据只传一次? → 静态VBO。
- 哪些数据每帧要变? → Uniform或动态VBO。
- 如何组织数据让GPU读写最快? → 交错布局、对齐优化。
- 如何减少状态切换? → 按材质/Shader分组渲染。
而OpenGL的进化史,也让你看到一条软件工程的铁律:任何抽象层的出现,都是为了解决上一个抽象层在特定规模下暴露的痛点。 从固定管线的"方便"到可编程管线的"灵活",再到现代管线的"高效",每一步都是对硬件能力、数据规模、开发者需求的深刻回应。
未来,当你面对Vulkan的复杂配置,或是研究AI渲染(如DLSS)时,你会感激今天你对OpenGL底层机制的理解------因为万变不离其宗,计算机图形学的核心永远是"数据如何高效地变成像素"。
-
如果想了解一些成像系统、图像、人眼、颜色等等的小知识,快去看看视频吧 :
- 抖音:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 快手:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- B站:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 认准一个头像,保你不迷路:

- 认准一个头像,保你不迷路:
-
您要是也想站在文章开头的巨人的肩膀啦,可以动动您发财的小指头,然后把您的想要展现的名称和公开信息发我,这些信息会跟随每篇文章,屹立在文章的顶部哦
