Real-Time Rendering-第三章 The Graphics Processing Unit(2)

3.4 The Vertex Shader

Vertex shader是图3.1所示的functional管线中的第一个阶段。由于这是执行任何图形处理的第一个阶段,因此在该阶段之前所进行的一些数据操作是毫意义的。在DirectX中,把vertex shader之前的阶段称为input assembler[123,261],在该阶段可以把大量的数据流组织在一起形成顶点和图元的集合,并发送到下一个管线阶段。例如,一个物体可以由一个坐标位置数组和一个颜色值数组进行表示。在input assembler阶段就可以创建构成物体的triangles(或lines,或points),本质上是创建带有位置和颜色信息的顶点。可以使用同一个位置数组(并使用一个不同的模型变换矩阵)和一个不同的颜色值数组表示第二个物体。在第12章12.4.5节将会详细讨论数据表示。另外,在input assembler阶段还支持执行instancing(同一个物体的多个实例)。这种方法支持通过一次简单的draw call,就可以对同一个物体绘制多次,每次绘制实例包含一些与其他实例不同的数据。在第15章15.4.2节讲述了instancing的具体使用方法。DirectX 10的input assembler阶段还对每个实例,图元和顶点标记了一个标识编号,以使得之后的任意shader阶段都可以读取。在早期的shader models中,这些数据必须被显示地添加到shader model中。

一个triangle mesh由一组顶点和一些额外的信息组成,这些信息描述了组成每一个三角形的顶点。其中vertex shader是第一个处理triangle mesh的管线阶段。Vertex shader阶段并不能描述顶点数据形成了什么样的三角形;正如字面意思,vertex shadre专门处理输入的顶点。一般来说,vertex shader提供了一种方法用于修改,创建或忽略每一个多边形顶点对应的变量,比如顶点的颜色,法向量,纹理坐标以及位置。通常vertex shader程序是把顶点从model space变换到homogeneous clip space(齐次裁剪空间);一个vertex shader至少要输出顶点的位置。

2001年发布的DirectX 8首先引入了此功能。由于vertex shader是当时管线的第一个阶段,而且相对来说不会频繁调用,因此既可以在GPU也可以在CPU上实现,然后把计算的结果发送到GPU中用于光栅化处理。这种做法使得从旧硬件到新硬件的转变重点是速度,而不是功能问题。所有目前生产的GPUs都支持vertex shading。

Vertex shader本身等同于与3.2节描述的通用着色内核虚拟机。每一个输入的顶点首先被vertex shader程序进行处理,然后输出一些由三角形或直线插值计算得到的变量值。另外需要注意的是,vertex shader既不能创建也不能销毁顶点,由某个vertex shader生成的结果不能传递给另一个vertex shader。由于每个vertex shader分别是独立执行的,因此在GPU中可以并行的使用任意数量的shader处理器,处理输入的顶点流。

注:旧版本的shader models还支持输入一个point sprite particle object(点精灵粒子物体)的大小,但是在现在的shader model中sprite功能已经成为了geometry shader的一部分。

后面的章节讲述了大量的vertex shader效果,比如shadow volume creation(阴影体的创建),vertex blending for animating joints(用于动画拼接的顶点混合),和silhouette rendering(轮廓渲染)。Vertex shader还包括一些其他的应用:

  • 镜头效果,使得屏幕出现鱼眼,处于水中,或者其他的失真画面。
  • 物体定义,对一个mesh只创建一次,并使用vertex shader使其变形。
  • 使物体发生扭转,弯曲以及逐渐变窄的操作。
  • 有规律的变形,比如旗帜,布料的飘动,或水的波动。
  • 基本图元的创建,通过把退化的meshes发送到管线中,并由这些meshes产生所需要的区域。在较新的GPUs中已经使用geometry shader替换了该功能。
  • 通过把整个frame buffer的数据作为一个纹理应用于一个与屏幕对齐的mesh上,执行程序化的变形操作,可以产生翻书,热雾,水波纹,以及其他的效果。
  • 顶点纹理的提取方法,可以用于把纹理应用到顶点meshes中,支持使用较少的计算成本就可以生成海面和地形的高度区域[23,703,887]。

