Metal入门学习:绘制渲染三角形
一、编程指南PDF下载链接(中英文档)
-
1、Metal编程指南PDF链接
https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 编程指南.pdf -
2、Metal着色语言(Metal Shader Language:简称MSL)编程指南PDF链接
https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 着色语言指南.pdf -
3、补充:官网API文档链接
https://developer.apple.com/documentation/metal/using_a_render_pipeline_to_render_primitives
二、内容前述
此示例展示如何配置渲染管线并将其用作渲染通道的一部分,以将简单的2D彩色三角形绘制到视图中。该示例为每个顶点提供位置和颜色,渲染管线使用该数据渲染三角形,在为三角形顶点指定的颜色之间插入颜色值。效果如下
三、了解Metal渲染管线
渲染管线处理绘图命令并将数据写入渲染通道的目标。渲染管线有许多阶段,一些使用着色器编程,另一些具有固定或可配置的行为。此示例侧重于管道的三个主要阶段:顶点阶段、光栅化阶段和片段阶段。顶点阶段和片段阶段是可编程的,可以使用Metal着色语言(MSL)编写函数。光栅化阶段具有固定的行为,如下图所示:

渲染从绘图命令开始,其中包括顶点数和要渲染的图元类型。 例如,这是此示例中的绘图命令:
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTrianglevertexStart:0vertexCount:3];
顶点阶段为每个顶点提供数据。当处理了足够多的顶点后,渲染管线将图元光栅化,确定渲染目标中的哪些像素位于图元的边界内。 片段阶段确定要写入这些像素的渲染目标的值。
四、渲染管道处理数据
顶点函数为单个顶点生成数据,片段函数为单个片段生成数据,可以自定义它们的工作方式。在配置管道阶段时要牢记目标,想象希望管道生成什么以及它如何生成这些结果。决定将哪些数据传递到渲染管道以及将哪些数据传递到管道的后续阶段。通常在三个地方执行此操作:
管道的输入,由应用程序提供并传递到顶点阶段。
顶点阶段的输出,传递到光栅化阶段。
片段阶段的输入,由应用程序提供或由光栅化阶段生成。
在此示例中,管道的输入数据是顶点的位置及其颜色。演示了通常在顶点函数中执行的变换类型,输入坐标在自定义坐标空间中定义,以距视图中心的像素为单位进行测量。这些坐标需要转换为 Metal 的坐标系。
声明一个Vertex 结构,使用SIMD向量类型来保存位置和颜色数据。要共享结构在内存中的布局方式的单一定义,请在公共标头中声明结构并将其导入Metal着色器和应用程序。
//输入的顶点数据信息
typedef struct {//向量:顶点位置vector_float2 position;//向量:颜色值vector_float4 color;
} Vertex;
SIMD类型在Metal Shading Language中很常见,使用simd库也是正常。SIMD类型包含特定数据类型的多个通道,因此将位置声明为vector_float2意味着它包含两个32位浮点值(将保存x和y坐标)。颜色使用vector_float4存储,因为它们有四个通道:红色、绿色、蓝色和alpha。
在应用程序中,输入数据使用常量数组指定:
//三个顶点位置和颜色值
static const Vertex vertices[] = {//位置 //颜色值{{250,-250},{1.0,0.0,0.0,1.0}},{{-250,-250},{0.0,1.0,0.0,1.0}},{{0,250}, {0.0,0.0,1.0,1.0}},
};
顶点阶段为顶点生成数据,需要提供颜色和变换后的位置。再次使用SIMD类型声明一个包含位置和颜色值的RasterizerData结构。
struct RasterizerData{//[[position]]:在顶点着色函数中,表示当前的顶点信息,类型是float4、//还可以表示描述了片元的窗口的相对坐标(x,y,z,1/w),//即该像素点在屏幕上的位置信息//----[[position]]----请在着色函数编程指南文档查看解释float4 position [[position]];//颜色值float4 color;
};
输出位置(在下面详细描述)必须定义为vector_float4。颜色的声明与输入数据结构中的颜色相同。
因为Metal需要知道光栅化数据中的哪个字段提供位置数据,而Metal不会对结构中的字段强制执行任何特定的命名约定,所以使用[[position]]属性限定符注释位置字段以声明该字段保存输出位置。
片段函数只是将光栅化阶段的数据传递给后面的阶段,因此它不需要任何额外的参数。
五、定义顶点着色函数
声明顶点函数,包括它的输入参数和它输出的数据。就像使用kernel关键字声明计算函数一样,使用vertex关键字声明顶点函数。(kernel:可查看这篇文章https://blog.csdn.net/qqwyuli/article/details/130785820)
//[[vertex_id]] :顶点id标识符,并不由开发者传递
//属性修饰符"[[buffer(index)]]" 为着色函数参数设定了缓存的位置
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],constant Vertex* vertices [[buffer(VertexInputIndexVertices)]],constant vector_uint2 *viewPortSizePointer [[buffer(VertexInputIndexViewportSize)]])
第一个参数vertexID使用[[vertex_id]]属性限定符,Metal的一个关键字。执行渲染命令时,GPU会多次调用顶点函数,为每个顶点生成一个唯一值。
第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的Vertex结构。
要将位置转换为Metal的坐标,该函数需要绘制三角形的视口大小(以像素为单位),因此它存储在 viewportSizePointer 参数中。
第二个和第三个参数具有[[buffer(n)]]属性限定符。默认情况下,Metal会自动为每个参数分配参数表中的槽。当将[[buffer(n)]]限定符添加到缓冲区参数时,明确地告诉Metal要使用哪个插槽。显式声明插槽可以更轻松地修改着色器,而无需更改应用程序代码。在共享头文件中声明两个索引的常量,如下所示:
typedef enum AAPLVertexInputIndex
{VertexInputIndexVertices = 0,VertexInputIndexViewportSize = 1,
} AAPLVertexInputIndex;
该函数的输出是一个 RasterizerData 结构。
六、编写顶点函数
顶点函数必须生成输出结构的两个字段。使用vertexID参数索引顶点数组并读取顶点的输入数据。此外,检索视口尺寸
float2 pixelSpaceXY = vertices[vertexID].position.xy;vector_float2 viewPortSize = vector_float2(*viewPortSizePointer);
顶点函数必须提供裁剪空间坐标中的位置数据,裁剪空间坐标是使用四维齐次向量(x,y,z,w)指定的3D点。光栅化阶段采用输出位置并将x、y和z坐标除以w,以在标准化设备坐标中生成3D点。规范化设备坐标与视口大小无关。

