几何着色器着色器

Metal Shaders? Render Pipeline? Vertex Shaders? Fragment Shaders? If you were anything like me, these words and phrases are meaningless or confusing. This tutorial is meant to help you get an easy footing on how it all works and allow you to build off from there.

金属着色器? 渲染管线? 顶点着色器? 片段着色器? 如果您像我一样,这些单词和短语将毫无意义或令人困惑。 本教程旨在帮助您轻松了解所有工作原理,并从那里开始。

建立 (Setup)

We’re going to be starting off with a macOS application. The reason for this is so that we can use our Mac’s GPU in the simulator. If you want to do it for an iOS application, you’ll have to run it on a physical device, since the iOS simulators do not support Metal.

我们将从macOS应用程序开始。 这样做的原因是我们可以在模拟器中使用Mac的GPU。 如果要针对iOS应用程序执行此操作,则必须在物理设备上运行它,因为iOS模拟器不支持Metal。

We’ll now be adding in our NSView subclass (essentially a UIView for Mac) called “MetalCircleView”. This is where we’ll be doing the heavy lifting. The application’s root view controller (called ViewController) will just be displaying this NSView.

现在,我们将添加名为“ MetalCircleView”的NSView子类(实际上是Mac的UIView)。 这就是我们要做的繁重工作。 应用程序的根视图控制器(称为ViewController)将仅显示此NSView。

The first thing we want to do is set up our init functions. We’ll be ignoring the draw function that the class got initialized with and instead going with our own.

我们要做的第一件事是设置我们的init函数。 我们将忽略类初始化所使用的draw函数,而是使用我们自己的类。

We’ll now show the view from within our ViewController to the window using auto layout constraints.

现在,我们将使用自动布局约束从ViewController到窗口显示视图。

Now if we run it, we should get an empty window!

现在,如果我们运行它,我们将获得一个空窗口!

设置我们的MetalKit视图 (Setting Up Our MetalKit View)

  1. Import MetalKit into your MetalCircleView file.将MetalKit导入到MetalCircleView文件中。
  2. Declare our MTKView (Metal-Kit-View) as a class instance variable.将我们的MTKView(Metal-Kit-View)声明为类实例变量。
  3. Constrain it to our view.将其约束到我们的观点。
  4. Set ourselves as its delegate conforming to the MTKViewDelegate.将自己设置为符合MTKViewDelegate的委托。

We now have optional fields to set up for our MetalView, which are discussed here in the documentation.

我们现在有可选字段设置我们的MetalView,这是讨论在这里的文件中。

告诉MTKView如何/何时“更新” (Telling the MTKView how/when to “update”)

We need to tell our view how and when it should redraw itself. We have three options:

我们需要告诉我们看法,它应该如何以及何时重绘。 我们有三种选择:

  1. We let it redraw itself based on its internal timer (continuous)我们让它根据其内部计时器重绘自身(连续)
  2. We tell it when to redraw itself using a setter which will happen based on its internal timer (initiated by us)我们告诉它何时使用设置器重绘自身,该设置器将基于其内部计时器(由我们启动)进行
  3. We directly tell it to draw ignoring its internal timer (initiated by us)我们直接告诉它忽略它的内部计时器(由我们启动)

We’ll be going with number two, as we’re only drawing once and will be relying on using the view’s currentRenderPassDescriptor (more on this later). As per the documentation we need to pause it usingmetalView.isPaused = true and enable its set needs display usingmetalView.enableSetNeedsDisplay = true. This tells it that it should be paused and should wait for us to tell it when it needs to display something.

我们将继续第二,因为我们只绘制一次,并且将依赖于使用视图的currentRenderPassDescriptor(稍后将对此进行更多介绍)。 按照文件,我们需要使用到暂停metalView.isPaused = true并启用其设定需要显示使用metalView.enableSetNeedsDisplay = true 。 这告诉它应该暂停它,并且应该等待我们告诉它何时需要显示一些东西。

将其连接到设备的GPU (Connecting it to the device’s GPU)

Our MTKView needs to be connected to a device, which is of type MTLDevice. You can essentially think of this device as the GPU itself.

我们的MTKView需要连接到MTLDevice类型的设备 。 实际上,您可以将此设备视为GPU本身。

The MTLDevice protocol defines the interface to a GPU

MTLDevice协议定义了与GPU的接口

We can fetch the GPU at run-time using MTLCreateSystemDefaultDevice() in iOS or tvOS, and in macOS. There is another option available for fetching a specific GPU (useful if you want to target a Mac’s dedicated GPU or integrated GPU) but that’s beyond the scope of this tutorial.

我们可以在运行时使用iOS或tvOS以及macOS中的MTLCreateSystemDefaultDevice()来获取GPU。 还有另一个选项可用于获取特定的GPU(如果要以Mac的专用GPU或集成GPU为目标,则很有用),但这不在本教程的讨论范围之内。

We want this metal device to be available globally so we declare it as a class instance variable, initialize it in our setupMetal() function and set it as our metalView’s device.

我们希望此金属设备在全球范围内可用,因此我们将其声明为类实例变量,在setupMetal()函数中对其进行初始化,并将其设置为metalView的设备。

metalDevice = MTLCreateSystemDefaultDevice()metalView.device = metalDevice

Our final product now looks like this and we’re ready to get started with setting up our rendering functionality!

现在我们的最终产品看起来像这样,我们已经准备好开始设置渲染功能!

设置我们的渲染功能 (Setting Up Our Rendering Functionality)

创建命令队列 (Creating the command queue)

The first thing we need to do is make a command queue, a MTLCommandQueue. This queue needs to be unique to our device (our GPU interface); we use it to communicate instructions to our GPU. The instructions are represented by a MTLCommandBuffer and are created for the command queue to execute.

我们需要做的第一件事是创建一个命令队列MTLCommandQueue 。 这个队列对于我们的设备(我们的GPU接口)必须是唯一的; 我们使用它来将指令传达给我们的GPU。 指令由MTLCommandBuffer表示,并为执行命令队列而创建。

Knowing this information at initialization time, we want to create the command queue and keep a reference to it as an instance variable. Then every time we want to render something, we need to create a command buffer object to hold our instructions.

在初始化时就知道了这些信息,我们想创建命令队列并将其引用保留为实例变量。 然后,每次我们要渲染某些东西时,我们都需要创建一个命令缓冲区对象来保存指令。

Since the command queue is unique to our device, we use our device to create it! We’ll want to add this as part of the setupMetal() function.

由于命令队列对于我们的设备是唯一的,因此我们使用我们的设备来创建它! 我们将其添加为setupMetal()函数的一部分。

