1.1 WebGL 简介


文档摘要

1.1 WebGL 简介 第一章:WebGL 基础领域 1.1 WebGL 简介:释放浏览器的 3D 图形潜能 在互联网技术的浪潮中,网页不再仅仅是静态文本和图片的集合,用户期望在浏览器中体验更加丰富、交互性更强的应用。WebGL (Web Graphics Library) 正是在这样的背景下应运而生,它是一项革命性的技术,赋予了现代浏览器强大的 3D 图形渲染能力,使得开发者可以直接在网页上创建和展示复杂的 3D 场景和互动体验,而无需安装任何插件。 1.1.1 WebGL 的定义与核心概念 WebGL 本质上是一个 JavaScript API,它基于 OpenGL ES 2.0(嵌入式系统和移动设备上广泛使用的图形标准)。

1.1 WebGL 简介

第一章:WebGL 基础领域

1.1 WebGL 简介:释放浏览器的 3D 图形潜能

在互联网技术的浪潮中,网页不再仅仅是静态文本和图片的集合,用户期望在浏览器中体验更加丰富、交互性更强的应用。WebGL (Web Graphics Library) 正是在这样的背景下应运而生,它是一项革命性的技术,赋予了现代浏览器强大的 3D 图形渲染能力,使得开发者可以直接在网页上创建和展示复杂的 3D 场景和互动体验,而无需安装任何插件。

1.1.1 WebGL 的定义与核心概念

WebGL 本质上是一个 JavaScript API,它基于 OpenGL ES 2.0(嵌入式系统和移动设备上广泛使用的图形标准)。这意味着 WebGL 继承了 OpenGL ES 的强大功能和灵活性,同时又融入了 Web 技术的开放性和跨平台性。

核心要点:

  • JavaScript API: WebGL 通过 JavaScript 接口暴露出来,开发者可以使用 JavaScript 代码来调用 WebGL 的功能,实现图形渲染。

  • OpenGL ES 2.0: WebGL 的底层图形处理基于 OpenGL ES 2.0 标准,这确保了 WebGL 的高效性和与硬件加速的兼容性。

  • 浏览器原生支持: WebGL 是浏览器原生支持的技术,无需用户安装额外的插件,即可在支持 WebGL 的浏览器中运行。

  • 硬件加速: WebGL 利用用户的 GPU (Graphics Processing Unit) 进行图形渲染,充分发挥硬件性能,实现流畅、高性能的 3D 图形效果。

  • 跨平台性: 由于 WebGL 基于 Web 标准,并且被主流浏览器广泛支持,因此使用 WebGL 开发的 3D 应用具有良好的跨平台性,可以在不同的操作系统和设备上运行。

1.1.2 WebGL 的诞生背景与发展历程

在 WebGL 出现之前,网页上的 3D 图形展示主要依赖于插件技术,例如 Flash 和 Java Applet。然而,这些插件存在诸多问题:

  • 安全风险: 插件容易成为安全漏洞的入口,给用户带来安全隐患。

  • 性能瓶颈: 插件的性能往往不如原生技术,难以实现复杂的 3D 渲染。

  • 平台限制: 插件的跨平台性较差,在某些平台或浏览器上可能无法正常运行。

  • 用户体验: 需要用户手动安装插件,降低了用户体验。

为了解决这些问题,Khronos Group (一个制定开放标准图形和媒体技术的非营利性组织) 在 2011 年正式发布了 WebGL 1.0 标准。WebGL 的诞生彻底改变了 Web 3D 图形开发的格局:

  • 开放标准: WebGL 是一个开放标准,由 Khronos Group 维护,保证了技术的开放性和互操作性。

  • 原生支持: 主流浏览器 (如 Chrome, Firefox, Safari, Edge 等) 迅速实现了对 WebGL 的原生支持,使得 WebGL 成为真正的跨平台 Web 3D 技术。

  • 社区活跃: WebGL 吸引了大量的开发者和设计师加入,形成了活跃的社区,推动了 WebGL 技术和生态的快速发展。