这里写图片描述
图3.5 最左边是一个正常的teapot。中间是由一个vertex shader执行简单地的剪切操作产生的结果。右边是使用noise function(噪声函数)创建一个区域扭曲了teapot模型的结果。(图片由NVIDIA免费提供的 FX Composer 2 生成)。

Vertex shader的输出数据有多种不同的使用方法。通常是用于生成每一个实例的trianlges,并进行光栅化处理,个别生成的pixel fragments发送到pixel shader程序中用于进一步的处理。随着Shader Model 4.0的引入,还可以把vertex shader的输出数据发送到geometry shader,streamed out,或者同时发送。这些选项是下一节将会讨论的主要内容。

3.5 The Geometry Shader

随着2006年末DirectX 10的发布,显卡的图形管线中增加了geometry shader。管线中vertex shader的下一个阶段就是geometry shader,并且该shader的应用是可选的。另外,geoemtry shader是Shader Model 4.0所需要的一部分,因此不能应用于之前版本的shader models中.

Geometry shader的输入是单个物体以及对应的顶点数据。通常该物体为mesh中的一个三角形,一条线段或一个点。此外,通过geometry shader可以定义并处理一些扩展的图元。特别是,可以输入位于三角形区域之外的三个额外顶点,以及折线上两个邻接顶点。如图3.6所示。

这里写图片描述
图3.6 Geometry shader程序的shader输入数据是一些简单的类型:point,line segment,trinagle。另外,还可以使用最右图两幅图中的图元,包含邻接顶点的线段和三角形。

Geometry shader阶段处理这些输入的基本图元,并输出0个或更多的图元。输出的图元为points,polylines,以及triangle strips。例如,通过调用一次geometry shader程序可以输出不止一个triangle strip。另外,非常重要的一点是geometry shader可以不产生任何的输出。通过这种方法,可以对一个mesh选择性地进行修改,比如通过编辑顶点,增加新的图元,以及删除其他的图元。

Geometry shader程序设置为输入对象的某种类型,并输出对象的另一种类型,而且这些类型不需要相互匹配。例如,输入类型为triangles,可以把三角形的中心输出为points类型,每一个输入的triangle对应一个point。即使输入和输出对象的类型相匹配,每个顶点附带的数据也可以被省略或扩大。例如,在geometry shader中可以计算三角形的面法线向量,并添加到每个输出顶点的数据中。另外,与vertex shader一样,对每一个生成的顶点,geometry shader必须输出一个位于homogeneous clip space的坐标位置。

Geometry shader执行时会保证生成结果的输出顺序与图元的输入顺序相同。但是这种方式会影响性能,因为如果有多个shader单元并行执行,输出的结果必须要先保存再进行排序。作为功能和性能之间的一个折中,Shader Model 4.0被限制为每次执行最多只能生成1024个32位的变量值。因此,想要通过一片给定的灌木树叶作为输入生成一千片树叶是不可行的,并且不建议使用geometry shader执行这种功能。另外,也不建议使用geometry shader把简单曲面细分为更多细节的triangle meshes[123]。这个阶段更多地是通过可编程性修改输入的数据或执行一些数量有限复制操作,而不是进行大量的复制或放大细节。例如,其中一种应用是生成数据执行变换后的6份拷贝,用于同时渲染一个cube map的6个纹理面,见第8章8.4.3节。此外,利用geometry shader还可以实现一些其他的算法,比如由point数据创建各种大小的粒子,沿着轮廓突起用于毛发渲染,以及查找物体边缘用于阴影算法。图3.7中显示了geometry shader的更多应用。在整本书中都会讨论geometry shader的这些应用。

这里写图片描述
图3.7 Geometry shader的一些应用。左图中是使用GS在运行时处理变形球等值面细分的效果。中间图片显示了使用GS和stream out对线段分开细分,并使用GS生成广告牌的效果。右图是使用vertex和geometry shader以及stream out模拟布料的效果。

3.5.1 Stream Output

GPU管线的正常流程是首先把数据发送到vertex shader中,然后渲染生成的triangles数据并在pixel shader中进行处理。数据总是要在整个管线中进行传递,并且生成的中间结果无法被应用程序访问。于是在Shader Model 4.0中引入了stream output的想法。在vertex shader(以及可选的geoemtry shader)阶段处理完顶点数据之后,可以把这些数据输出到一个数据流中,即一个有序数组,除此之外还要发送到rasterization阶段。实际上,可以完全关闭rasterization阶段,而把管线纯粹作为一个非图形应用的流处理器。使用这种方法处理的数据可以经过管线再发送回应用程序中,因此可以对数据进行反复处理。这种操作方式在模拟流水或其他粒子效果时特别有用,在第10章10.7会进行详细讨论。