metalCommandQueue = metalDevice.makeCommandQueue()!

(At this point you should be wondering why I’m force-unwrapping. Make sure you handle your optionals properly!) After setting up the command queue. our code should look like this.

(这时您应该想知道为什么我要强制展开。请确保您正确处理了可选组件!)设置命令队列之后。 我们的代码应如下所示。

发布我们的第一个GPU命令! (Issuing Our First GPU Command!)

Now that we have the basic setup and knowledge to issue our first GPU command, we’ll be rendering an RGBA color value to our MTKView.

现在,我们已经具备了发出第一个GPU命令的基本设置和知识,接下来将向我们的MTKView渲染RGBA颜色值。

The first thing we want to do inside draw is to create our commandBuffer. This will contain the instructions we need to execute our commands!

我们要在draw内做的第一件事是创建我们的commandBuffer。 这将包含执行命令所需的指令!

创建管道 (Creating the pipeline)

Our command buffer needs a pipeline to be fed through. The pipeline needs internal information and interface information. We use a MTLRenderPassDescriptor to configure the interface information. For this tutorial, we don’t need to create our own — we can fetch the default one from the MTKView using .currentRenderPassDescriptor .

我们的命令缓冲区需要流水线。 管道需要内部信息和接口信息。 我们使用MTLRenderPassDescriptor来配置接口信息。 对于本教程,我们不需要创建自己的-我们可以使用.currentRenderPassDescriptor从MTKView中获取默认值。

Now accessing our render pass descriptor’s colorAttachements array property, we can set a value at it’s (0th entry).clearColor which describes the color data for the texture assigned to the view’s current drawable. More simply, this can be thought of as the “background color” for our metal view.

现在访问渲染过程描述符的colorAttachements数组属性,我们可以在其(第0个条目).clearColor处设置一个值,该值描述分配给视图当前可绘制对象的纹理的颜色数据。 更简单地说,可以将其视为金属视图的“背景色”。

Next, we need a MTLRenderCommandEncoder to configure the inside of the pipeline. It’s compiled from our commandBuffer using the renderDescriptor.

接下来,我们需要一个MTLRenderCommandEncoder来配置管道内部。 它是使用renderDescriptor从commandBuffer编译的。

From here we can start inputting vertex data and drawing commands to be drawn on the GPU, or better thought of as “encoding” commands for the GPU to run. For now, we’re not ready to encode any real drawing commands so we’ll leave that for later (I lied to you in the section title :p). We want to see that beautiful blue background color in our MTKView!

从这里开始,我们可以开始输入顶点数据和要在GPU上绘制的绘制命令,或者更好地将其视为让GPU运行的“编码”命令。 目前,我们还没有准备好对任何实际的绘图命令进行编码,因此我们将其留待以后使用(我在标题:p中对您撒谎了)。 我们希望在我们的MTKView中看到美丽的蓝色背景色!

We need to do four things to end the encoding and fire off the commandBuffer to be executed on the GPU and displayed to our view!

我们需要做四件事来结束编码并触发要在GPU上执行并显示在视图中的commandBuffer!

  1. End the encoding.结束编码。

renderEncoder.endEncoding()

renderEncoder.endEncoding()

2. Tell the GPU where to send the rendered result.

2.告诉GPU将渲染结果发送到哪里。

commandBuffer.present(view.currentDrawable!)

commandBuffer.present(view.currentDrawable!)

We can use the MTKView’s currentDrawable, a drawable representing the current frame. A MTLDrawable is a “displayable resource that can be rendered or written to.

我们可以使用MTKView的currentDrawable,它是表示当前帧的drawable。 MTLDrawable是一种可以显示或写入的可显示资源。

3. Add the instruction to our metalCommandQueue

3.将指令添加到我们的metalCommandQueue

commandBuffer.commit()

commandBuffer.commit()

4. Tell our metal view to draw which triggers the draw method, we’ll be adding this at the end of our setupMetal() function, but you can call it anywhere you’d like (after you’ve set up the metal components, of course).

4.告诉我们的金属视图进行绘制以触发draw方法,我们将在setupMetal()函数的末尾添加此setupMetal() ,但是您可以在任意位置调用它(设置金属组件之后) , 当然)。

metalView.needsDisplay = true

metalView.needsDisplay = true

Our draw function should now look like this.

现在,我们的绘制函数应如下所示。

If you hit run, you should see a blue screen!

如果点击运行,您应该会看到一个蓝屏!

注意: (Note:)

Earlier when we were choosing how to update our MTKView, I mentioned we went with setting it manually using the view’s internal timer because of our reliance on the currentRenderPassDescriptor. If we had gone with issuing the draw() command manually, ignoring its timer, we would have had to call it twice, as the view would not have had a currentRenderDescriptor the first time.

早些时候,当我们选择如何更新MTKView时,我提到我们一直使用视图的内部计时器手动设置它,因为我们依赖于currentRenderPassDescriptor。 如果我们不考虑它的计时器而手动发出了draw()命令,那么我们将不得不调用它两次,因为视图第一次不会具有currentRenderDescriptor。

We now need to encode commands into the renderEncoder to let it know what to draw from vertex points passed in. We also need a way to represent this information such that we can create it in our view and the metal shader can use it properly as well. But first, we need to go through a high-level overview of how the GPU actually draws stuff.

现在,我们需要将命令编码到renderEncoder中,以使它知道从传入的顶点中绘制什么。我们还需要一种表示此信息的方式,以便我们可以在视图中创建它,并且金属着色器也可以正确使用它。 但是首先,我们需要深入了解GPU如何实际绘制内容。

管道阶段 (Pipeline Stages)

source). 来源 )。

Encoding Drawing Commands/Vertex Data: The data that the GPU receives, and that must be processed in the pipeline.

编码绘图命令/顶点数据: GPU接收的并且必须在管道中处理的数据。

Vertex Shader: Converts the 3D vertex locations into 2D screen coordinates. It also passes vertex data down the pipeline.

顶点着色器:将3D顶点位置转换为2D屏幕坐标。 它还将顶点数据向下传递到管道。

Tessellation: Subdivides triangles into further triangles to provide higher-quality results.

细分:将三角形细分为更多的三角形,以提供更高质量的结果。

Rasterization: Discretizes the 2D geometric data into 2D discrete pixels. This will also take data attached to each vertex and interpolate it over the whole shape to every rasterized pixel.

栅格化:将2D几何数据离散为2D离散像素。 这还将获取附加到每个顶点的数据,并将其插值到整个形状上的每个栅格化像素。

Fragment Shader: Given the interpolated pixel data from the rasterizer, the fragment shader determines the final color of each pixel.