随着技术的不断进步,WebGL 也在持续演进。2017 年,WebGL 2.0 标准发布,它基于 OpenGL ES 3.0,引入了更多现代图形渲染特性,例如:

  • Transform Feedback: 允许着色器将数据写回缓冲区,实现更复杂的几何处理和粒子系统。

  • 纹理压缩: 支持多种纹理压缩格式,减少纹理数据大小,提升加载速度和渲染效率。

  • 3D 纹理和纹理数组: 支持更高维度的纹理数据,扩展了纹理的应用范围。

  • 增强的着色器语言 (GLSL ES 3.0): 提供了更强大的着色器编程能力。

WebGL 2.0 的出现进一步提升了 WebGL 的图形渲染能力,使其能够胜任更加复杂和精细的 3D 应用开发。

1.1.3 WebGL 的应用领域

WebGL 的强大功能使其在众多领域都有着广泛的应用前景:

  • 3D 游戏: WebGL 使得在浏览器中运行高性能 3D 游戏成为可能,无需安装任何插件,用户可以直接在网页上体验媲美原生应用的 3D 游戏。

  • 数据可视化: WebGL 可以用于创建交互式的 3D 数据可视化图表,帮助用户更直观地理解和分析复杂的数据。例如,地理信息系统 (GIS)、科学数据可视化、金融数据可视化等。

  • 虚拟现实 (VR) 和增强现实 (AR): WebGL 与 WebVR 和 WebXR 等技术结合,可以构建 Web 端的 VR 和 AR 应用,为用户带来沉浸式的虚拟体验。

  • 产品展示和电子商务: WebGL 可以用于创建 3D 产品模型展示,让用户可以 360 度查看产品细节,提升用户体验和购买决策。

  • 在线教育和培训: WebGL 可以用于创建交互式的 3D 教学模型和场景,例如医学解剖模型、机械结构演示、虚拟实验室等,提升学习效果。

  • 建筑可视化和城市规划: WebGL 可以用于创建 3D 建筑模型和城市规划模型,帮助建筑师、设计师和城市规划者进行方案展示和沟通。

  • 艺术和创意设计: WebGL 为艺术家和设计师提供了新的创作媒介,可以创作出独特的交互式 3D 艺术作品和数字艺术装置。

总而言之,WebGL 的应用领域非常广泛,几乎所有需要 3D 图形展示和交互的场景都可以考虑使用 WebGL 来实现。

1.1.4 WebGL 的工作原理:渲染管线

要理解 WebGL 的工作原理,核心在于理解 渲染管线 (Rendering Pipeline)。渲染管线是 GPU 执行图形渲染任务的一系列步骤,WebGL 程序通过控制渲染管线的各个阶段来实现 3D 图形的绘制。

以下是一个简化的 WebGL 渲染管线流程图:

渲染管线的主要阶段解释:

  1. 顶点数据 (Vertex Data): 这是渲染的输入数据,定义了 3D 模型的几何形状。顶点数据包括顶点的位置、颜色、法线、纹理坐标等信息。这些数据通常存储在缓冲区对象 (Buffer Object) 中,并传递给顶点着色器。

  2. 顶点着色器 (Vertex Shader): 顶点着色器是一个在 GPU 上运行的小程序,负责处理每个顶点的属性。主要任务包括:

    • 模型、视图、投影变换 (MVP Transformation): 将顶点坐标从模型空间转换到裁剪空间,为后续的裁剪和投影做准备。

    • 顶点属性计算: 根据需要计算顶点的颜色、法线、纹理坐标等属性,并将这些属性传递给片元着色器。

  3. 图元装配 (Primitive Assembly): 将顶点着色器处理后的顶点数据组织成几何图元 (Primitives),例如点、线、三角形等。WebGL 主要支持三角形作为基本图元。

  4. 光栅化 (Rasterization): 将图元转换为片元 (Fragments)。片元可以理解为像素的候选项,包含了像素的位置信息和从顶点着色器传递过来的属性信息。光栅化过程确定了哪些像素被图元覆盖,并为每个片元生成对应的像素坐标和属性插值。

  5. 片元着色器 (Fragment Shader): 片元着色器也是一个在 GPU 上运行的小程序,负责处理每个片元,决定最终像素的颜色。主要任务包括:

    • 纹理采样: 从纹理图像中读取纹理颜色。

    • 光照计算: 根据光照模型和材质属性计算片元的颜色。

    • 颜色混合: 将纹理颜色、光照颜色等进行混合,得到最终的片元颜色。

  6. 测试与混合 (Tests and Blending): 对片元进行一系列测试,例如深度测试 (Depth Test)、Alpha 测试 (Alpha Test) 等,决定片元是否可见。如果片元通过测试,则将其颜色与帧缓冲区中已有的颜色进行混合 (Blending),最终写入帧缓冲区。

  7. 帧缓冲区 (Framebuffer): 帧缓冲区是存储最终渲染结果的缓冲区,包含了颜色缓冲区、深度缓冲区、模板缓冲区等。渲染完成后,帧缓冲区的内容会被显示到屏幕上。

关键概念总结:

  • 着色器 (Shader): 顶点着色器和片元着色器是 WebGL 程序的核心,它们是用 GLSL ES (OpenGL Shading Language for Embedded Systems) 编写的程序,运行在 GPU 上,负责顶点和片元处理。

  • 缓冲区对象 (Buffer Object): 用于存储顶点数据、索引数据等,高效地将数据传递给 GPU。

  • 纹理 (Texture): 用于存储图像数据,可以应用到 3D 模型表面,增加细节和真实感。

  • 状态机 (State Machine): WebGL 是一个状态机,通过设置各种状态 (例如颜色、深度测试、混合模式等) 来控制渲染过程。

1.1.5 WebGL 代码实践:清空画布

让我们通过一个最简单的 WebGL 代码示例来体验一下 WebGL 的基本用法:清空画布为指定颜色。

HTML 文件 (index.html):

<!DOCTYPE html> <html> <head> <title>WebGL 简介 - 清空画布</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <canvas id="glCanvas"></canvas> <script src="main.js"></script> </body> </html>

JavaScript 文件 (main.js):