3.6 The Pixel Shader

在vertex shader和geometry shader执行完相应的操作之后,基本图元会被裁剪并设置以及用于光栅化,正如上一章所述。这部分操作处理在管线执行过程中是相对固定的,而不是可编程的。在此过程上,会遍历每一个triangle,并通过三角形区域对顶点中包含的所有变量进行插值计算。Pixel shader是下一个可编程阶段。在OpenGL中该阶段又被称为fragment shader,在某些情况下这个名称更贴切。这种想法是指一个三角形覆盖每个像素点单元格的全部或部分,并且所表示的材质为不透明或透明的。光栅器并不会直接影响像素点中存储的颜色,而是生成数据,在一定程序上描述三角形如何覆盖像素的单元格。

注:一个明显的例外是,pixel shader程序也可以指定所使用的插值计算的类型,比如perspective corrected(透视纠正)或screen space(或不使用任何插值计算)。

管线中vertex shader程序的输出成为pixel shader程序的有效输入数据。在Shader Model 4.0中,从vertex shader总共可以向pixel shader传递16个向量值(第一个向量中包含4个分量值)。在使用geometry shader的情况下,则可以输出32个向量到pixel shader中[261]。

注:在DirectX 10.1中,vertex shader程序的输入和输出都为32个向量值。

在Shader Model 3.0中专门为pixel shader增加了一些额外的输入数据。比如,增加了一个输入标记值用于指明三角形的哪一面是可见的。在一次运行过程中,对每一个三角形的正面和背面渲染一个不同材质的情况下,这个标记值是非常有用的。另外,在pixel shader中还可以输入fragment的屏幕位置。

然而,pixel shader也有一定的限制,只能影响到传递到该shader中的fragment数据。也就是说,在pixel shader程序的执行过程中,不能把计算的结果直接发送给相邻的pixels。相反,pixel shader使用由顶点插值计算的结果数据,以及任何保存的常量和纹理数据进行计算的结果只会影响单个像素点。但是,这个限制并不是像听起来那么严重。在第10章10.0节描述了使用图像处理技术,最终可以影响相邻的像素点。

在pixel shader访问(虽然是间接的)邻接像素点信息的其中一种情况是,为了计算像素之间的渐变或导数信息。Pixel shader能够计算任意的输入变量值,用于沿着屏幕的x和y轴改变每一个像素。这对于各种计算和纹理寻址是非常有用的。这些渐变值对于某些操作运算是非常重要的,比如滤波运算(见第6章6.2.2节)。大多数GPUs通过处理 2×2 或更大的单元组的像素实现该功能。当pixel shader请求一个渐变值时,就会返回两个相邻像素点的差值。这种实现方式导致的后果之一是,受dynamic flow control影响的shader部分无法访问渐变信息—位于一个组内的像素必须执行相同的指令。这是pixel shader的一个基本限制,甚至存在于离线渲染系统中[31]。只有pixel shader阶段才能够访问渐变信息,任何其他的可编程shader阶段都不具备这个功能。

通常pixel shader程序会设置fragment color,用于在最后的merging阶段执行合并操作。另外,在rasterization阶段生成的深度值也可以通过pixel shader进行修改。而stencil buffer值不能被pixel shader修改,而是经过pixel shader阶段直接传递到merge阶段。在SM 2.0及以上版本中,pixel shader还可以丢弃输入的fragment数据,即不产生输入数据。这种操作会丧失性能,因为这会导致无法使用GPU执行正常的优化。更多细节请阅读第18章18.3.7节。此外需要注意的是,在SM 4.0[123]版本中已经把一些merge阶段执行的操作如fog computation以及alpha testing,移到了pixel shader阶段进行计算。