片段着色器:给定光栅化器插入的像素数据,片段着色器确定每个像素的最终颜色。

着色器 (Shaders)

Metal supports three types of shader functions: Vertex, Fragment, and Compute (kernels). These describe parts of the render pipeline.

Metal支持三种类型的着色器功能:“顶点”,“片段”和“计算”(内核​​)。 这些描述了渲染管道的各个部分。

Source) 来源 )

Vertex Shaders: A function used to manipulate the vertex points of a polygon. It runs on each vertex point we pass in. Here, we can manipulate the position of the vertex points and other properties such as the color.

顶点着色器:用于操纵多边形顶点的功能。 它在传入的每个顶点上运行。在这里,我们可以操纵顶点的位置和其他属性(例如颜色)。

Ex. In the vertex shader, I can manipulate each vertex position, so If I wanted to, I could pass in points to make a circle then manipulate them into a square. I can also pass in a color for each vertex point and then change it inside the function as well.

例如 在顶点着色器中,我可以操纵每个顶点的位置,因此,如果需要,我可以传入点以制作一个圆,然后将它们操纵为一个正方形。 我还可以为每个顶点输入一种颜色,然后在函数内部也进行更改。

Fragment Shaders: A function used to manipulate how the pixels between vertices look. It runs on each pixel between a set of vertex points. Here we can return the color information of each pixel.

片段着色器:用于控制顶点之间的像素外观的功能。 它在一组顶点之间的每个像素上运行。 在这里,我们可以返回每个像素的颜色信息。

Uniform Scalars: At this point, you might wonder: what about passing in scalars? Let’s say a constant Float type to represent the position multiplier of our object; by changing this constant we can make our polygon bigger or smaller. Well, this is called a Uniform because it’s a value that’s uniformly applied to all the points, AKA it doesn’t change.

均匀标量:在这一点上,您可能会想:传递标量怎么样? 假设一个常量Float类型表示对象的位置乘数; 通过更改此常数,我们可以使多边形更大或更小。 好吧,这叫做统一,因为它是一个统一地应用于所有点的值,也就是它不变。

原语 (Primitives)

At the lowest levels, GPUs are designed to render triangles. Triangles are the easiest and most versatile objects for it to work with, and that’s what today’s hardware focuses on doing (StackOverflow explanation here). That doesn’t mean we can only tell the GPU to draw triangles. For example, if you’re using a framework that supports quads (rectangles), you could pass it four points and tell it to draw a rectangle. This makes it easier on the programmer, but in reality, the GPU still breaks down that instruction into two triangle instructions.

在最低级别,GPU被设计为渲染三角形。 三角形是最容易使用且用途最广泛的对象,而这正是当今硬件所关注的事情( 此处的 StackOverflow说明)。 这并不意味着我们只能告诉GPU绘制三角形。 例如,如果您使用支持四边形(矩形)的框架,则可以将其传递四个点,并告诉它绘制一个矩形。 这使程序员更容易,但实际上,GPU仍将该指令分解为两个三角形指令。

Think of it as writing a complicated line of code in a high-level language. When the code gets compiled into assembly, that “one” instruction gets broken down into a series of multiple instructions that the CPU can actually execute.

可以将其视为用高级语言编写复杂的代码行。 当代码被编译为汇编代码时,该“一条”指令将分解为CPU可以实际执行的一系列多条指令。

MTLPrimitiveType The geometric primitive type for drawing commands.

MTLPrimitiveType 绘图命令的几何图元类型。

  1. point — rasterizes a point at each vertex

    —在每个顶点栅格化一个点

  2. line — rasterizes a line between each separate pair of vertices (makes unconnected lines)

    line —栅格化每对单独的顶点之间的线(制作未连接的线)

  3. lineStrip — rasterizes a line between each pair of vertices (makes a series of connected lines)

    lineStrip —栅格化每对顶点之间的线(制作一系列连接的线)

  4. triangle — rasterizes a triangle for every separate triplet of points

    三角形 —为每个单独的三元组栅格化一个三角形

  5. triangleStrip — rasterizes a triangle for every three adjacent triplets of points

    triangleStrip —为每三个相邻的三元组栅格化一个三角形

To summarize, we need three high-level steps to make a circle.

总而言之,我们需要三个高级步骤来画一个圆圈。

  1. Create the vertex points on the CPU.在CPU上创建顶点。
  2. Send the vertex points to the vertex shader.将顶点发送到顶点着色器。
  3. Apply the color in the fragment shader.在片段着色器中应用颜色。

设置我们的金属文件 (Setting Up Our Metal File)

There are multiple ways to do this. What we need is to specify a library for our render encoder to use. This metal library is constructed from .metal files. In the .metal file, we can specify the shader functions. Fun fact: You can also build the library at run time from a string.

有多种方法可以做到这一点。 我们需要指定一个库供渲染编码器使用。 该金属库由.metal文件构造而成。 在.metal文件中,我们可以指定着色器功能。 有趣的事实:您还可以在运行时从字符串构建库。

The first thing we need to do is create our metal file in our project folder. This is just like adding a new file except we select “Metal” instead of “Cocoa Class” or “Swift”.

我们需要做的第一件事是在我们的项目文件夹中创建金属文件。 就像添加一个新文件一样,除了我们选择“金属”而不是“可可粉”或“快速”。

Go ahead and name it CircleShader.metal.

继续并将其命名为CircleShader.metal。

Opening it up, we see that we’re importing the Metal Standard Library and using the metal namespace. The language used here is called the Metal Specification Language. If you’ve ever worked with C++, you’ll notice it already looks similar; this is because the MSL (Metal Shading Language) is based off of C++.

打开它,我们看到我们正在导入Metal标准库并使用metal名称空间。 这里使用的语言称为“ 金属规范语言” 。 如果您曾经使用过C ++,您会发现它看起来已经很相似了。 这是因为MSL(金属着色语言)基于C ++。

创建数据结构以将我们的顶点传递给GPU (Creating a Data Structure to communicate our Vertex Points to the GPU)

We need a common language between our swift file for our vertex points and our metal files. We need to be able to create our vertex points in swift (CPU side), then read them in Metal (GPU side). Container types for the data need to be consistent.

我们需要在用于顶点的快速文件和金属文件之间使用通用语言。 我们需要能够快速创建顶点(CPU端),然后在Metal中读取它们(GPU端)。 数据的容器类型需要保持一致。

First, let’s look at what we need to represent a vertex point: We need a position variable holding two coordinates, and we need a color variable holding the color information for the point.

首先,让我们看一下表示一个顶点所需的内容:我们需要一个具有两个坐标的位置变量,并且我们需要一个具有该点的颜色信息的颜色变量。