规范化设备坐标使用左手坐标系并映射到视口中的位置。基元被裁剪到这个坐标系中的一个盒子,然后光栅化。裁剪框的左下角位于(x,y)坐标(-1.0,-1.0),右上角位于 (1.0,1.0)。正z值指向远离相机的方向(进入屏幕)。z坐标的可见部分介于0.0(近裁剪平面)和1.0(远裁剪平面)之间。
将输入坐标系转换为归一化设备坐标系

因为这是一个二维应用,不需要齐次坐标,所以先给输出坐标写一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味着坐标已经在规范化的设备坐标空间中,顶点函数应该在该坐标空间中生成(x,y)坐标。将输入位置除以视口大小的一半以生成规范化的设备坐标。由于此计算是使用SIMD类型执行的,因此可以使用一行代码同时划分两个通道。 进行除法,将结果放入输出位置的x和y通道
out.position = vector_float4(0,0,0,1.0);
out.position.xy = pixelSpaceXY / (viewPortSize / 2);
最后将颜色值复制到out.color返回值中
out.color = vertices[vertexID].color;
完整的顶点着色函数
//[[vertex_id]] :顶点id标识符,并不由开发者传递
//属性修饰符"[[buffer(index)]]" 为着色函数参数设定了缓存的位置
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],constant Vertex* vertices [[buffer(VertexInputIndexVertices)]],constant vector_uint2 *viewPortSizePointer [[buffer(VertexInputIndexViewportSize)]]){RasterizerData out;float2 pixelSpaceXY = vertices[vertexID].position.xy;vector_float2 viewPortSize = vector_float2(*viewPortSizePointer);out.position = vector_float4(0,0,0,1.0);out.position.xy = pixelSpaceXY / (viewPortSize / 2);out.color = vertices[vertexID].color;return out;
};
七、编写片段着色函数(片元着色函数)
片段是对呈现目标的可能更改。光栅化器确定渲染目标的哪些像素被基元覆盖。仅渲染像素中心位于三角形内的片段。