目前的pixel shader已经支持执行大量的运算。于是这种在单次渲染流程中计算任意数量的变量值的能力,产生了multiple render targets(MRT)的想法。不再把pixel shader程序的输入结果保存到单个的color buffer中,而把每个fragment生成的多个向量保存到不同的buffers中。这些buffers必须具有相同的尺寸,而且有些架构还要求每一个buffer具有相同的bit depth(位深度,尽管根据需要会有不同的格式)。在列表3.1中列出的PS output registers数量是指可以独立访问的buffers数量,即4个或8个 buffers。与可显示的color buffer不同,任何额外的渲染目标具有一些其他的限制。比如,在这些render targets上无法执行antialiasing(抗锯齿操作)。尽管具有这些限制,MRT依然是一个强大的辅助工具,可以更高效地执行渲染算法。如果要从同一个数据集中计算大量的中间结果图像,只需要执行一次渲染流程即可,而不用对每一个输出buffer都执行一次。与MRTs相关的另一种关键作用是,能够把结果图像当作纹理数据进行读取。

3.7 The Merging Stage

在第2章2.4.4节已经讨论了,merging阶段是对多个单独的fragments(在pixel shader生成的)的深度值和颜色值与frame buffer进行合并。另外,stencil-buffer和Z-buffer操作也在该阶段处理。最后,在该阶段处理的另一个操作是color blending(颜色混合),这个操作常用于transparency(透明)和compositing(合成)操作。

Merging阶段位于fixed-function固定功能管线阶段,比如裁剪操作和完全可编程shader之间。尽管merging阶段是不可编程的,但可执行的操作却是高度可配置的。特别是color blending操作可以设置成执行大量不同的操作。最常用的操作是,有关颜色和alpha值的乘法,加法和减法的组合,还有一些其他的操作也是可选的,比如求最小值和最大值,以及位逻辑运算。此外,在DirectX 10中增加了一种功能能够把来自于pixel shader中的两种颜色值与frame buffer的颜色值进行混合—称为dual-color blending

在使用MRT功能的情况下,可以在多个buffers中执行blending操作。DirectX 10.1中引入了在每个MRT buffer中执行不同的混合操作的功能。在以前的版本中,所有buffers问题执行相同的混合操作(需要注意的是dual-color blending与MRT是不兼容的)。

3.8 Effects

到目前为上,对管线的描述重点讨论了各个可编程阶段。但是控制这些阶段所需要的vertex,geometry和pixel shader程序,并不是相互独立运行的。首先,单个的shader程序在独立运行的情况下是毫无用处的:因为vertex shader程序的输出结果需要被输入到pixel shader中。做任何操作时,这两个shader程序都要加载,而且编程人员必须要执行一些操作使vertex shader程序的输出与pixel shader程序的输入相匹配。通过在几次流程中执行任意数量的shader程序可以产生特定的渲染效果。除了shader程序本身,有时必须在特定配置下设置状态变量,以使得shader程序能正常运行。比如,渲染状态包括是否使用以及如何使用Z-buffer和stencil buffer,还包括fragment如何影响该fragment覆盖的像素值(如,替换,添加或混合)。

由于这些原因,很多组织已经开发了一些effects语言,比如HLSL FX,CgFX,以及COLLADA FX。一种effect文件中试图包含执行一种特定的渲染算法所需要的全部相关信息[261,974]。通常还会定义一些全局的参数变量,这些变量可以被应用程序赋值。例如,在单个effect文件中可能会定义渲染一种令人信服的塑料材质所需要的vertex和pixel shaders。该effect还可以对外定义一些全局参数变量,比如塑料颜色和粗糙度,这样就可以在渲染每一个模型时使用同一种effect文件,只需要修改相应的参数值即可。

为了详细描述effect文件的风格,我们将会一步步讲解从NVIDIA FX Composer 2的effects系统中得到的一个effect示例。在这个DirectX 9 HLSL effect文件中实现了Gooch shading的一个非常简单的形式[423]。Gooch shading的其中一点是使用表面的法向量,并与光源的位置进行比较。如果法向量正对着光源,就会使用一种暖色调对该表面进行着色;如果法向量指向远离光照的地方,则使用冷色调。然后根据这两个法向量的夹角对这两种自定义的颜色进行插值计算。这种shading技术是一种non-photorealistic渲染,在第11将会讨论该主题。这种effect示例程序的运行结果如图3.8所示。

这里写图片描述
图3.8 Gooch shading,从一个暖色调的橙色渐变成一个冷色调的蓝色。(图片由NVIDIA 免费提供的 FX Composer 2 生成)。