If we want to carry two sets of information for one vertex point, then we’ll go with a struct.

如果我们要为一个顶点携带两组信息,那么我们将使用一个结构。

struct VertexIn {    position : vector_float2 //<x,y>    color : vector_float4 //<R,G,B,A>}var verticesForCircle = [VertexIn]() //array of VertexIn

If we want to carry only one set of information, then we don’t need a struct.

如果我们只想携带一组信息,则不需要结构。

var verticesForCircle = [vector_float2]() //array of <x,y>

Let's say we want our circle to be a solid color. It makes no sense to pass in a color with our vertex data, as we can just hardcode it in the shader functions. For this reason, we’ll be going with just a vector float rather than a struct.

假设我们希望圆是纯色。 通过顶点数据传递颜色是没有意义的,因为我们可以在着色器函数中对其进行硬编码。 因此,我们将只使用向量浮点数而不是结构。

You might also notice that the examples above contained vector_floats. Under the Accelerate framework, Apple uses the SIMD library for vectors. It was built for C++and is also available in Swift, so we’ll use it to represent our values.

您可能还注意到上面的示例包含vector_floats。 在Accelerate框架下,Apple使用SIMD库存储矢量。 它是为C ++构建的,在Swift中也可用,因此我们将使用它来表示我们的价值观。

Importing simd into our .metal and .swift files:

将simd导入我们的.metal和.swift文件:

simd C++ (metal)

simd C ++ (金属)

#include <simd/simd.h>

#include <simd/simd.h>

Declaring a vector:

声明向量:

vector_float2 varName;

vector_float2 varName;

simd Swift

西姆·斯威夫特

import simd

import simd

Declaring a vector:

声明向量:

let varName : simd_float2

let varName : simd_float2

Using the SIMD library, we ensure that our data is being represented consistently in memory across the CPU and the GPU.

使用SIMD库,我们可以确保在CPU和GPU的内存中一致地表示我们的数据。

为我们的圆创建顶点 (Creating the Vertex Points for Our Circle)

We can now create our vertex points for our circle! Our first step is to think of how the GPU draws primitives. The more triangles we render, the smoother the circle.

现在,我们可以为圆创建顶点了! 我们的第一步是考虑GPU如何绘制图元。 我们渲染的三角形越多,圆就越平滑。

There are two options here.

这里有两个选择。

1.

1。

Calculate all the points around the perimeter of the circle and shove in the origin point between each two. When we only have a few triangles, you can easily see that we’re really just trying to make enough triangles to hide the flat outer edges.

计算围绕圆的周长的所有点,然后推入每两个点之间的原点。 当我们只有几个三角形时,您可以轻松地看到我们实际上只是在尝试制作足够的三角形以隐藏平坦的外边缘。

2.

2。

Don’t use the origin and instead make all the vertices of the triangle touch the perimeter.

不要使用原点,而应使三角形的所有顶点都触及周边。

There’s no right or wrong answer here, so we’ll go with the easier option (option 1).

这里没有正确或错误的答案,因此我们将使用更简单的选项(选项1)。

We’re going to create an instance variable called circleVertices and a function called createVertexPoints() . Inside the createVertexPoints() function, we’ll want a helper function to calculate degrees to radians as we’ll be using Swift trigonometry functions.

我们将创建一个名为circleVertices的实例变量和一个名为createVertexPoints()的函数。 在createVertexPoints()函数内部,我们将需要一个辅助函数来计算弧度,因为我们将使用Swift三角函数。

Our MetalCircleView class should now look like this:

我们的MetalCircleView类现在应如下所示:

Since there are 360 degrees in a circle we can make n*360 perimeter points (where n represents non-zero, positive integers) with (n*360)/2 origin points. Essentially the bigger n is the more triangles we render and the smoother the circle is. Fortunately, n=2 is good enough for us.

由于一个圆具有360度,我们可以用(n * 360)/ 2个原点制作n * 360个边界点(其中n表示非零,正整数)。 本质上,n越大,我们渲染的三角形越多,圆也越平滑。 幸运的是,n = 2对我们已经足够了。

I’ll skimp on the trigonometry lesson, but here’s how we get 720 perimeter points.

我将跳过三角学课,但是这是我们获得720个周界点的方法。

Actually, it’s 721 perimeter points. This is because we want to do a full circle (literally). We start off at 0* and we want to make sure we end off on 360*. If we had gone from 0…<720, we would have ended on 395.5*. This makes a noticeable difference as there would be a sliver of the circle unfilled if we left it this way. Now in between every two perimeter points, we need to form a triangle with the origin.

实际上,它是721个周长点。 这是因为我们想做一个完整的圈(字面上)。 我们从0 *开始,并希望确保以360 *结尾。 如果从0…<720开始,我们将以395.5 *结尾。 这产生了显着的差异,因为如果我们以这种方式将其留出,则将有一个小圆圈未填充。 现在,在每两个边界点之间,我们需要与原点形成一个三角形。

It’s worth noting that the points we’re creating are normalized to the screen. In Apple’s Hello Triangle Example, it’s defined as:

值得注意的是,我们创建的点已在屏幕上标准化。 在Apple的Hello Triangle Example中,其定义为:

The vertex function translates arbitrary vertex coordinates into normalized device coordinates, also known as clip-space coordinates. Clip space is a 2D coordinate system that maps the viewport area to a [-1.0, 1.0] range along both the x and y axes.

顶点函数将任意顶点坐标转换为规范化的设备坐标,也称为裁剪空间坐标 。 剪辑空间是2D坐标系,可将视口区域沿x和y轴映射到[-1.0,1.0]范围。

What this means is that the area we can render points in goes from -1.0 to 1.0 on both the x- and y-axis, and that this coordinate system maps to a viewport area. In our case, we haven’t touched the viewport area, so the viewport area is our entire MTKView.

这意味着我们可以渲染点的区域在x轴和y轴上都从-1.0变为1.0,并且此坐标系映射到视口区域。 在我们的案例中,我们没有触摸视口区域,因此视口区域是我们整个MTKView的区域。

We’re now ready to send this data to the GPU and create shader functions :)

现在我们准备将这些数据发送到GPU并创建着色器功能:)

设置着色器功能 (Setting Up the Shader Functions)

指针和内存 (Pointers and memory)

In Metal Shading Language Specification Chapter 4

在《 金属着色语言规范》第4章中

Arguments to Metal graphics and kernel functions declared in a program that are pointers must be declared with the Metal device, threadgroup, threadgroup_imageblock, or constant address space attribute.

必须使用Metal设备,线程组,threadgroup_imageblock或常量地址空间属性声明程序中声明为指针的Metal图形和内核函数的参数。