function main() { const canvas = document.querySelector("#glCanvas"); const gl = canvas.getContext("webgl"); // 获取 WebGL 上下文 // 检查 WebGL 是否可用 if (!gl) { alert("Unable to initialize WebGL. Your browser or graphics card may not support it."); return; } // 设置清屏颜色 (红色) gl.clearColor(1.0, 0.0, 0.0, 1.0); // RGBA 格式,取值范围 0.0 到 1.0 // 清空颜色缓冲区 gl.clear(gl.COLOR_BUFFER_BIT); } main();

代码详解:

  1. 获取 Canvas 元素:

    const canvas = document.querySelector("#glCanvas");

    通过 document.querySelector 获取 HTML 中 id 为 "glCanvas" 的 <canvas> 元素。WebGL 的渲染结果将绘制在这个 canvas 画布上。

  2. 获取 WebGL 上下文:

    const gl = canvas.getContext("webgl");

    调用 canvas.getContext("webgl") 尝试获取 WebGL 渲染上下文。这是 WebGL API 的入口点,所有 WebGL 操作都通过 gl 对象进行。 如果浏览器不支持 WebGL,getContext("webgl") 将返回 null

  3. 检查 WebGL 支持:

    if (!gl) { alert("Unable to initialize WebGL. Your browser or graphics card may not support it."); return; }

    这是一个重要的错误检查步骤。确保 gl 对象不为 null,以判断 WebGL 是否成功初始化。如果初始化失败,弹出警告信息并终止程序。

  4. 设置清屏颜色:

    gl.clearColor(1.0, 0.0, 0.0, 1.0);

    gl.clearColor() 函数用于设置清屏颜色。它接受四个参数,分别代表红色 (Red)、绿色 (Green)、蓝色 (Blue) 和透明度 (Alpha) 分量,取值范围都是 0.0 到 1.0。 这里设置为 (1.0, 0.0, 0.0, 1.0),表示红色,完全不透明。

  5. 清空颜色缓冲区:

    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.clear() 函数用于清空指定的缓冲区。gl.COLOR_BUFFER_BIT 是一个常量,表示颜色缓冲区。 这条语句将颜色缓冲区清空为之前使用 gl.clearColor() 设置的颜色。

运行结果:

在浏览器中打开 index.html 文件,如果你的浏览器支持 WebGL,你将看到一个红色的画布。 这表示我们已经成功使用 WebGL 清空了画布颜色。

1.1.6 WebGL 代码实践:绘制一个简单的三角形

清空画布只是 WebGL 的基本操作。接下来,我们将通过一个稍微复杂一点的例子,绘制一个简单的彩色三角形,来进一步理解 WebGL 的渲染流程。

为了绘制图形,我们需要编写 着色器程序 (Shader Programs)。着色器程序是用 GLSL ES 语言编写的,需要在 WebGL 程序中编译和链接。

修改 JavaScript 文件 (main.js):

function main() { const canvas = document.querySelector("#glCanvas"); const gl = canvas.getContext("webgl"); if (!gl) { alert("Unable to initialize WebGL. Your browser or graphics card may not support it."); return; } // **顶点着色器程序** const vsSource = ` attribute vec4 aVertexPosition; // 顶点位置属性 attribute vec4 aVertexColor; // 顶点颜色属性 varying lowp vec4 vColor; // 传递给片元着色器的颜色 void main() { gl_Position = aVertexPosition; // 直接使用顶点位置 vColor = aVertexColor; // 将顶点颜色传递给片元着色器 } `; // **片元着色器程序** const fsSource = ` varying lowp vec4 vColor; // 从顶点着色器接收的颜色 void main() { gl_FragColor = vColor; // 将颜色输出到片元 } `; // 初始化着色器程序 const shaderProgram = initShaderProgram(gl, vsSource, fsSource); // 获取着色器程序中属性的位置 const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'), }, }; // **顶点数据** const positions = [ // 顶点坐标 (x, y) 0.0, 0.5, -0.5, -0.5, 0.5, -0.5, ]; const colors = [ // 颜色 (r, g, b, a) 1.0, 0.0, 0.0, 1.0, // 红色 0.0, 1.0, 0.0, 1.0, // 绿色 0.0, 0.0, 1.0, 1.0, // 蓝色 ]; // 初始化缓冲区 const positionBuffer = initBuffer(gl, positions); const colorBuffer = initBuffer(gl, colors); // 绘制场景 drawScene(gl, programInfo, positionBuffer, colorBuffer); } // 初始化着色器程序 function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); // 创建着色器程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 检查着色器程序是否链接成功 if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram)); return null; } return shaderProgram; } // 创建指定类型的着色器,上传 source 源码并编译 function createShader(gl, type, source) { const shader = gl.createShader(type); // 创建着色器对象 gl.shaderSource(shader, source); // 将源码发送到着色器对象 gl.compileShader(shader); // 编译着色器程序 // 检查编译是否成功 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } // 初始化缓冲区 function initBuffer(gl, data) { const buffer = gl.createBuffer(); // 创建缓冲区对象 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 绑定缓冲区对象到 ARRAY_BUFFER gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); // 将数据写入缓冲区 return buffer; } // 绘制场景 function drawScene(gl, programInfo, positionBuffer, colorBuffer) { gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置清屏颜色 (黑色) gl.clearDepth(1.0); // 设置深度缓冲区清除值 gl.enable(gl.DEPTH_TEST); // 开启深度测试 gl.depthFunc(gl.LEQUAL); // 近的物体遮挡远的物体 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清空颜色和深度缓冲区 // 使用着色器程序 gl.useProgram(programInfo.program); { const numComponents = 2; // 每次迭代提取 2 个值 (x, y) const type = gl.FLOAT; // 缓冲区数据类型为浮点型 const normalize = false; // 不进行归一化 const stride = 0; // 步长为 0,数据紧密排列 const offset = 0; // 从缓冲区起始位置开始读取 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 绑定顶点位置缓冲区 gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, // 属性位置 numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); // 启用顶点属性 } { const numComponents = 4; // 每次迭代提取 4 个值 (r, g, b, a) const type = gl.FLOAT; const normalize = false; const stride = 0; const offset = 0; gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); // 绑定顶点颜色缓冲区 gl.vertexAttribPointer( programInfo.attribLocations.vertexColor, // 属性位置 numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor); // 启用顶点属性 } const offset = 0; const vertexCount = 3; // 绘制 3 个顶点,构成一个三角形 gl.drawArrays(gl.TRIANGLES, offset, vertexCount); // 执行绘制 } main();

代码详解 (绘制三角形):

  1. 着色器程序 (Vertex Shader & Fragment Shader):

    • 顶点着色器 (vsSource):

      • attribute vec4 aVertexPosition;: 声明一个顶点属性 aVertexPosition,用于接收顶点位置数据。vec4 表示 4 维向量 (x, y, z, w),这里我们只使用 x 和 y 坐标。

      • attribute vec4 aVertexColor;: 声明一个顶点属性 aVertexColor,用于接收顶点颜色数据 (RGBA)。

      • varying lowp vec4 vColor;: 声明一个 varying 变量 vColor,用于将顶点颜色数据传递给片元着色器。lowp 表示低精度,适用于颜色等精度要求不高的变量。

      • gl_Position = aVertexPosition;: 将顶点位置 aVertexPosition 赋值给内置变量 gl_Position,这是顶点着色器必须输出的变量,表示顶点在裁剪空间中的位置。

      • vColor = aVertexColor;: 将顶点颜色 aVertexColor 赋值给 varying 变量 vColor,传递给片元着色器。

    • 片元着色器 (fsSource):

      • varying lowp vec4 vColor;: 声明一个 varying 变量 vColor,用于接收从顶点着色器传递过来的颜色数据。

      • gl_FragColor = vColor;: 将接收到的颜色 vColor 赋值给内置变量 gl_FragColor,这是片元着色器必须输出的变量,表示片元的颜色。

  2. 初始化着色器程序 (initShaderProgram, createShader):

    • initShaderProgram 函数负责编译顶点着色器和片元着色器,并将它们链接成一个 着色器程序 (Program)

    • createShader 函数负责创建指定类型的着色器对象,加载 GLSL ES 源码,并编译着色器。

  3. 获取属性位置 (programInfo):

    • gl.getAttribLocation(shaderProgram, 'aVertexPosition'): 获取着色器程序中 aVertexPosition 属性的位置索引。

    • gl.getAttribLocation(shaderProgram, 'aVertexColor'): 获取 aVertexColor 属性的位置索引。

    • 这些位置索引在后续设置顶点属性时会用到。

  4. 顶点数据 (positions, colors):

    • positions: 定义了三角形三个顶点的 x, y 坐标。

    • colors: 定义了三个顶点对应的颜色 (RGBA)。

  5. 初始化缓冲区 (initBuffer):

    • initBuffer 函数负责创建缓冲区对象,并将顶点数据或颜色数据写入缓冲区。

    • gl.createBuffer(): 创建缓冲区对象。

    • gl.bindBuffer(gl.ARRAY_BUFFER, buffer): 将缓冲区对象绑定到 ARRAY_BUFFER 目标。ARRAY_BUFFER 用于存储顶点属性数据。

    • gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW): 将数据写入缓冲区。Float32Array 将 JavaScript 数组转换为 WebGL 需要的 Float32Array 类型。 gl.STATIC_DRAW 表示缓冲区内容将不会经常改变。

  6. 绘制场景 (drawScene):

    • 清空缓冲区: 清空颜色缓冲区和深度缓冲区,为新的帧做准备。

    • 使用着色器程序: gl.useProgram(programInfo.program) 激活之前链接好的着色器程序。

    • 设置顶点属性:

      • gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer): 绑定顶点位置缓冲区。

      • gl.vertexAttribPointer(...): 指定顶点属性的读取方式。

        • programInfo.attribLocations.vertexPosition: 属性位置索引。

        • numComponents = 2: 每个顶点位置由 2 个分量 (x, y) 组成。

        • type = gl.FLOAT: 数据类型为浮点型。

        • normalize = false: 不进行归一化。

        • stride = 0: 步长为 0,数据紧密排列。

        • offset = 0: 从缓冲区起始位置开始读取。

      • gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition): 启用顶点属性。

      • 颜色属性的设置方式与顶点位置属性类似。

    • 绘制图元: gl.drawArrays(gl.TRIANGLES, offset, vertexCount) 执行绘制操作。

      • gl.TRIANGLES: 绘制图元类型为三角形。每三个顶点组成一个三角形。

      • offset = 0: 从顶点数组的起始位置开始绘制。

      • vertexCount = 3: 绘制 3 个顶点。