在effect文件的开始部分定义了effect变量。最前面几个变量在effect中是与相机位置相关的“不可调整的”参数,在应用程序中自动跟随相机位置的变化而改变:

    float4x4 WorldXf : World;
    float4x4 WorldITXf : WorldInverseTranspose;
    float4x4 WvpXf : WorldViewProjection;

这种定义变量的语法为 type id : semantic。其中float4x4类型用于表示矩阵,id是用户自定义的变量名,semantic表示一个内置的名称。正如semantic(语义)字面意思所表示的,WorldXf变量表示model-to-world变换矩阵,WorldITXf表示矩阵的逆转置,而WvpXf则表示一个从model space变换到相机的clip space的矩阵。这些使用明确的semantics表示的变量值一般由应用程序提供,并且不会在user interface(UI)中显示。

接下来,指定用户自定义的变量:

    float3 Lamp0Pos : Position <
        string Object = "PointLight0";
        string UIName = "Lamp 0 Position";
        string Space = "World";
    > = { -0.5f, 2.0f, 1.25f };

    float3 WarmColor <
        string UIName = "Gooch Warm Tone";
        string UIWidget = "Color";
    > = { 1.3f, 0.9f, 0.15f };

    float3 CoolColor <
        string UIName = "Gooch Cool Tone";
        string UIWidget = "Color";
    > = { 0.05f, 0.05f, 0.6f };

在该定义的尖括号内提供了一些额外的annotations(标注),并指定了变量的默认值。这些annotations由应用程序指定,并且在effect或shader编译器中没有任何作用。通过应用程序可以查询到这些annotations。在这种情况下,annotations描述了如何使用UI对外提供这些变量。

下一步,定义了shader输入和输出的数据结构:

    struct appdata {
        float3 Position : POSITION;
        float3 Normal : NORMAL;
    };

    struct vertexOutput {
        float4 HPosition : POSITION;
        float3 LightVec : TEXCOORD1;
        float3 WorldNormal : TEXCOORD2;
    };

其中结构体appdata定义了模型中每一个顶点的数据,也就是定义了vertex shader程序的输入数据类型。结构体vertexOutput则定义了vertex shader产生的输出数据和pixel shader的输入数据。另外,把TEXCOORD*用作输出变量名的semantic,是管线发展过程中的一种组件结构。首先,在一个表面上可以关联多个纹理,因此把这些额外的数据字段都称为texture coordinates(纹理坐标)。在实际过程上,这些字段里存储了从vertex shader传递到pixel shader的任意数据。

下一步,定义各个shader程序的代码语句。在这里,我们只有一个vertex shader程序:

    vertexOutput std_VS(appdata IN) {
        vertexOutput OUT;
        float4 No = float4(IN.Normal, 0);
        OUT.WorldNormal = mul(No, WorldITXf).xyz;
        float4 Po = float4(IN.Position, 1);
        float4 Pw = mul(Po, WorldXf);
        OUT.LightVec = (Lamp0Pos - Pw.xyz);
        OUT.HPosition = mul(Po, WvpXf);
        return OUT;
    }

在该程序中,首先使用一次矩阵乘法计算位于world space中的表面法向量。变换操作是下一章将要讨论的主题,因此我们不会讲解在这里使用的逆转置矩阵。另外,还要使用离屏变换计算位于world space中的点位置。然后使用光源位置减去该点位置,就可以得到从表面到光源的方向向量。最后,把物体的位置变换到clip space用于进行光栅化处理。这是任何vertex shader程序都要输出的clip space坐标位置。

给定了位于world space中光源的方向和表面的法向量,就可以在pixel shader程序中计算表面的颜色:

    float4 gooch_PS(vertexOutput IN) : COLOR
    {
        float3 Ln = normalize(IN.LightVec);
        float3 Nn = normalize(IN.WorldNormal);
        float ldn = dot(Ln,Nn);
        float mixer = 0.5 * (ldn + 1.0);
        float4 result = lerp(CoolColor, WarmColor, mixer);
        return result;
    }