These specify what address space in the GPU the array should be stored in. device attribute specifies a read-write address space and constant specifies a read-only address space.

这些指定哪些地址空间在GPU阵列应存放在。 device属性指定一个读写地址空间和constant指定只读地址空间。

程序范围函数常量 (Program scope function constants)

Program scope variables declared with (or initialized with) the following attribute are function constants:[[function_constant(index)]]

用以下属性声明(或用以下属性初始化)的程序范围变量是函数常量 :[[function_constant(index)]]

These attributes are usually used on parameters to let metal know where to pass in specific data.

这些属性通常用于参数,以使金属知道在何处传递特定数据。

First I’ll show you the template, and then explain what’s going on.

首先,我将向您显示模板,然后说明发生了什么。

vertex function

顶点功能

const constant vector_float2 *vertexArray [[buffer(0)]]

const constant vector_float2 *vertexArray [[buffer(0)]]

The first parameter is us taking in our array of vertex points that we’ll be passing in. Breaking down the syntax, we see we have a pointer to an array of vector floats. The vertex data, as you will see soon, needs to be passed in as “buffer data”. The [[buffer(0)]] specifies that we want the first (and our only) buffer data to be passed into this parameter. The constant attribute tells metal to store the vertex data in read-only memory space.

第一个参数是我们要传入的顶点集合。分解语法,我们看到有一个指向矢量浮点数组的指针。 如您将很快看到的,顶点数据需要作为“缓冲区数据”传递。 [[buffer(0)]]指定我们希望将第一个(也是唯一的)缓冲区数据传递到此参数中。 constant属性告诉Metal将顶点数据存储在只读存储空间中。

unsigned int vid [[vertex_id]]

unsigned int vid [[vertex_id]]

The second parameter vid stands for “vector id”. This uniquely identifies which vertex we’re currently on; it will be used as the index for our vertexArray. Just as how in our vertexArray parameter we needed to let metal know that it needs to pass in, we let metal know to pass our vertex id into the vid parameter using [[vertex_id]] .

第二个参数vid代表“向量id”。 这唯一地标识了我们当前位于哪个顶点; 它将用作我们的vertexArray的索引。 就像我们需要在vertexArray参数中让Metal知道需要传入一样,我们也可以让Metal知道使用[[vertex_id]]将顶点id传递给vid参数。

VertexOut

VertexOut

The output is of type VertexOut, which holds a position vector and a color vector. The output first goes through tessellation/rasterization, so the [[position]] attribute tells metal to use the position field of the struct as the for the normalized screen position. You may have noticed by now that this is a 4D field instead of the 2D we pass in for a position. The 3rd/4th coordinates represent the depth and homogenous space — something we don’t have to worry about. That VertexOut struct will then be getting fed into the input of our fragment function from which we’ll want to use the color field.

输出的类型为VertexOut,其中包含位置向量和颜色向量。 输出首先经过镶嵌/栅格化,因此[[position]]属性告诉金属将结构的position字段用作标准化屏幕位置。 您现在可能已经注意到,这是一个4D字段,而不是我们传递给某个职位的2D字段。 第3/4个坐标表示深度和同质空间-我们不必担心。 然后,该VertexOut结构将被馈入我们要使用color字段的fragment函数的输入中。

fragment function

片段功能

VertexOut interpolated [[stage_in]]

VertexOut interpolated [[stage_in]]

We have only one input parameter here of type VertexOut called interpolated. The [[stage_in]] attribute tells the metal that the variable should be fed in the interpolated result of the rasterizer.

这里只有一个输入类型为VertexOut输入参数interpolated[[stage_in]]属性告诉金属应该将变量输入光栅化器的内插结果中。

The output is just an <R, G, B, A> color that we fetch from the VertexOut struct that was passed through from the vertexShader function.

输出只是我们从VertexShader函数传递的VertexOut结构中获取的<R,G,B,A>颜色。

填充着色器功能 (Populating the Shader Functions)

  1. We get the current vertex from the buffer using the vertex id.我们使用顶点ID从缓冲区中获取当前顶点。
  2. We initialize the output of type VertexOut.我们初始化VertexOut类型的输出。
  3. We set the output’s 4D position information with just the 2D position from our currentVertex point.我们仅使用从currentVertex点开始的2D位置设置输出的4D位置信息。
  4. We return the output to be rasterized and then passed into our fragment shader.我们将输出栅格化,然后传递到片段着色器中。
  5. In our fragment shader, we just return the color.在片段着色器中,我们只返回颜色。

一些有趣的注意事项: (Some interesting notes:)

  • If you don’t include the [[position]] attribute in the struct, then you’ll get a compile error telling you that VertexOut is an invalid return type.如果未在结构中包含[[position]]属性,则将收到一个编译错误,告诉您VertexOut是无效的返回类型。
  • If you’re only passing out a vector_float4 with no struct, metal will automatically inference it to being the coordinates.如果只传递没有结构的vector_float4,则metal会自动将其推断为坐标。

此处的优化: (Optimizations here:)

You may have noticed that instead of passing through the color we can just hardcode its return value in the fragmentShader itself. This is a good optimization for us (having a solid color for the circle), but it’s not a scalable solution for anything else.

您可能已经注意到,除了传递颜色之外,我们还可以在fragmentShader本身中对返回值进行硬编码。 对我们来说,这是一个很好的优化(圆圈为纯色),但对于其他任何事物,它都不是可扩展的解决方案。

设置渲染管线 (Setting Up Our Rendering Pipeline)

This is our last step! Hooray. Now that we have the vertex points to make the circle and the metal shaders to render it, all we have to do is to use our metal shaders as part of our pipeline and feed it in the vertex points as buffer data!

这是我们的最后一步! 万岁。 现在我们有了顶点来制作圆,并使用了金属着色器进行渲染,我们要做的就是将我们的金属着色器用作管道的一部分,并将其作为缓冲数据输入顶点!

Here’s where we left off last time in our draw function in the MetalCircelView class.

这是我们上次在MetalCircelView类的draw函数中MetalCircelView

We created a command buffer to be added to our commandQueue, which was created for our GPU interface. We set up the input and output of the pipeline. Now all that’s left is to tie the renderEncoder (or the “inside of our pipeline”) with our shader functions and pass it in our vertex points as buffer data!

我们创建了一个命令缓冲区以添加到我们的commandQueue中,该命令队列是为我们的GPU接口创建的。 我们设置管道的输入和输出。 现在剩下的就是将renderEncoder (或“管道内部”)与着色器函数绑定在一起,并将其作为缓冲区数据传递到顶点!