运行结果:

在浏览器中打开 index.html 文件,如果代码正确,你将看到一个彩色的三角形,每个顶点颜色不同,三角形内部颜色会根据顶点颜色进行插值。

代码执行流程图 (绘制三角形):

1.1.7 总结与展望

本节我们对 WebGL 进行了简要的介绍,涵盖了 WebGL 的定义、诞生背景、应用领域、工作原理以及基本的代码实践。通过清空画布和绘制三角形的例子,我们初步了解了 WebGL 的渲染流程和基本 API 的使用。

总结:

  • WebGL 是一个基于 OpenGL ES 2.0 的 JavaScript API,为浏览器带来了强大的 3D 图形渲染能力。

  • WebGL 是开放标准、浏览器原生支持、硬件加速、跨平台的技术。

  • WebGL 在游戏、数据可视化、VR/AR、产品展示、教育、建筑等领域有着广泛的应用前景。

  • WebGL 的核心工作原理是渲染管线,通过顶点着色器、片元着色器等阶段处理顶点和片元数据,最终生成渲染结果。

  • 着色器程序 (Shader Programs) 是 WebGL 开发的关键,需要使用 GLSL ES 语言编写。

  • 缓冲区对象 (Buffer Objects) 用于高效地将数据传递给 GPU。

展望:

WebGL 作为一项重要的 Web 技术,未来将继续发展和完善。WebGL 2.0 的普及将带来更强大的图形渲染能力,WebGPU 等新一代 Web 图形 API 的出现,将进一步提升 Web 图形的性能和灵活性。随着 Web 技术的不断进步,WebGL 将在更多领域发挥重要作用,为用户带来更加丰富、沉浸式的 Web 体验。

下一步学习建议:

  • 深入学习 GLSL ES 着色器语言,掌握顶点着色器和片元着色器的编写。

  • 学习 WebGL 的矩阵变换 (模型矩阵、视图矩阵、投影矩阵),实现 3D 场景的变换和投影。

  • 学习 WebGL 的纹理 (Texture) 和光照 (Lighting) 技术,增加 3D 模型的细节和真实感。

  • 学习 WebGL 的高级渲染技术,例如阴影 (Shadows)、后期处理 (Post-processing) 等。

  • 探索 WebGL 的各种应用框架和库,例如 Three.js, Babylon.js 等,提升开发效率。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U