openGL學習(Shader)

認識Shader

在計算機圖形學中,Shader(著色器)是一種運行在 GPU(圖形處理單元)上的程序,用于控制圖形渲染過程中頂點和像素的處理。著色器是 OpenGL、Direct3D、Vulkan 等圖形 API 的核心組成部分,它們允許開發者自定義渲染管線的各個階段,從而實現高度自定義的視覺效果。

著色器類型

  1. 頂點著色器(Vertex Shader)

    • 處理每個頂點的數據。

    • 可以進行頂點位置的變換、紋理坐標的生成、光照計算等。

    • 通常用于實現 3D 變換、動畫效果等。

  2. 片段著色器(Fragment Shader)

    • 處理每個像素(或片段)的數據。

    • 決定每個像素的顏色和透明度。

    • 用于實現光照、陰影、紋理映射、后處理效果等。

  3. 幾何著色器(Geometry Shader)

    • 在頂點著色器和片段著色器之間處理整個圖元(如點、線、三角形)。

    • 可以生成新的頂點或圖元。

    • 用于實現高級的幾何變換和效果。

  4. 張量著色器(Tessellation Shader)

    • 包括兩個階段:域著色器(Domain Shader)和原語生成著色器(Hull Shader)。

    • 控制細分曲面的生成和處理。

    • 用于實現復雜的曲面細分和建模。

  5. 計算著色器(Compute Shader)

    • 執行通用計算任務,不直接參與渲染管線。

    • 可以并行處理大量數據。

    • 用于實現物理模擬、圖像處理、AI 計算等。

著色器編寫

著色器通常使用 GLSL(OpenGL Shading Language)或 HLSL(High-Level Shader Language)等語言編寫。以下是一個簡單的 GLSL 頂點著色器示例:

#version 330 core// in代表輸入 
//vec3代表是一個三維向量(xyz)
// aPos 是我們自己取的名字
// layout(location = n) 代表告訴vertexShaser去vao的第n個屬性描述中去取數據。
// gl_Position 是glsl的內置變量,負責向后續階段輸出頂點位置處理的結果。 一般為NDC坐標。
//vec4代表 四維向量。layout(location = 0) in vec3 aPos;  // 頂點位置void main() {gl_Position = vec4(aPos, 1.0);  // 將頂點位置傳遞給片段著色器
}

以及一個簡單的 GLSL 片段著色器示例:

#version 330 core//out代表輸出變量
//vec4代表四維向量,rgba,紅綠藍,透明度
//FragColor 是最終輸出的變量
out vec4 FragColor;  // 輸出顏色void main() {FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // 設置像素顏色為橙色
}

著色器編譯和鏈接

在 OpenGL 中,著色器需要經過編譯和鏈接才能使用。以下是一個簡化的流程:

  1. 編寫著色器代碼:使用 GLSL 編寫頂點著色器和片段著色器代碼。

  2. 創建著色器對象:使用 glCreateShader 創建著色器對象。

  3. 編譯著色器:使用 glShaderSourceglCompileShader 編譯著色器代碼。

  4. 創建程序對象:使用 glCreateProgram 創建程序對象。

  5. 附加著色器:使用 glAttachShader 將編譯好的著色器附加到程序對象。

  6. 鏈接程序:使用 glLinkProgram 鏈接程序對象。

  7. 使用程序:使用 glUseProgram 使用鏈接好的程序對象。

著色器的優勢

  1. 高度自定義:開發者可以自定義渲染管線的各個階段,實現復雜的圖形效果。

  2. 性能優化:著色器在 GPU 上運行,可以利用 GPU 的并行處理能力,提高渲染性能。

  3. 跨平臺兼容性:著色器語言(如 GLSL)在不同的圖形 API 和硬件平臺上具有較好的兼容性。

總之,著色器是現代圖形渲染中不可或缺的一部分,它們為開發者提供了強大的工具來實現高度自定義的視覺效果。