将我们的Metal函数绑定到renderEncoder中 (Tying in our metal functions into our renderEncoder)

The first step is to create a MTLRenderPipelineState .

第一步是创建MTLRenderPipelineState

To use MTLRenderCommandEncoder to encode commands for a rendering pass, specify a MTLRenderPipelineState object that defines the graphics state, including vertex and fragment shader functions, before issuing any draw calls.

要使用MTLRenderCommandEncoder对渲染过程的命令进行编码,请在发出任何绘制调用之前,指定一个MTLRenderPipelineState对象,该对象定义图形状态,包括顶点和片段着色器功能

To create a pipeline state, we need a MTLRenderPipelineDescriptor .

要创建管道状态,我们需要一个MTLRenderPipelineDescripto r

An argument of options you pass to a device to get a render pipeline state object.

传递给设备以获取渲染管线状态对象的选项的参数。

So we’re going to create a new class instance variable for the MTLRenderPipelineState and a function to create the MTLRenderPipelineState which we’ll call in the setupMetal() function right before making our view draw.

因此,我们将为MTLRenderPipelineState创建一个新的类实例变量,并为创建MTLRenderPipelineState创建一个函数,在进行视图绘制之前,将在setupMetal()函数中调用该函数。

To create the pipeline state, we need to:

要创建管道状态,我们需要:

  1. Create the pipeline descriptor.创建管道描述符。
  2. Find our metal files using the GPU Interface.使用GPU接口查找我们的金属文件。
  3. Tell the pipeline descriptor what our vertex and fragment functions are called.告诉管道描述符,我们的顶点和片段函数被调用了什么。
  4. Tell the pipeline descriptor in what format to store the pixel data.告诉流水线描述符以什么格式存储像素数据。
  5. Create the pipeline state from the pipeline descriptor.从管道描述符创建管道状态。

As usual, make sure to handle throws and optionals properly (do as I say not as I do).

和往常一样,请确保正确处理throws和optionals(按照我说的做,不要像我那样做)。

Now to connect it to our render encoder all we have to do is use its setRenderPipelineState function.

现在要将其连接到渲染编码器,我们要做的就是使用其setRenderPipelineState函数。

We’re now ready to draw primitives from our vertex points!

现在,我们准备从顶点绘制基本体!

将顶点转换为缓冲区数据 (Turning the vertex points into buffer data)

First, we need to create the buffer data which is of type MTLBuffer . The documentation on this is worth a read to understand what’s going on.

首先,我们需要创建MTLBuffer类型的缓冲区数据。 值得一读的文档是为了了解正在发生的事情。

A MTLBuffer object can be used only with the MTLDevice that created it. Don’t implement this protocol yourself; instead, use the following MTLDevice methods to create MTLBufferobjects:

MTLBuffer对象只能与创建它的MTLDevice一起使用。 不要自己实现此协议; 而是使用以下MTLDevice方法创建MTLBuffer对象:

  1. makeBuffer(length:options:)

    makeBuffer(length:options:)

creates a MTLBuffer object with a new storage allocation.

创建具有新存储分配的MTLBuffer对象。

2. makeBuffer(bytes:length:options:)

2. makeBuffer(bytes:length:options:)

creates a MTLBuffer object by copying data from an existing storage allocation into a new allocation.

通过将数据从现有存储分配复制到新分配来创建MTLBuffer对象。

3. makeBuffer(bytesNoCopy:length:options:deallocator:)

3. makeBuffer(bytesNoCopy:length:options:deallocator:)

creates a MTLBuffer object that reuses an existing storage allocation and does not allocate any new storage.

创建一个MTLBuffer对象,该对象重用现有的存储分配,并且不分配任何新的存储。

We want to go with option two, as we already have the data stored in our circleVertexes array

我们想使用选项二,因为我们已经将数据存储在circleVertexes数组中

We declare our vertexBuffer at the top as an instance variable:

我们将顶部的vertexBuffer声明为实例变量:

private var vertexBuffer : MTLBuffer!

private var vertexBuffer : MTLBuffer!

And then populate it inside of our setupMetal() function:

然后将其填充到我们的setupMetal()函数中:

vertexBuffer = metalDevice.makeBuffer(bytes: circleVertices, length: circleVertices.count * MemoryLayout<simd_float2>.stride, options: [])!

The makeBuffer function takes “length” number of bytes from our circleVertices and stores it into GPU/CPU accessible memory. For the length, we get the stride (The number of bytes from the start of one instance of T to the start of the next when stored in contiguous memory or in an Array<T> ) from the MemoryLayout of the data’s type (in our case a simd_float2) and multiply it by the number of entries of that type we have in the array.

makeBuffer函数从makeBuffer获取“长度”个字节,并将其存储到GPU / CPU可访问的内存中。 对于长度,我们从数据类型的MemoryLayout中获得跨度(从T的一个实例的开始到存储在连续内存或Array<T>中的下一个实例的开始的字节数)。 (如果是simd_float2),则将其乘以数组中该类型的条目的数量。

Tying it all together, we’re left with this:

将它们捆绑在一起,剩下的就是:

绘制我们的第一个基元(圆!) (Drawing Our First Primitive (the Circle!))

We’re almost there! We have everything in place to issue our draw command on the render encoder. Here is where the documentation for the MTLRenderCommandEncoder really becomes important. There’s two notable sections:

我们快到了! 我们已经准备就绪,可以在渲染编码器上发出绘制命令。 这是MTLRenderCommandEncoder的文档真正变得重要的地方。 有两个值得注意的部分:

1.指定顶点功能的资源(缓冲区数据) (1. Specifying resources for a vertex function (buffer data))

func setVertexBuffer(MTLBuffer?, offset: Int, index: Int)

func setVertexBuffer(MTLBuffer?, offset: Int, index: Int)

Sets a buffer for the vertex function.

设置顶点函数的缓冲区。

Remember how we used the [[buffer(some index)]] attribute for our vertexArray parameter in our vertex shader function? Well, in our draw function, we can set the vertexBuffer at a specific index such that metal knows which input parameter to pass it to.

还记得我们在顶点着色器函数中如何将[[buffer(some index)]]属性用于vertexArray参数吗? 好吧,在我们的绘制函数中,我们可以将vertexBuffer设置为特定的索引,这样金属就可以知道将其传递给哪个输入参数。

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

Here setting the index to 0 corresponds to the [[buffer(0)]] attribute. The offset specifies the starting point of our buffer data that we want to assign to that index. Since we care about all of our vertex points we set the offset to 0.

此处将索引设置为0对应于[[buffer(0)]]属性。 偏移量指定了我们要分配给该索引的缓冲区数据的起点。 由于我们关心所有顶点,因此将偏移量设置为0。