其中向量 Ln 表示规范化的光源方向向量,而向量 Nn 则表示规范化的表面法向量。经过规范化,这两个向量的点积 ldn 表示它们之前夹角的余弦值。我们将会使用该值在冷色调和暖色调颜色值之间进行线性插值。函数 lerp() 接受一个介于0到1之间的混合值因子,其中0表示使用冷色调,1表示使用暖色调,而介绍0,1之间的值则表示对这两种色调进行混合。由于夹角余弦值的值域为[-1,1],因此使用mixer变量把该值转换到[0,1]范围。然后使用该值对两种色调进行混合,并产生一个含有正确颜色值的fragment。这些shaders程序都是函数。一种effect文件中可以由任何数量的函数组成,并且可以包含来自其他effects文件中的通用函数。

Effect文件中的 pass 通常由一个vertex和pixel(以及geometry)shader组成,以及该pass所需要的任何状态设置。而 technique 则是一种或多种passes的集合,用于产生期望的渲染效果。在这个简单的effect示例文件中,只有一种technique,该technique只有一种pass:

    technique Gooch < string Script = "Pass=p0;"; > {
        pass p0 < string Script = "Draw=geometry;"; > {
            VertexShader = compile vs_2_0 std_VS();
            PixelShader = compile ps_2_a gooch_PS(); }
        ZEnable = true;
        ZWriteEnable = true;
        ZFunc = LessEqual;
        AlphaBlendEnable = false;
    }

注:在DirectX 9及早期的版本中,一种pass还可以不包含shaders,只用于控制固定功能的管线。

在该technique中的状态设置表示,在merging阶段正常使用Z-buffer—可以进行读写,并且在fragment的深度值小于等于buffer中存储的z-depth时表示深度测试通过。另外关闭了alpha blending,即使用该技术的模型都被认为是不透明的。这些规定意味着,如果fragment的z-depth等于或小于Z-buffer中存储的任意值,就使用计算得到的fragment颜色值替换对应的像素点的颜色值。换名话说,这是Z-buffer的一种标准用法。

此外,在同一个effect文件中可以存储多种techniques。通常这些techniques是同一种effect的多个形式,每一种technique指向一个不同的shader model(比如SM2.0相对于SM3.0)。Effects具有大量的应用场景。图3.9中显示了一些现代的可编程shader管线的强大的应用。通常一种effect中封装了相关的techniques。目前已经开发出了各种方法用于管线shaders集合[845,847,887,974,1271]。

这里写图片描述
图3.9 使用可编程shaders产生的各种各样的材质以及post-processing效果。(图片由NVIDIA 免费提供的 FX Composer 2 生成)。

到这里我们已经完成了GPU本身的讲解。除以之外,GPU可以处理很多其他的任务,以及关于相关函数的更多使用和组合方法。本书的中心主题是有关如何充分利用这些功能的理论与算法。基于这些目的,我们讨论的重点将会是深入理解管线中的关键要素,变换和可视化展现。

Further Reading and Resources

David Blythe在一篇有关DirectX 10的论文中[123]很好地概述了现在GPU管线和设计背后的基本原理,以及相关的参考文章。

关于可编程的vertex和pixel shaders的内容就可以单独写满整本书。对此我们建议最好是跳转到相关的网站:查阅ATI[50]和NVIDIA[944]开发者网站,并获取最新的技术信息。他们免费提供的FX Composer 2RenderMonkey交互式shader设计工具套件提供了一种非常好的方法,可以直接试用现成的shadres,并对其进行修改以观察相应的结果。Sander[1105]提供了固定功能管线在SM 2.0版本的HLSL中的一种实现。

要学习shader编程语法格式方面的内容需要一些额外的工作。在OpenGL Shading Language一书中补充了OpenGL红宝书中[969]没有讲到的部分,讲述了OpenGL的可编程shading语言,GLSL。对于HLSL的学习,在每一个新发布的DirectX API中都持续进行了讲解;除了该SDK之外的有关链接和书籍,见本书的配置网站(http://www.realtimerendering.com)。O’Rorke在一篇文章[974]中对effects进行了详细介绍,并提供了管理shaders的高效的方法。Cg语言提供了一种抽象层,可以导出到许多主流的APIs和平台,同时也提供了用于主流建模和动画工具中的插件。Sh元编程语言则更抽象,本质上是一种C++库,用于把与图形相关的代码一一映射到GPU中。

对于高级的shader技术学习,从阅读GPU GemsShaderX系列的书籍开始。在Game Programming Gems系列书籍中同样含有一些相关的文章。最后,DirectX SDK中包含大量重要的shader和算法示例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值