颜色和纹理

# 概念理解

图元光栅化(rasterzation process):发生在顶点着色器和片元着色器之间的从图形到片元的转化。

光栅化是三维图形的关键技术之一,它负责将矢量的几何图形转变成栅格化的片元。图形被栅格化后我们就可以在片元着色器中做更多的事情,如果每个片元指定不同的颜色,颜色可以通过内插出计算,也可以直接编程指定。

# 多顶点数据着色程序

例子一:多属性缓冲

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多属性缓冲</title>
    <script src="./lib/cuon-matrix.js"></script>
    <script src="./lib/cuon-utils.js"></script>
    <script src="./lib/webgl-debug.js"></script>
    <script src="./lib/webgl-utils.js"></script>
</head>

<body onload="main()">
    <canvas width="300" height="300" id='webgl'>
        请使用比较高端的浏览器查看这个功能
    </canvas>
    <script>
        let VSHADER_SOURCE =
            `attribute vec4 a_Position;
             attribute float a_PointSize;
             void main(){
                gl_Position=a_Position;
                gl_PointSize=a_PointSize;
             }`;

        let FSHADER_SOURCE =
            `void main(){
                gl_FragColor=vec4(1.0,0.0,0.0,1.0);
            }`;

        function main() {
            let canvas = document.querySelector('#webgl');
            let gl = getWebGLContext(canvas);
            if (!gl) {
                console.info('failed initlizated gl.');
                return;
            }

            if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
                console.info('failed initlizeted shader.');
                return;
            }

            let n = initVertexBuffer(gl);
            if (n < 0) {
                console.info('failed initlizated vertexBuffer');
                return;
            }

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.POINTS, 0, n);
        }

        function initVertexBuffer(gl) {
            let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
            let sizes = new Float32Array([10.0, 20.0, 30.0]);
            let n = 3;
            let vertexBuffer = gl.createBuffer();
            if (!vertexBuffer) {
                console.info('failed initalized vertexBuffer');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW);
            let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
            if (a_Position < 0) {
                console.info('failed initalized position');
                return -1;
            }
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(a_Position);

            let sizeBuffer = gl.createBuffer();
            if (!sizeBuffer) {
                console.info('failed initalized sizeBuffer');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STREAM_DRAW);
            let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
            if (a_PointSize < 0) {
                console.info('failed initalized pointSize');
                return -1;
            }
            gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(a_PointSize);
            return n;
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
多个对象缓冲区绑定到顶点着色器
多个对象缓冲区绑定到顶点着色器

例子二:多个属性数据打包到同一个缓冲区对象

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多属性缓冲区对象升级写法</title>
    <script src="./lib/cuon-matrix.js"></script>
    <script src="./lib/cuon-utils.js"></script>
    <script src="./lib/webgl-debug.js"></script>
    <script src="./lib/webgl-utils.js"></script>
</head>

<body onload="main()">
    <canvas width="300" height="300" id='webgl'>
        请使用比较高端的浏览器查看这个功能
    </canvas>
    <script>
        let VSHADER_SOURCE =
            `attribute vec4 a_Position;
             attribute float a_PointSize;
             void main(){
                gl_Position=a_Position;
                gl_PointSize=a_PointSize;
             }`;

        let FSHADER_SOURCE =
            `void main(){
                gl_FragColor=vec4(1.0,0.0,0.0,1.0);
             }`;

        function main() {
            let canvas = document.querySelector('#webgl');
            let gl = getWebGLContext(canvas);
            if (!gl) {
                console.info('failed initlizated gl.');
                return;
            }

            if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
                console.info('failed initlizeted shader.');
                return;
            }

            let n = initVertexBuffer(gl);
            if (n < 0) {
                console.info('failed initlizated vertexBuffer');
                return;
            }

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.POINTS, 0, n);
        }

        function initVertexBuffer(gl) {
            let verticeSizes = new Float32Array([0.0, 0.5, 10.0, -0.5, -0.5, 20.0, 0.5, -0.5, 30.0]);
            let n = 3;
            let vertexBuffer = gl.createBuffer();
            if (!vertexBuffer) {
                console.info('failed initalized vertexBuffer');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, verticeSizes, gl.STREAM_DRAW);
            let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
            let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
            if (a_Position < 0 || a_PointSize < 0) {
                console.info('failed initalized position');
                return -1;
            }

            let FSIZE = verticeSizes.BYTES_PER_ELEMENT;
            //解释后面两个参数的作用,意思是一组数据有几个字节,偏移几个,一组数据有3个FSIZE,偏移0个,取出2个,即第1个第2个
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
            gl.enableVertexAttribArray(a_Position);
            //解释后面两个参数的作用,意思是一组数据有几个字节,偏移几个,一组数据有3个FSIZE,偏移2个,取出1个,即第3个
            gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
            gl.enableVertexAttribArray(a_PointSize);
            return n;
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
多属性打包到同一缓冲区对象
多属性打包到同一缓冲区对象

例子三:彩色三角形

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多颜色绘制</title>
    <script src="./lib/cuon-matrix.js"></script>
    <script src="./lib/cuon-utils.js"></script>
    <script src="./lib/webgl-debug.js"></script>
    <script src="./lib/webgl-utils.js"></script>
</head>

<body onload="main()">
    <canvas width="300" height="300" id='webgl'>
        请使用比较高端的浏览器查看这个功能
    </canvas>
    <script>
        var VSHADER_SOURCE =
            `attribute vec4 a_Position;
             attribute vec4 a_Color;
             varying vec4 v_Color;  
             void main() {
                gl_Position = a_Position;
                gl_PointSize = 10.0;
                v_Color = a_Color;   
             }`;

        var FSHADER_SOURCE =
            `precision mediump float;
             varying vec4 v_Color;
             void main() {
              gl_FragColor = v_Color;
            }`;

        function main() {
            let canvas = document.querySelector('#webgl');
            let gl = getWebGLContext(canvas);
            if (!gl) {
                console.info('failed initlizated gl.');
                return;
            }

            if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
                console.info('failed initlizeted shader.');
                return;
            }

            let n = initVertexBuffer(gl);
            if (n < 0) {
                console.info('failed initlizated vertexBuffer');
                return;
            }

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            //绘制三个彩色点
            //gl.drawArrays(gl.POINTS, 0, n);
            gl.drawArrays(gl.TRIANGLES, 0, n);
        }

        function initVertexBuffer(gl) {
            let verticeColors = new Float32Array([0.0, 0.5, 1.0, 0.0, 0.0, -0.5, -0.5, 0.0, 1.0, 0.0, 0.5, -0.5, 0.0,
                0.0, 1.0
            ]);
            let n = 3;
            let vertexBuffer = gl.createBuffer();
            if (!vertexBuffer) {
                console.info('failed initalized vertexBuffer');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, verticeColors, gl.STREAM_DRAW);
            let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
            let a_Color = gl.getAttribLocation(gl.program, 'a_Color');
            if (a_Position < 0 || a_Color < 0) {
                console.info('failed initalized position');
                return -1;
            }

            let FSIZE = verticeColors.BYTES_PER_ELEMENT;
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
            gl.enableVertexAttribArray(a_Position);
            gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
            gl.enableVertexAttribArray(a_Color);
            return n;
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
彩色三角形

提示

WebGL中,如果顶点着色器与片元着色器中有类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值就会被自动传入片元着色器。

实际上,这两个变量前后的值是不一样的,到达片元着色器中的值是经过插值处理后的。

变量从一个数值变化到另一个数值的过程叫做内插过程。

顶点数据到片元过程
图形装配以及光栅化
调用片元着色器

例子四:片元着色按照坐标信息

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>按照点的坐标位置显示点的颜色</title>
    <script src="./lib/cuon-matrix.js"></script>
    <script src="./lib/cuon-utils.js"></script>
    <script src="./lib/webgl-debug.js"></script>
    <script src="./lib/webgl-utils.js"></script>
</head>

<body onload="main()">
    <canvas width="300" height="300" id='webgl'>
        请使用比较高端的浏览器查看这个功能
    </canvas>
    <script>
        let VSHADER_SOURCE =
            `attribute vec4 a_Position;
             void main() {
                gl_Position = a_Position;
                gl_PointSize = 10.0;
             }`;

        let FSHADER_SOURCE =
            `precision mediump float;
             uniform float u_Width;
             uniform float u_Height;
             void main() {
                gl_FragColor = vec4(gl_FragCoord.x/u_Width,0.0,gl_FragCoord.y/u_Height,1.0);
            }`;

        function main() {
            let canvas = document.querySelector('#webgl');
            let gl = getWebGLContext(canvas);
            if (!gl) {
                console.info('failed initlizated gl.');
                return;
            }

            if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
                console.info('failed initlizeted shader.');
                return;
            }

            let n = initVertexBuffer(gl);
            if (n < 0) {
                console.info('failed initlizated vertexBuffer');
                return;
            }

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            //绘制三个彩色点
            //gl.drawArrays(gl.POINTS, 0, n);
            gl.drawArrays(gl.TRIANGLES, 0, n);
        }

        function initVertexBuffer(gl) {
            let verticeColors = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
            let n = 3;
            let vertexBuffer = gl.createBuffer();
            if (!vertexBuffer) {
                console.info('failed to create buffer.');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, verticeColors, gl.STREAM_DRAW);
            let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
            if (a_Position < 0) {
                console.info('failed to get storage location of a_Position.');
                return -1;
            }

            let FSIZE = verticeColors.BYTES_PER_ELEMENT;
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 2, 0);
            gl.enableVertexAttribArray(a_Position);

            let u_Width = gl.getUniformLocation(gl.program, 'u_Width');
            if (u_Width < 0) {
                console.info('failed to get storage location of u_Width.');
                return -1;
            }
            gl.uniform1f(u_Width, gl.drawingBufferWidth);

            let u_Height = gl.getUniformLocation(gl.program, 'u_Height');
            if (u_Height < 0) {
                console.info('failed to get storage location of u_Height.');
                return -1;
            }
            gl.uniform1f(u_Height, gl.drawingBufferHeight);
            return n;
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
按照片元坐标着色每个片元(验证片元着色器调用了每个片元
内插值示意图

# 纹理映射

将一张图片映射到一个几何图形的表面上。将一张真实世界的图片贴到一个由两个三角形组成的矩形上,这样矩形表面看上去就是这张图片,此时,这张图片又可以称为纹理图像或纹理。

纹理映射的作用,就是根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色,组成纹理图像的像素又被称为纹素,没一个纹素的颜色都使用RGB或RGBA格式编码。

使用纹理坐标(texture coordinates)来确定纹理图像的哪部分将覆盖到几何图形上。

纹素

例子五:纹理映射

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>纹理坐标</title>
    <script src="./lib/cuon-matrix.js"></script>
    <script src="./lib/cuon-utils.js"></script>
    <script src="./lib/webgl-debug.js"></script>
    <script src="./lib/webgl-utils.js"></script>
</head>

<body onload="main()">
    <canvas width="300" height="300" id='webgl'>
        请使用比较高端的浏览器查看这个功能
    </canvas>
    <script>
        var VSHADER_SOURCE =
            `attribute vec4 a_Position;
             attribute vec2 a_TexCoord;
             varying vec2 v_TexCoord;
             void main() {
               gl_Position = a_Position;
               v_TexCoord = a_TexCoord;
             }`;

        var FSHADER_SOURCE =
            `precision mediump float;
             varying vec2 v_TexCoord;
             uniform sampler2D u_Sampler;
             void main() {
                gl_FragColor = texture2D(u_Sampler,v_TexCoord);
            }`;

        function main() {
            let canvas = document.querySelector('#webgl');
            let gl = getWebGLContext(canvas);
            if (!gl) {
                console.info('failed initlizated gl.');
                return;
            }

            if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
                console.info('failed initlizeted shader.');
                return;
            }

            let n = initVertexBuffer(gl);
            if (n < 0) {
                console.info('failed initlizated vertexBuffer');
                return;
            }

            if (!initTextures(gl, n)) {
                console.info('failed to initTexture.');
                return;
            }

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            //绘制三个彩色点
            //gl.drawArrays(gl.POINTS, 0, n);
            gl.drawArrays(gl.TRIANGLES, 0, n);
        }

        function initTextures(gl, n) {
            let texture = gl.createTexture();
            let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
            if (!u_Sampler) {
                console.info('failed to get storage location of u_sampler.');
                return;
            }

            let img = new Image();
            img.onload = function () {
                loadTexture(gl, n, texture, u_Sampler, img);
            };
            img.src = './resources/blueflower2.JPG';
            return true;
        }

        //wegbl无法直接操作纹理对象,必须将纹理对象绑定到纹理单元上,再通过纹理单元才能访问到里面的数据。
        //也可以理解为:纹理对象是私有的,纹理单元是公开的。
        function loadTexture(gl, n, texture, u_sampler, img) {
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); //对纹理图像Y轴翻转,因为图像的坐标和webgl中纹理的坐标是不一样的,翻转Y轴后正好完全匹配了纹理。
            gl.activeTexture(gl.TEXTURE0); //激活0号纹理,webgl支持8张纹理,有0-7号纹理单元。
            gl.bindTexture(gl.TEXTURE_2D, texture); //绑定纹理对象,告诉这个纹理对象的类型是什么,webgl支持的纹理类型有TEXTURE_2D,TEXTURE_CUBE_MAP两种。
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); //配置纹理对象参数,看纹理是否需要放大,缩小,填充等。
            //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); //配置纹理对象参数,看纹理是否需要放大,缩小,填充等。
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img); //配置纹理图像,将纹理图像分配给纹理对象。就是将加载的图片给纹理对象。
            gl.uniform1i(u_sampler, 0); //将0号纹理传递给着色器
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); //绘制矩形
        }

        function initVertexBuffer(gl) {
            let verticesTexCoords = new Float32Array([
                -0.5, 0.5, -0.3, 1.7, 
                -0.5, -0.5, -0.3, -0.2, 
                 0.5, 0.5, 1.7, 1.7,
                 0.5, -0.5, 1.7, -0.2
            ]);
            let n = 4;
            let vertexBuffer = gl.createBuffer();
            if (!vertexBuffer) {
                console.info('failed to create vertextBuffer.');
                return -1;
            }

            let textureCoordBuffer = gl.createBuffer();
            if (!textureCoordBuffer) {
                console.info('failed to create textureCoordBuffer.');
                return -1;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STREAM_DRAW);

            gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STREAM_DRAW);

            let FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

            let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
            if (a_Position < 0) {
                console.info('failed to get storage location of a_Position.');
                return -1;
            }
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
            gl.enableVertexAttribArray(a_Position);

            let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
            if (a_TexCoord < 0) {
                console.info('failed to get storage location of a_TexCoord');
                return;
            }
            gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
            gl.enableVertexAttribArray(a_TexCoord);
            return n;
        }
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
纹理映射

# 纹理坐标

纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL系统中的纹理坐标系统是二维的。为了将纹理坐标和广泛使用的点坐标区分开,WebGL使用st坐标系统或者uv坐标系。

webgl纹理ts坐标系和图片的xy坐标系y轴相反
纹理映射到顶点坐标上

纹理坐标映射到顶点主要分五个部分

  • 顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器。
  • 片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元。
  • 设置顶点的纹理坐标
  • 准备待加载的纹理图像,令浏览器读取它
  • 监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理

WebGL通过一种称作纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。即使你的程序只需要使用一张纹理图像,也的为其指定一个纹理单元。

默认情况下WebGL至少支持8个纹理单元,一些其他的系统支持的个数更多。

WebGL中无法直接操控纹理对象,而必须将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象。

上次更新: 2025/02/15, 13:42:25
最近更新
01
Git问题集合
01-29
02
安装 Nginx 服务器
01-25
03
安装 Docker 容器
01-25
更多文章>
×
×