片段函数处理来自单个位置的光栅化器的传入信息,并计算每个渲染目标的输出值。这些片段值由管道中的后续阶段处理,最终写入渲染目标。
注意:
片段被称为可能更改的原因是片段阶段之后的管道阶段可以配置为拒绝某些片段或更改写入渲染目标的内容。在此示例中,片段阶段计算的所有值都按原样写入渲染目标。
此示例中的片段着色器接收与顶点着色器输出中声明的相同参数。使用fragment关键字声明片段函数。它采用单个参数,与顶点阶段提供的RasterizerData结构相同。添加 [[stage_in]]属性限定符以指示此参数由光栅器生成。

返回插值颜色作为函数的输出
return in.color;
完整的片元着色函数
fragment float4 fragmentShader(RasterizerData in [[stage_in]]){return in.color;
};
八、创建渲染Pipeline State对象
顶点函数和片元函数已经完成,可以创建一个使用它们的渲染管道。首先,获取默认库并为每个函数获取一个MTLFunction对象。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下来,创建一个 MTLRenderPipelineState 对象。渲染管道有更多阶段需要配置,可以使用 MTLRenderPipelineDescriptor 来配置管道。
MTLRenderPipelineDescriptor *descriptor = [[MTLRenderPipelineDescriptor alloc] init];
descriptor.label = @"Simple pipeline";
descriptor.vertexFunction = vertexFunction;
descriptor.fragmentFunction = fragmentFunction;
descriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat;_pipelineState = [_device newRenderPipelineStateWithDescriptor:descriptor error:&error];
除了指定顶点和片段函数外,还声明了管道将绘制到的所有渲染目标的像素格式。像素格式(MTLPixelFormat)定义了像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。由于此示例只有一个渲染目标并且由视图提供,因此将视图的像素格式复制到渲染管道描述符中。渲染管道状态必须使用与渲染通道指定的像素格式兼容的像素格式。在此示例中,渲染通道和管道状态对象都使用视图的像素格式,因此它们始终相同。
当 Metal 创建渲染管道状态对象时,管道被配置为将片段函数的输出转换为渲染目标的像素格式。如果要针对不同的像素格式,则需要创建不同的管道状态对象。可以在针对不同像素格式的多个管道中重复使用相同的着色器。
九、设置视口
现在有了管道的渲染管道状态对象,将要渲染三角形。可以使用渲染命令编码器来执行此操作。首先,设置视口,以便 Metal 知道要绘制到渲染目标的哪一部分。
[commandEncoder setViewport:(MTLViewport){0.0,0.0,_viewPortSize.x,_viewPortSize.y,0.0,1.0}];
十、设置渲染Pipeline State
为要使用的管道设置渲染管道状态。
[commandEncoder setRenderPipelineState:_pipelineState];
十一、将参数数据发送到顶点函数
通常使用缓冲区(MTLBuffer)将数据传递给着色器。然而,当只需要将少量数据传递给顶点函数时,就像这里的情况一样,将数据直接复制到命令缓冲区中。
该示例将两个参数的数据复制到命令缓冲区中。顶点数据是从示例中定义的数组中复制的。视口数据是从用于设置视口的同一变量复制而来的。
在此示例中,片段函数仅使用它从光栅器接收的数据,因此没有要设置的参数。
//设置顶点着色器参数
[commandEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:VertexInputIndexVertices];
[commandEncoder setVertexBytes:&_viewPortSize length:sizeof(_viewPortSize) atIndex:VertexInputIndexViewportSize];
十二、对绘图命令进行编码
指定图元的种类、起始索引和顶点数。渲染三角形时,将使用vertexID参数的值 0、1 和2调用顶点函数。
[commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
与使用Metal绘制到屏幕一样,结束编码过程并提交命令缓冲区。但是,可以使用同一组步骤对更多渲染命令进行编码。最终图像呈现为好像命令是按照指定的顺序处理的。(为了性能,允许 GPU 并行处理命令甚至部分命令,只要最终结果看起来是按顺序呈现的。)
十三、尝试颜色插值
在此示例中,颜色值被插值到整个三角形中 这通常是想要的效果,但有时希望一个值由一个顶点生成并在整个图元中保持不变。在顶点函数的输出上指定平面属性限定符来执行此操作。现在试试这个,在示例项目中找到RasterizerData的定义,并将[[flat]]限定符添加到其颜色字段。
float4 color [[flat]];
再次运行示例。渲染管线在整个三角形上统一使用第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。可以混合使用平面着色和插值,只需在顶点函数的输出中添加或省略平面限定符即可。金属着色语言规范定义了其他属性限定符,也可以使用它们来修改光栅化行为。
十四、完整代码
例子:github链接:https://github.com/dennie-lee/MetalDrawTriangleDemo
相关文章:
Metal入门学习:绘制渲染三角形
一、编程指南PDF下载链接(中英文档) 1、Metal编程指南PDF链接 https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 编程指南.pdf 2、Metal着色语言(Metal Shader Language:简称MSL)编程指南PDF链接 https://github.com/dennie-lee/ios_te…...
python 中常见变量类型
数值 a 10 b 123 … 字符串 在python中 用单引号’‘和双引号""括起来的都是字符串,不使用引号括起来的不是字符串,字符串是使用最多的数据类型,用来表示一段文本信息。 比如: a ‘123’ b “123” 字符串之间可以用加法运算…...
SVN使用教程(一)
文章目录 前言一、SVN是什么?二、SVN和Git对比,有什么优势?三、SVN主要应用四、SVN仓库五、安装SVN客户端 前言 提示:这里可以添加本文要记录的大概内容: 在制作系统或者写文档,都需要用于管理和跟踪开发…...
【5.19】四、性能测试—指标、种类
目录 4.1 性能测试概述 4.2 性能测试的指标 4.3 性能测试的种类 为了追求高质量、高效率的生活与工作,人们对软件产品的性能要求越来越高,例如软件产品要足够稳定、响应速度足够快,在用户量、工作量较大时也不会出现崩溃或卡顿等现象。人们…...
Windows平台上的5种敏捷软件开发(过程)模型
我是荔园微风,作为一名在IT界整整25年的老兵,今天总结一下Windows平台上的5种敏捷软件开发(过程)模型。 说到这个问题,你必须先知道除了敏捷模型还有没有其他什么模型?同时要比较模型的区别,首先还要看看什么叫软件开…...
一文实现部署AutoGPT
一文实现部署AutoGPT 简介AutoGPT的概述AutoGPT的用途和优势 预备知识Python基础机器学习基础自然语言处理基础 环境设置Python环境安装和配置需要的库和框架的安装,例如PyTorch, Transformers等 AutoGPT模型加载如何下载和加载预训练的AutoGPT模型模型参数和配置 使…...
数值计算 - 误差的来源
误差的来源是多方面的,但主要来源为:过失误差,描述误差,观测误差,截断误差和舍入误差。 过失误差 过失误差是由设备故障和人为的错误所产生的误差,在由于每个人都有“权利”利用机器进行数值计算,所以在计算…...
【软件测试】5年测试老鸟总结,自动化测试成功实施,你应该知道的...
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 自动化测试 Pytho…...
【Hadoop】二、Hadoop MapReduce与Hadoop YARN
文章目录 二、Hadoop MapReduce与Hadoop YARN1、Hadoop MapReduce1.1、理解MapReduce思想1.2、Hadoop MapReduce设计构思1.3、Hadoop MapReduce介绍1.4、Hadoop MapReduce官方示例1.5、Map阶段执行流程1.6、Reduce阶段执行流程1.7、Shuffle机制 2、Hadoop YARN2.1、Hadoop YARN…...
Python教程:文件I/O的用法
本章只讲述所有基本的的I/O函数,更多函数请参考Python标准文档。 1.打印到屏幕 最简单的输出方法是用print语句,你可以给它传递零个或多个用逗号隔开的表达式。此函数把你传递的表达式转换成一个字符串表达式,并将结果写到标准输出如下&…...
序员工作1年,每天上班清闲,但却焦虑万分,若是你,你会吗?
有个学弟在后台留言 他谈到了自己去年毕业的 因为在大学里边有一些校企合作 所以呢他也是花了钱 然后去培训了有半年 去年毕业之后到现在工作有一年了 那目前的薪资是8,000块钱 虽然说相较于其他同学呢 这个薪资呢还算可以 但是呢 自己每天现在就处于一种非常 压抑的那种状态 所…...
Bed Bath and Beyond EDI 需求分析
Bed Bath and Beyond(Bed Bath and Beyond)是一家美国的家居用品零售商,成立于1971年,总部位于新泽西州Union。该公司在美国、加拿大和墨西哥拥有超过1500家门店。其产品涵盖了床上用品、浴室用品、厨房用品、家居装饰等领域&…...
【5.20】五、安全测试——渗透测试
目录 5.3 渗透测试 5.3.1 什么是渗透测试 5.3.2 渗透测试的流程 5.3 渗透测试 5.3.1 什么是渗透测试 渗透测试是利用模拟黑客攻击的方式,评估计算机网络系统安全性能的一种方法。这个过程是站在攻击者角度对系统的任何弱点、技术缺陷或漏洞进行主动分析&#x…...
java版鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+前后端分离构建工程项目管理系统源代码
鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 1. 项目背景 一、随着公司的快速发展,企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性,公司对内部工程管…...
大语言模型架构设计
【大模型慢学】GPT起源以及GPT系列采用Decoder-only架构的原因探讨 - 知乎本文回顾GPT系列模型的起源论文并补充相关内容,中间主要篇幅分析讨论为何GPT系列从始至终选择采用Decoder-only架构。 本文首发于微信公众号,欢迎关注:AI推公式最近Ch…...
SpringBoot整合Swagger2,让接口文档管理变得更简单
在软件开发的过程中,接口文档的编写往往是一个非常重要的环节,因为它是前端和后端沟通的桥梁,帮助团队更好地协作。然而,手动编写接口文档不仅耗费时间,还容易出错,因此我们需要一种简单的方法来管理接口文…...
socket | 网络套接字、网络字节序、sockaddr结构
欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和…...
golang-websocket
WebSocket 是一种新型的网络通信协议,可以在 Web 应用程序中实现双向通信。 WebSocket与HTTP协议的主要区别是: HTTP 和 WebSocket 协议的区别 HTTP 是单向的,而 WebSocket 是双向的。 在客户端和服务器之间的通信中,每个来自客…...
Nginx + fastCGI 实现动态网页部署
简介 本文章主要介绍下,如何通过Nginx fastCGI来部署动态网页。 CGI介绍 在介绍fastCGI之前先介绍下CGI是什么。CGI : Common Gateway Interface,公共网关接口。在物理层面上是一段程序,运行在服务器上,提供同客户端HTML页面的…...
精彩回顾 | Fortinet Accelerate 2023·中国区巡展厦门站
Fortinet Accelerate 2023中国区 5月16日,Fortinet Accelerate 2023中国区巡展来到魅力“鹭岛”——厦门,技术、产品和业务专家,携手亚马逊云科技、唯一网络等云、网、安合作伙伴,与交通、物流、金融等各行业典型代表客户&#x…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