void  prepareShader()
{cout << "prepareShader()" << endl;//1.完成vs和fs,并且裝字符串const char* vertexShaderSource ="#version 460 core\n""layout(location = 0) in vec3 aPos;\n""void main()\n""{\n""gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);\n""}\n\0";const char* fragmentShaderSource ="#version 330 core\n""out vec4 FragColor;\n""void main()\n""{\n""FragColor = vec4(1.0f,0.5f,0.2f,1.0);\n""}\n\0";//2創建Shader程序(vs和fs)GLuint vertex, fragment;vertex = glCreateShader(GL_VERTEX_SHADER);fragment = glCreateShader(GL_FRAGMENT_SHADER);//3為shader程序輸入Shader代碼glShaderSource(vertex, 1, &vertexShaderSource, NULL); //字符串用\0進行結尾,不需要告訴他長度。glShaderSource(fragment, 1, &fragmentShaderSource, NULL); //字符串用\0進行結尾,不需要告訴他長度。int success = 0;char infoLog[1024];//4 執行Shader編譯glCompileShader(vertex);//5 檢查是否正確編譯glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetShaderInfoLog(vertex, 1024, NULL, infoLog); //獲取日志信息cout << "Error Vertex Shaser Complie: " << infoLog << endl;}//4 執行fragment編譯glCompileShader(fragment);//5 檢查是否正確編譯glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetShaderInfoLog(vertex, 1024, NULL, infoLog); //獲取日志信息cout << "Error Fragment Shader Complie: " << infoLog << endl;}// 創建Program殼子GLuint program = 0;program =	glCreateProgram();//將編譯后的結果放進ProgramglAttachShader(program, vertex);glAttachShader(program, fragment);// 執行program鏈接操作,形成最終的shader程序。glLinkProgram(program);//檢查鏈接錯誤glGetProgramiv(program, GL_LINK_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetProgramInfoLog(program, 1024, NULL, infoLog); //獲取日志信息cout << "Error Program Link: " << infoLog << endl;}//清理glDeleteShader(vertex);glDeleteShader(fragment);}
#include <iostream>
//#include "thrirdParty/include/GLFW/glfw3.h" 
#include "glad/glad.h"   //需要先引用glad的頭文件。 用于加載 OpenGL 函數指針
#include "GLFW/glfw3.h" // 用于創建窗口和處理輸入。	
#include <assert.h>
#include "wrapper/checkerror.h"
#include "application/application.h"
using namespace std;/*
* 目標:學習DrawArray進行繪制
* 1.采用GL_TRANGLES進行繪制三角形
* 這里可以縮放窗體,實驗NDC坐標的作用。
* prepareVAOForGLTriangles:構建四個頂點的VAO
*  
*/GLuint vao, program;void prepareSingleBuffer()
{//1.準備頂點位置數據與顏色數據float positions[] = {-0.5f,-0.5f,0.0f,0.5f,-0.5f,0.0f,0.0f,0.5f,0.0f,};float colors[] = {1.0f,0.0f,0.0f,0.0f,1.0f,0.0f,0.0f,0.0f,1.0f,};//2.為位置&顏色數據各自生成一個VBOGLuint posVbo = 0, colorVbo = 0;GL_CALL(glGenBuffers(1, &posVbo));GL_CALL(glGenBuffers(1, &colorVbo));//3.給兩個分開的VBO各自填充數據//positions填充數據GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, posVbo));GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW));//colors填充數據GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, colorVbo));GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW));//生成VAO并且綁定GLuint vao = 0;glGenVertexArrays(1, &vao);glBindVertexArray(vao);// 分別將數據放入VAO//描述位置屬性glBindBuffer(GL_ARRAY_BUFFER, posVbo);//只有綁定了vbo,下面的屬性描述才于此有關系glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),0);glBindBuffer(GL_ARRAY_BUFFER, colorVbo);//只有綁定了vbo,下面的屬性描述才于此有關系glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);glBindVertexArray(0); //將VAO進行解綁}void prepareInterLeavedBuffer()
{cout << "prepareInterLeavedBuffer()" << endl;float vertices[] = {-0.5f,-0.5f,0.0f,1.0f,0.0f,0.0f,0.5f,-0.5f,0.0f,0.0f,1.0f,0.0f,0.0f,0.5f,0.0f,0.0f,0.0f,1.0f};//2.為位置&顏色數據各自生成一個VBOGLuint vbo = 0;GL_CALL(glGenBuffers(1, &vbo));GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, vbo));GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW));//生成VAO并且綁定//GLuint vao = 0;glGenVertexArrays(1, &vao);glBindVertexArray(vao);// 分別將數據放入VAO//描述位置屬性glBindBuffer(GL_ARRAY_BUFFER, vbo);//只有綁定了vbo,下面的屬性描述才于此有關系glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);//顏色屬性//glBindBuffer(GL_ARRAY_BUFFER, vbo);//只有綁定了vbo,下面的屬性描述才于此有關系glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float),(void*)(3*sizeof(float)));glBindVertexArray(0); //將VAO進行解綁
}void prepareVAOForTriangles()
{cout << "prepareVAOForTriangles()" << endl;//目前暫時不用顏色float vertices[] = {-0.5f,-0.5f,0.0f,0.5f,-0.5f,0.0f,0.0f,0.5f,0.0f,0.5f,0.5f,0.0f,0.8f,0.8f,0.0f,0.8f,0.0f,0.0f};//2.生成一個VBOGLuint vbo = 0;GL_CALL(glGenBuffers(1, &vbo));GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, vbo));GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW));//生成VAO并且綁定glGenVertexArrays(1, &vao);glBindVertexArray(vao);// 分別將數據放入VAO//描述位置屬性glBindBuffer(GL_ARRAY_BUFFER, vbo);//只有綁定了vbo,下面的屬性描述才于此有關系glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glBindVertexArray(0); //將VAO進行解綁
}void  prepareShader()
{cout << "prepareShader()" << endl;//1.完成vs和fs,并且裝字符串const char* vertexShaderSource ="#version 460 core\n""layout(location = 0) in vec3 aPos;\n""void main()\n""{\n""gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);\n""}\n\0";const char* fragmentShaderSource ="#version 330 core\n""out vec4 FragColor;\n""void main()\n""{\n""FragColor = vec4(1.0f,0.5f,0.2f,1.0);\n""}\n\0";//2創建Shader程序(vs和fs)GLuint vertex, fragment;vertex = glCreateShader(GL_VERTEX_SHADER);fragment = glCreateShader(GL_FRAGMENT_SHADER);//3為shader程序輸入Shader代碼glShaderSource(vertex, 1, &vertexShaderSource, NULL); //字符串用\0進行結尾,不需要告訴他長度。glShaderSource(fragment, 1, &fragmentShaderSource, NULL); //字符串用\0進行結尾,不需要告訴他長度。int success = 0;char infoLog[1024];//4 執行Shader編譯glCompileShader(vertex);//5 檢查是否正確編譯glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetShaderInfoLog(vertex, 1024, NULL, infoLog); //獲取日志信息cout << "Error Vertex Shaser Complie: " << infoLog << endl;}//4 執行fragment編譯glCompileShader(fragment);//5 檢查是否正確編譯glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetShaderInfoLog(vertex, 1024, NULL, infoLog); //獲取日志信息cout << "Error Fragment Shader Complie: " << infoLog << endl;}// 創建Program殼子//GLuint program = 0;program =	glCreateProgram();//將編譯后的結果放進ProgramglAttachShader(program, vertex);glAttachShader(program, fragment);// 執行program鏈接操作,形成最終的shader程序。glLinkProgram(program);//檢查鏈接錯誤glGetProgramiv(program, GL_LINK_STATUS, &success);if (!success) // 0有問題,非0沒有問題{glGetProgramInfoLog(program, 1024, NULL, infoLog); //獲取日志信息cout << "Error Program Link: " << infoLog << endl;}//清理glDeleteShader(vertex);glDeleteShader(fragment);}void render()
{//GL_CALL( glClear(1) );//加上gl_call如果產生錯誤可以打印出來,雖然vs智能提示有問題GL_CALL(glClear(GL_COLOR_BUFFER_BIT));//渲染操作//1. 使用當前的programglUseProgram(program);//綁定當前的vaoglBindVertexArray(vao);//發出繪制指令//繪制三角形// 如果點數不夠,就會連到0,0點  (123 和456)繪制兩組三角形glDrawArrays(GL_TRIANGLES, 0, 6);//繪制的模式// GL_TRIANGLES 繪制三角形//		默認以三個點繪制一組三角形,不夠三個點就不顯示//	GL_TRIANGLE_STRIP://		末尾點數為偶數【n-2 n-1 n】,基數【n-1 n-2 n】 n從0開始//		并且會復用之前的點//	GL_TRIANGLE_FAN:以扇形序列v0為起點,連接三角形。// GL_LINES:繪制直線// GL_LINE_STRIP//	 }void onResize(int width,int height)
{GL_CALL(glViewport(0, 0, width, height));cout << "onResize " << endl;
}void frameBufferSizeCallback(GLFWwindow* win, int width, int height)
{std::cout << "窗體的最新大小為:" << width << "高度為:" << height << std::endl;//更新窗體的大小glViewport(0, 0, width, height);}void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods){// key 字母按鍵碼 // scancode 物理按鍵碼		 // action:0抬起1按下2長按// mods:是否有shift(1)或ctrl(2) cout << "key = " << key << " scancode = " << scancode << " action = " << action << " mods = " << mods << endl;if (key == GLFW_KEY_W){//按下了w}else if (action == GLFW_PRESS){//按下了}else if (action == GLFW_RELEASE){//抬起}}int main(int argc,char**argv)
{cout << "===================================" << endl;if (app->init(800,600) == -1){return -1;}//設置監聽幀緩沖窗口大小回調函數。//glfwSetFramebufferSizeCallback(win, frameBufferSizeCallback);//glfwSetKeyCallback(win, keyCallback);app->setResizeCallBack(onResize);app->setkeyCallBack(keyCallback);//設置OpenGL視口以及清理顏色glViewport(0,0,800,600);glClearColor(0.2f,0.3f,0.3f,1.0f);prepareShader();prepareVAOForTriangles();// 3. 執行窗體循環// while (app->update() == 0 ){render();}// 4. 退出程序前做相關清理app->destroy();cout << "===================================" << endl;//const double M_PI = 3.14159265358979323846;//double radians = M_PI / 2; // 90度,轉換為弧度//double sineValue = sin(radians);//std::cout << "sin(" << radians << ") = " << sineValue << std::endl;//system("pause");return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/88890.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/88890.shtml
英文地址,請注明出處:http://en.pswp.cn/web/88890.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

webpack高級配置

一、了解webpack高級配置&#xff1a; 1、什么是webpack高級配置&#xff1a; 進行 Webpack 優化&#xff0c;讓代碼在編譯或者運行時性能更好 2、webpack優化從哪些方面入手&#xff1a; ① 提升開發體驗&#xff0c;增強開發和生產環境的代碼調試&#xff1a; 如果代碼編寫…

LLM表征工程還有哪些值得做的地方

LLM表征工程還有哪些值得做的地方 在大型語言模型(LLM)的表征工程領域,近年來涌現出多個具有突破性的創新方向,這些方法通過動態調整、多模態融合、結構化記憶增強等技術,顯著提升了模型的適應性、可解釋性和效率。 一、動態自適應表征:從靜態到動態的范式革新 傳統LL…

LabVIEW智能避障小車

?LabVIEW結合 NI、德州儀器&#xff08;TI&#xff09;、歐姆龍&#xff08;Omron&#xff09;等硬件&#xff0c;設計實現了一款具備智能避障、循跡功能的輪式機器人。系統支持手動操控與自主運行兩種模式&#xff0c;通過無線通信實時傳輸傳感器數據與圖像信息&#xff0c;在…

邏輯代數中的基本規則,代入規則和反演規則,對偶規則

本文探討了代入規則在邏輯等式中的應用&#xff0c;解釋了如何通過替換變量來保持等式的正確性&#xff0c;同時介紹了反演規則和對偶規則的概念。代入規則定義:在任何一個包含變量A的邏輯等式中&#xff0c;如果用另一個邏輯式代入式中的所有A位置&#xff0c;則等式依然成立反…

Javaweb使用websocket,請先連上demo好吧!很簡單的!

Javaweb使用websocket先看結構及效果MyWebSocketHandler用于處理消息WebSocketConfig用于配置建聯地址等SchedulerConfig必須配置這個MyWebSocketInterceptor建聯的攔截器SpringBootWebsocketApplication啟動類POM依賴展示效果源碼先看結構及效果 MyWebSocketHandler用于處理消…

文心大模型4.5開源測評:保姆級部署教程+多維度測試驗證

前言&#xff1a;國產大模型開源的破局時刻 2025年6月百度文心大模型4.5系列的開源&#xff0c;標志著國產AI從"技術跟跑"向"生態共建"的關鍵跨越。 文心大模型4.5是百度自主研發的新一代原生多模態基礎大模型&#xff0c;通過多個模態聯合建模實現協同優…

前端學習5:Float學習(僅簡單了解,引出flex)

一、Float基礎概念1. 設計初衷&#xff1a; float最初是為實現文字環繞圖片的效果&#xff08;類似雜志排版&#xff09;&#xff0c;后來被開發者用來做頁面布局。2. 核心特性&#xff1a;使元素脫離普通文檔流&#xff08;但仍在DOM中&#xff09;元素會向左/右浮動&#xff…

08-自然壁紙實戰教程-視頻列表-云

08-自然壁紙實戰教程-視頻列表 前言 視頻列表頁面本質上也是一個數據展示的列表&#xff0c;不同之處在于之前是是展示壁紙&#xff0c;Image組件負責渲染&#xff0c;這里展示的是視頻&#xff0c;使用Video組件&#xff0c;另外視頻頁面也實現了下載的基本功能&#xff0c;…

SCI特刊征稿

我們團隊聯合北京工業大學研究團隊在SCI源刊CMC組織了特刊SI: Advanced Edge Computing and Artificial Intelligence in Smart Environment,主要收錄邊緣計算和人工智能方向的文章&#xff0c;歡迎領域專家和學者投稿&#xff0c;網址https://www.techscience.com/cmc/special…

DO,VO,DTO.....

在 Java 項目里&#xff08;尤其是 Spring、MyBatis 這類框架&#xff09;&#xff0c;經常會看到一堆以 O 結尾的類&#xff1a;VO、DO、DTO、BO、POJO……它們本質上都是普通的 Java Bean&#xff08;即 POJO&#xff09;&#xff0c;但職責和出現的位置不同。下面用“用戶下…

數據結構之并查集和LRUCache

系列文章目錄 數據結構之ArrayList_arraylist o(1) o(n)-CSDN博客 數據結構之LinkedList-CSDN博客 數據結構之棧_棧有什么方法-CSDN博客 數據結構之隊列-CSDN博客 數據結構之二叉樹-CSDN博客 數據結構之優先級隊列-CSDN博客 常見的排序方法-CSDN博客 數據結構之Map和Se…

UE5多人MOBA+GAS 21、給升龍添加連段攻擊,從角色的按下事件中傳遞事件給GA

文章目錄給升龍制作可連段緩存下一連段用普攻鍵來觸發升龍后續的連段在角色中發送按下普攻標簽事件在升龍中接收按下事件&#xff0c;觸發連段以及傷害和力量的傳遞最后在藍圖中設置一下升龍技能的完整代碼給升龍制作可連段 給升龍技能添加一些連段 緩存下一連段 緩存下一連…

基于光柵傳感器+FPGA+ARM的測量控制解決方案

基于光柵傳感器結合FPGA與ARM的測量控制解決方案&#xff0c;通過硬件協同分工實現高精度、實時性及多場景適應性&#xff1a;?? ?一、系統架構分工??傳感層&#xff08;光柵傳感器&#xff09;?采用光柵尺輸出正交脈沖信號&#xff0c;分辨率達0.5μm&#xff0c;精度1μ…

NW831NW910美光固態閃存NW887NW888

美光固態閃存深度解析&#xff1a;NW831、NW910、NW887、NW888系列全方位評測一、技術根基與架構創新美光NW系列固態閃存的技術突破源于其先進的G9 NAND架構&#xff0c;該架構采用5納米制程工藝和多層3D堆疊技術&#xff0c;在單位面積內實現了高達256層的存儲單元堆疊&#x…

reasense api 文檔

API 架構 英特爾實感&#xff08;Intel RealSense?&#xff09;API 提供對深度攝像頭流數據的配置、控制和訪問功能。該 API 支持通過高層級 API 快速啟用攝像頭基礎功能&#xff0c;或通過底層級 API 全面控制所有攝像頭設置。請根據需求選擇合適的 API&#xff1a; 高層級 P…

ArkTs實現骰子布局

Entry Component struct workA {// 定義6種顏色數組&#xff0c;使用ResourceColor類型確保顏色值合法性State color: ResourceColor[] [#ef2816, #f0a200, #6ab002, #005868, #41192e, #141411]// 定義公共樣式裝飾器&#xff0c;避免重復樣式代碼Stylesys() {// 白色圓形基礎…

c語言內存函數以及數據在內存中的存儲

代碼見&#xff1a;登錄 - Gitee.com 1. memcpy使用和模擬實現 strcpy&#xff0c;strncpy是拷貝字符串的&#xff0c;有局限性 函數原型&#xff1a; void * memcpy ( void * destination, const void * source, size_t num ); 功能&#xff1a; memcpy 是完成內存塊拷?的…

Codeforces Round 787 (Div. 3)(A,B,C,D,E,F,G)

Codeforces Round 787 (Div. 3) - Codeforces A. Food for Animals 題意 有a袋狗糧,b袋貓糧,c袋通用糧食&#xff0c;問現在有x只狗y只貓,每一個動物都要吃一袋糧食,問糧食夠不夠吃 思路 首先肯定考慮貓吃貓糧&#xff0c;狗吃狗糧。然后再考慮如果不夠吃的話才會去吃通用…

LLaMA-Factory的webui快速入門

一、webui的啟動方式 LLaMA-Factory 支持通過 WebUI 零代碼微調大語言模型。 在完成安裝 后&#xff0c;您可以通過以下指令進入 WebUI: llamafactory-cli webui 使用上面命令啟動服務后&#xff0c;即可使用默認7860端口進行訪問。訪問地址&#xff1a;http://ip:7860,截止…

【第四節】ubuntu server安裝docker

首先更新軟件源 sudo apt update sudo apt upgrade安裝docker 下載 Docker 官方 GPG 密鑰 # 1. 下載 Docker 官方 GPG 密鑰 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg再次更新軟件源…