2.绘制几何图元 (2. Drawing geometric primitives)

func drawPrimitives(type: MTLPrimitiveType, vertexStart: Int, vertexCount: Int)

func drawPrimitives(type: MTLPrimitiveType, vertexStart: Int, vertexCount: Int)

Encodes a command to render one instance of primitives using vertex data in contiguous array elements.

使用连续数组元素中的顶点数据对命令进行编码以渲染图元的一个实例。

This is what triggers our vertexShader function to run. Everything we’ve done so far has been for this moment. We tell our render Encoder to draw a specific primitive (remember when we went over the MTLPrimitiveTypes), what vertex to start from, and the vertexCount.

这就是触发我们的vertexShader函数运行的原因。 到目前为止,我们到目前为止所做的一切都是如此。 我们告诉渲染Encoder绘制一个特定的图元(记住当我们遍历MTLPrimitiveTypes时),从哪个顶点开始以及vertexCount。

You may be wondering why we need to specify vertexStart point and vertexCount point. This is needed when you want to create different primitive types in the same render pass. If your first 1000 vertexes are for triangles and the next 1000 are for lines, you will want to specify from what vertex does the next primitive type start.

您可能想知道为什么我们需要指定vertexStart点和vertexCount点。 当您想在同一渲染过程中创建不同的原始类型时,这是必需的。 如果您的前1000个顶点用于三角形,而后1000个顶点用于线,则需要指定下一个基本类型从哪个顶点开始。

renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 1081)

renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 1081)

We have 1081 vertex points and we want to render triangles from the very first point.

我们有1081个顶点,我们想从第一个点开始绘制三角形。

Finally, our draw function should look like this:

最后,我们的绘制函数应如下所示:

All we need to do is press run and we should see our first circle!

我们需要做的就是冲压运行,我们应该看到我们的第一个圆圈!

Wait a minute…. now, that doesn’t look like a complete circle. We can clearly see the distinction between the triangles from the origin. Not to mention these weird artifacts from the rendering. Looks like our original idea for the triangles doesn’t make sense.

等一下…。 现在,这看起来还不完整。 从原点可以清楚地看到三角形之间的区别。 更不用说渲染中的这些奇怪的工件了。 看起来我们最初对三角形的想法没有意义。

Let’s look back at the triangle primitive options — we have two:

让我们回顾一下三角形基本选项-我们有两个:

  1. triangle — rasterizes a triangle for every separate triplet of points

    三角形 —为每个单独的三元组栅格化一个三角形

  2. triangleStrip — rasterizes a triangle for every three adjacent triplet of points

    triangleStrip —为每三个相邻的三元组栅格化一个三角形

What if we change the primitive type from triangle to triangleStrip?

如果将基本类型从triangle更改为triangleStrip怎么办?

We now have a full circle, hooray! We’ve essentially closed the gaps by drawing more triangles with the points we’ve created.

我们现在有一个完整的圈子,万岁! 实际上,我们通过使用创建的点绘制更多的三角形来缩小差距。

A visual representation of how the gaps were filled with more triangles using another color
使用其他颜色直观显示间隙如何被更多的三角形填充

To tie it all together, our MetalCircleView class should look like this:

要将它们捆绑在一起,我们的MetalCircleView类应如下所示:

Full source code on my Github here.

我Github上完整的源代码在这里 。

剩下的问题:视口 (Remaining Question: The Viewport)

At this point, you should be wondering why the circle scales and stretches with the window. Keep in mind we’ve constrained our metal view to our window, so changing that around stretches our “normalized” 2D coordinate space. If you remember in the “Creating our Vertex Points” section, we saw that the normalized coordinate space is mapped to our MTLViewPort .

在这一点上,您应该想知道为什么圆会随窗口缩放和延伸。 请记住,我们已经将金属视图限制在窗口中,因此更改周围的位置会拉伸“规范化”的2D坐标空间。 如果您还记得“创建我们的顶点”部分,我们看到标准化的坐标空间已映射到我们的MTLViewPort

There are two ways to handle this:

有两种方法可以解决此问题:

  1. Constrain the MTKView such that it’s width == height (either ratio or hardcoded value).约束MTKView,使其宽度==高度(比率或硬编码值)。
  2. Set the viewport on the renderEncoder in the draw function.在draw函数中的renderEncoder上设置视口。

Which leads perfectly into the last section of this tutorial :)

完美地进入了本教程的最后一部分:)

从这往哪儿走 (Where to Go From Here)

We’ve just created our very first circle in metal! We learned how to use the basics of metal (setting up our rendering pipeline), use a shading language (The Metal Shading Language), learned how a GPU draws, and drawn our first primitives to make a circle!

我们刚刚创建了我们的第一个金属圈! 我们学习了如何使用金属的基础知识(设置渲染管线),使用着色语言(金属着色语言),学习了GPU如何绘制以及绘制了我们的第一个图元来画圆!

The next steps I would suggest are:

我建议的下一步是:

  1. Passing more fields into the vertexArray in the metal function. Think back to when we chose to represent our vertices using only one field. Try passing in the vertices as a struct with a color field as well.在metal函数中将更多字段传递到vertexArray中。 回想一下当我们选择仅使用一个字段表示顶点时。 尝试将顶点也作为带有色域的结构传递。
  2. Pass in buffer data to the fragment shader function.将缓冲区数据传递到片段着色器函数。
  3. Drawing more shapes in one render pass.在一个渲染通道中绘制更多形状。
  4. Setting the viewport area upon the drawableSizeWillChange delegate method of the MTKView by making the view redraw itself.通过使视图自身重绘,在MTKView的drawableSizeWillChange委托方法上设置视区区域。

I hope you’ve enjoyed this not-so-brief introduction into metal :). The complete project can be found on my GitHub page here.

希望您喜欢这个不太简短的金属简介:)。 完整的项目可以在我的GitHub页面上找到 。

Also, check out the next tutorial in this series!

另外,请查看本系列的下一个教程!

翻译自: https://medium.com/better-programming/making-your-first-circle-using-metal-shaders-1e5049ec8505

几何着色器着色器


http://www.taodudu.cc/news/show-5804977.html

相关文章:

  • vue 3.0 rfc_Ultimate Developer PC 2.0-第2部分-构建WEI 7.9和RFC(用于构建GOM)的更新和PODCAST(上帝自己的计算机)...
  • linux ubuntu 安装光盘,LINUX系统(Ubuntu)光盘安装图解.doc
  • 糟糕的一周
  • COLMAP+OpenMVS实现物体三维重建mesh模型
  • 时间间隔感测试器2.0
  • 安装A93WebService,[SC] OpenService 失败报错解决
  • Solidworks蓝屏问题
  • 反激式开关电源设计方案,12V6A输出
  • KBPC610-ASEMI整流方桥6A 1000V
  • RS607-ASEMI整流桥6A 1000V
  • TBM610-ASEMI迷你贴片整流桥6A 1000V
  • ISTA6A SIOC 亚马逊包装认证
  • cat6 万兆_CAT6以及CAT6A系统万兆测试方法
  • ValueError: unsupported format character ‘j‘ (0x6a) at index 4
  • HK32F103CBT6A最小开发版系统
  • 100V输入、6A输出的降压 DC/DC控制器-QX3901
  • ASEMI快恢复二极管6A10参数,6A10规格,6A10封装
  • 1688API item_get - 获得1688商品详情
  • HBase技术介绍(来源:搜索技术博客-淘宝)
  • 新笔记本电脑安装Ubuntu16.04.5和windows10双系统
  • 回收站服务器运行失败,win7回收站清空无效怎么办|win7清空回收站失败的解决方法...
  • win7计算机回收站网络闪退,win7使用过程中在回收站上出现的常见问题及解决方法汇总...
  • 电脑打开回收站显示服务器运行失败,win7 64位系统无法清空回收站的故障原因及解决方法...
  • 怎么恢复win7上网本由于删除后清空回收站的文件
  • win7回收站恢复方法
  • 如何自定义Latex快捷键
  • Ubuntu篇——终端操作常用快捷键
  • Ubuntu常用快捷键-快速打开关闭终端以及更多常用指令
  • ROS学习VScode常用快捷键
  • 基于STM32的ESP8266WiFi模块波形检测仪

几何着色器着色器_使用金属着色器制作第一个圆圈相关推荐

  1. python装饰器调用顺序_聊一聊Python装饰器的代码执行顺序

    为什么写这篇文章? 起因是QQ群里边有人提了一个问题:之前导入模块只需要1~2秒,为什么现在变成需要2~3分钟? 我的第一感觉是:是不是导入的模块顶层代码里边,做了什么耗时的事情.隔了一天,他的问题解 ...

  2. 发那科机器人控制柜示教器不通电_邳州FANUC示教器维修维修{机器人故障免费检测}...

    发那科驱动器维修发那科Fanuc伺服放大器F436的故障解决方案_发那科机器人维修,FANUC机器人保养,伺服电机示教器减速器维修,驱动器维修,苏州罗韦发那科机器人维修 本文介绍发那科Fanuc伺服放 ...

  3. 神经网络优化器的选择_神经网络:优化器选择的重要性

    神经网络优化器的选择 When constructing a neural network, there are several optimizers available in the Keras A ...

  4. 索氏提取器使用注意_索氏提取器的使用方法

    展开全部 首先,先来认识一下索氏提取器62616964757a686964616fe59b9ee7ad9431333431373230在整套装置构造与名称,如图所示: 索氏提取器的使用方法: 1.在烧 ...

  5. 安卓手机小说阅读器_书城小说阅读器app下载_书城小说阅读器手机版下载

    书城小说阅读器是一款智能小说阅读软件,可以智能搜索全网的小说,通过关键字搜索小说,一键轻松阅读小说,感兴趣的朋友快来下载书城小说阅读器吧. 书城小说阅读器app特色 1.内置智能查找.目录查找.关键字 ...

  6. 三人抢答器逻辑电路图_三人抢答器plc程序图分享

    plc梯形图是使用得最多的图形编程语言,被称为PLC的第一编程语言.梯形图与电器控制系统的电路图很相似,具有直观易懂的优点,很容易被工厂电气人员掌握,特别适用于开关量逻辑控制.梯形图常被称为电路或程序 ...

  7. 说说id获取器手机版_说说id获取器.

    1.我望着圆月,像在凝视着你亲爱的,就在那明亮的月镜中,我看到了一个同样凝视我的你. 2.这世道,你这样的傻逼狗也敢在这里得瑟? 3.你长的拖慢网速,你长的太耗内存光着身子追我两公里我回一次头都算我是 ...

  8. 擦窗机器人出发点_一种擦窗机器人初步设计与实现

    龙源期刊网 http://www.qikan.com.cn 一种擦窗机器人初步设计与实现 作者:朱彪彪 徐军委 于阳光 安春桥 来源:<智富时代> 2019 年第 08 期 [摘 要]设计 ...

  9. 拼装机器人感想_体验亲子拼装机器人的乐趣

    原标题:体验亲子拼装机器人的乐趣 假日亲子群 上周日上午,海峡导报假日亲子营组织了20组家庭到外图书城的小玩伴,共同体验亲子拼装机器人的乐趣. 活动中,孩子们了解什么是机器人,接触认知齿轮.轴套等零件 ...

最新文章

  1. Active Directory之Sysvol的修复、移动及重建
  2. angularJs的学习笔记
  3. 【MM模块】Reservation 预留
  4. lynx---CentOS终端访问IP
  5. 由于找不到mfc110.dll,无法继续执行代码的解决方法
  6. c语言省二历年真题讲解安徽,安徽省计算机等级二考试真题C语言年12月.doc
  7. 第015讲 仿sohu首页面布局
  8. (转)Tomcat目录结构
  9. 讨厌的任意门事件,删了会出系统提示:安装prosheild.msi问题,不要乱删!
  10. CANoe软件中制作DBC文件的小教程
  11. 系统测试缺陷检出密度越大越好吗?
  12. 办公文件实时自动同步工具-FileYee,好用!
  13. 如何注册gmail邮箱
  14. web前端 作业 1
  15. 遥感图像的空间分辨率,光谱分辨率等
  16. OpenSSL BIO 自我扫盲
  17. Asp.net Ajax Control Toolkit设计编程备忘录(色眼窥观版)——第4回(忍者专辑)
  18. 好文推荐:强悍VC:谍影迷踪
  19. Java训练01“ 根据身高体重计算BMI指数”
  20. 发个军棋游戏(军棋1v1,下载)

热门文章

  1. 打破那面镜子,做回真正的自己(深度好文)
  2. 通信运营商真的每天躺赚4个亿吗?
  3. 半导体、晶体管、电容、电阻、芯片、PCB的区别
  4. 过滤器和拦截器的执行顺序
  5. 扑克牌魔术里面的算法
  6. 多吃防晒食物 肌肤白皙年轻
  7. EXCEL 页眉页脚生成脚本
  8. css实现背景模糊,文字清晰效果
  9. 手握智算中心“绿洲”,毫末跑在中美自动驾驶长跑第一线
  10. iOS 内存泄漏排查方法及原因分析