Decorative image frame

【笔记】【百人计划】图形3.3 曲面细分与几何着色器

一、背景

1.1应用

  • 曲面细分着色器
    • 海浪(曲面细分+顶点位移动画)
    • (交互)雪地(曲面细分+顶点位移)
    • 置换贴图(曲面细分+顶点位移)
  • 几何着色器
    • 几何动画(几何图元)
    • 草地等(与曲面细分着色器结合)

1.2渲染管线

  • 顶点着色器
  • 曲面细分着色器
    • 细分控制着色器Hull Shader(TCS)
    • Tessellation Primitive Generator(不可编程)
    • 细分计算着色器Domain Shader(TES)
  • 几何着色器
  • 片元着色器

1.3曲面细分着色器

1.3.1TESS的输入与输出
  • Hull Stage

    • Hull function
      • 每个patch每个顶点运行一次
      • 输入
        • Patch,可以看成是多个顶点的集合,包含每个顶点的属性,可以指定一个Patch包含的顶点数以及自己的属性
        • Index,指定hull shader输出patch中的哪一个顶点
      • 输出patch内对应顶点
    • Patch Constant Function
      • 输入Patch
      • 每个patch运行一次
      • 输出Tessellation Factor
    • 两个阶段是并行
    • 功能
      • 将图元细分(三角形、矩形等)
    • 输出
      • 细分后的顶点数据
  • Tessellation Primitive Generator

    • 为新网格的所有顶点生成重心坐标
  • Domain stage

    • 输入重心坐标、Patch
    • 为细分后曲面的所有顶点运行一次
    • 通常在顶点着色器的逻辑操作的部分,应该放在这里
  • (Geometry Stage)

1.3.2HULL shader参数
  • Tessellation Factor
    • 决定将一条边分成几部分
    • equal_Spacing
    • fractional_even_spacing
    • fractional_odd_spacing

image-20220818002411909

  • Inner Tessellation Factor
    • (与上面的参数是同一等级)将边等分后,向内延伸相交,直至内部没有交点。

image-20220818002434236

1.4几何着色器

1.4.1几何着色器的输入与输出
  • 输入为图元(三角形、矩形、边)
    • 根据图元不同,shader中会出现对应不同数量的顶点
  • 输出同样为图元(一个或多个),需要自己从顶点构建,顺序很重要,同时定义最大输出的顶点数

二、曲面细分

  • 将一个Quad细分
  • 与置换贴图结合
    • 注意使用置换贴图不在fs中,也是在domain shader中的vert函数进行的,GPU无法获取mipmap信息,需要使用tex2Dlod等来读取图片。
    • 法线也需要重新计算
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
#pragma hull hullProgram
#pragma domain ds

#pragma vertex tessvert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Tessellation.cginc"

...
struct VertexInput;
struct VertexOutput{
float4 vertex: SV_POSITION;
float3 normal;
float3 tangent;
float2 uv;
};

VertexOutput vert(VertexInput v)
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.tangent = v.tangent;
}//顶点着色器的函数,但这里没有拿给顶点着色器,而是用来在DomainShader进行空间转换

//只有在能够使用曲面细分着色器的平台才能使用
#ifdef UNITY_CAN_COMPIE_TESSELLATION
struct TessVertex {
float4 vertex: INTERNALTESSPOS;
float3 normal: NORMAL;
float4 tangent: TANGENT;
float2 uv: TEXCOORD0;
};
struct OutputPatchConstant {//不同的图元,该结构会有不同
float edge[3]: SV_TESSFACTOR;
float inside: SV_INSIDETESSFACTOR;
};
TessVertex tessvert(VertexInput v){
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}//顶点着色器,注意这里没有进行空间转换,而是在domianshader里进行的空间转换

float _TessellationUniform;
OutputPatchConstant hsconst(InputPatch<TessVertex, 3> patch) {
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
}

//定义Hull shader的函数
[UNITY_domian("tri")]//指定图元
[UNITY_partitioning("fractional_odd")]//拆分edge的方式,equal_spacing等
[UNITY_outputtopology("triangl_cw")]
[UNITY_patchconstantfunc("hsconst")]//指定patch const function。一个patch一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)]//不同图元会对应不同控制点
TessVertex hullProgram(InputPatch<TessVertex, 3> patch, uint id : SV_OutputControlPointID){
return patch[id];
}

//定义Domian shader的函数
[UNITY_domain("tri")]//同样指定图元
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> patch, float3 bary : SV_DomainLocation) {
//bary重心坐标
//计算模型空间位置
VertexInput v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
VertexOutput o = vert(v);//调用原本该在顶点着色器中的转换函数。
return o;
}//这里才终于把顶点数据拿给下一阶段

#ENDIF

float4 frag...

三、几何着色器

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
#pragma vertex vert
#pragma fragment frag
#pragma geometry geo
...
struct VertexInput;
struct VertexOutput{
float4 vertex: SV_POSITION;
float3 normal;
float3 tangent;
};
//注意这次的顶点着色器是vert函数,上次是tessvert,同样不做任何操作
VertexOutput vert;
struct geometryOutput{
float4 pos: SV_POSITION;
float2 uv: TEXCOORD0;
}
geopetryOutput CreateGeoOutput(float3 pos, float2 uv){
geometryOutput o;
o.pos = UnityObjectToClipPos;
o.uv = uv;
return o;
}//在几何着色器中完成空间转换,如果同时使用曲面细分和几何着色器,也要放到几何着色器来进行


[maxvertexcount(3)]
void geo(triangle vertexOutput IN[3]: SV_POSITION, inout TriangleStream<geometryOutput> triStream){
float pos = IN[0].vertex;
float vNormal = IN[0].normal;
float vTangent = IN[0].tangent;
float vBinormal = cross(vNormal,vTangent)*vTangent.w;

float height = ...
float width = ...

...
geometryOutput o;
triStream.Append(CreatGeoOutput(dosomething(pos),dosomething(uv));
triStream.Append(CreatGeoOutput(dosomething(pos),dosomething(uv));
triStream.Append(CreatGeoOutput(dosomething(pos),dosomething(uv));//添加了一个新的三角形图元
}

也可以添加更多图元,如:

image-20220818013037250

image-20220818013127627

三角形的构建是triStream自动完成的。(uv是自己计算的。)

作业

使用曲面细分、几何着色器做一些有意思的Demo

1661109050656

看了一圈,基本上大家都是参考那两个教程,就不多写什么了。(做到这个效果还挺想复刻一下Clannad的片头的,说不定挺适合)

这里主要完成了基本的曲面细分-几何着色器多pass渲染带阴影和光照的草地-风的扰动。

事实上,几何着色器构建的图元会覆盖原来的三角形图元,原来的表面其实消失了。这里可以再加一个pass渲染。

并且地面pass可以不做细分来节省开销

还可以进一步处理的部分:

  • 基于距离的曲面细分
  • 基于RT或物体位置的草地交互

继续往后做这两个方向感觉就需要考虑偏向may佬说的在项目中整体方案的思路了。

暂时就放一放

参考资料

[1] https://www.bilibili.com/video/BV1XX4y1A7Ns

【技术美术百人计划】图形 3.3 曲面细分与几何着色器 大规模草渲染

[2] https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

[3] https://roystan.net/articles/grass-shader.html

【笔记】【百人计划】图形3.2 混合模式及剔除

一、什么是混合模式

  • 混合
    • 就是两种颜色混在一起。具体就是把某一像素位置原来的颜色和将要画上去的颜色,通过某种方式混在一起,从而实现新的效果
  • 透过红色玻璃看绿色玻璃
    • 可以先绘制绿色玻璃,再绘制红色玻璃。在绘制红色的时候,利用混合功能,把将要绘制上去的红色和原来的颜色(绿色)进行混合,于是得到新的颜色
    • 也可以理解成,绿色首先对背景施加影响,无论红色玻璃存在与否,这个影响都是存在的,所以首先计算后面的绿色。然后再考虑前面的红色的影响。这也是我理解透明物体从后往前绘制的原理的一种方式。

最终颜色=Shader计算后的颜色值*源因子(SrcFactor)+累计颜色*目标因子(DstFactor)

二、混合模式的类型

PS中的混合模式

image-20220817000953004

ShaderLab内的混合

  1. 如果颜色的某一分量超过1.0,则会被自动截取位1.0,不需要考虑越界的问题。

  2. 再所有着色器执行完毕,所有纹理都被应用,所有像素准备被呈现到屏幕之后,使用Blend命令来操作这些像素混合。

  3. 语法

    语法
    Blend Off 关闭blend混合(默认)
    Blend SrcFactor DstFactor 配置并启动混合计算
    Blend SrcFactor DstFactor, SrcFactorA DstFactorA 同上,但是使用不同的要素来混合Alpha通道
    BlendOp Value 如果使用BlendOp命令,则混合操作将设置为该值。否则,混合操作默认为Add。
    BlendOp OpColor, OpAlpha 同上,但是使用不同的操作来处理alpha通道
    AlphaToMaskOn 常用于开启MSAA的地表植被的渲染

Blend和BlendOp

1
2
3
4
5
6
7
finalValue = sourceFactor * sourceValue operation destinationFactor * destinationValue
//finalValue: GPU写入目标缓冲区的值
//sourceFactor: Blend命令中定义
//sourceValue: 片元着色器输出的值
//operation: 混合操作
//destinationFactor: Blend命令中定义
//destinationValue: 目标缓冲区现有值的值
  • 可以写在Pass中或SubShader中(BlendOp在同一个代码块中还必须有一个Blend命令)
  • 启用混合会禁用GPU上的一些优化(主要是隐藏表面去除Early-Z)

三、混合模式的实现方式

Unity附带的Blend枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//混合模式
[Enum(UnityEngine.Rendering.BlendOp)] _BlendOp ("BlendOp", float) = 0
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("SrcBlend", float) = 1
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend ("DstBlend", float) = 0

//深度开关
//ZWriteMode没有内置,只有两种状态,也可以用Toogle
[Enum(Off, 0, On, 1)] _ZWriteMode ("ZWriteMode", float) = 1


SubShader{
Tag {"RenderType" = "Transparent" "Queue" = "Transparent"}
ZWrite [_ZWriteMode]
Blend [_SrcBlend] [_DstBlend]
//这部分定义是CPU阶段的渲染设置,并不传入shader,不用定义uniform
}

Photoshop的Blend实现方式

自己查吧

  • 普通的Blend
    • Blend SrcAlpha OneMinusSrcAlpha
      • //Alpha混合 Alpha Blending
  • 变暗Darken
    • BlendOp Min
    • Blend One One
    • min(当前颜色,缓存颜色)*1
  • 正片叠底
    • Blend Discolors Zero
    • 当前颜色*缓存颜色+缓存颜色*0
  • 滤色Screen
    • Blend OneMinusDstColor One 或Blend One OneMinusSrcColor
    • Src*(1-Dst)+Dst*1 = Src+Dst-Src*Dst
    • 当前颜色*(1-缓存颜色)+缓存颜色*1 或 当前颜色*1 + 缓存颜色* (1-缓存颜色)
  • 变亮Lighten
    • BlendOp Max
    • Blend One One
    • max(当前颜色,缓存颜色)
  • 线性减淡LinearDodge
    • Blend One One
    • 缓存颜色*1+当前颜色*1
  • 颜色加深ColorBurn
    • 高级Opengl混合
    • 此模式目前仅在具有GL_KHR_blend_equation_advanced或GL_NV_blend_equation_advanced扩展支持的Opengl硬件上可用
    • 1-(1-Dst)/Src
  • 。。。

四、剔除的实现方式

  • 法线剔除
    • 也称背面消隐,根据法线朝向判断哪个面被剔除掉,可以用来控制是否双面渲染
    • Cull + [Off, Front, Back]
  • 面裁切
    • Clip函数会参数小于某像素点直接在片元阶段丢弃,常用于制作溶解,裁剪等效果
    • Clip();//默认会切掉0.5的部分,或者使用if
1
2
3
if(input.posInObjectCoords.y > 0.5) discard;
//剔除模式
[Enum(UnityEngine.Rendering.CullMode)] _CullMode ("CullMode", float) = 2

总结

  • 开启双面渲染相当于绘制了两次
  • Clip函数在某些PowerVR的机型上效率低
  • 面裁切Clip使用AlphaTest队列

作业

实现常用的混合模式,并设计使用界面

参考资料

[1] https://www.bilibili.com/video/BV1sL4y1v7SS

【笔记】Cherno C++ Tutorial note 04

Unions

Virtual Destructors

Casting

Conditional and Action Breakpoints

Safety in modern C++ and how to teach it

Precompiled Headers in C++

在大型项目中预编译头(PCH)可能更常用。

获取头文件并转换到一种编译后的格式,这一编译器就可以直接使用,而不需要每次编译都读取

如STL,每次#include<vector>都需要读取整个头文件,vector的头文件也需要读取。

这样编译时间就会很长。并且每次都要重复编译。

因此预编译头就把一大堆头文件编译一次,处理成二进制格式,这样编译器就可以很快处理。

Not to do

预编译头重新构建也需要花时间,因此不要把经常改动的文件放进去,

因此预编译头常用于外部库。如STL,glfw

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
#include "pch.h"

int main(){
LOG("Hello World")
}

//pch.h
#pragma once
#include <iostream>
#include <algorithm>
#include <funcitonal>
#include <memory>
#include <thread>
#include <utility>

//Data Stuctures
#include <string>
#include <stack>
#include <deque>
#include <array>
#include <vector>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>

//Windows API
#include <Windows.h>

那么如果不用预编译头的话,每次编译的中间文件都会把这一大堆头文件里的代码复制进去。(实测有接近4w行,就为了一个hello world)

所以只需要在做一个cpp文件

1
2
//pch.cpp
#include "pch.h"

在这个文件的C++属性中开启创建预编译头属性

image-20230409164925404

并在项目设置中同样位置设置成使用预编译头,并选择对应的头文件。

image-20230409165118838

那么就会在对应位置生成一个预编译头的输出文件

Cherno也演示了用g++如何使用预编译头。直接编译pch.h,会出现一个gch文件,这也就是预编译头的输出文件了。

【笔记】【百人计划】图形3.1 深度与模板测试

图形3.1 深度与模板测试

一、模板测试Stencil Test

image-20220816182543699

1.1 渲染管线中的逐片元操作

  • 逐片元操作(可配置,不可编程)
    • 像素权限测试Pixel Ownership Test(屏幕窗口使用权限,如scene和game窗口区分)
    • 裁剪测试Scissor Test(可以对渲染部分区域进行控制)
    • 透明度测试Alpha Test(片元透明度大于阈值,则通过测试,否则剔除——只能实现不透明和全透明)
    • 模板测试Stencil Test
    • 深度测试Depth Test
    • 混合Blending
    • Dithering
    • Logic Op

1.2 模板测试理解

  • 通过一定条件来判断是对该片元抛弃还是保留
  • 模板缓冲区
    • 和颜色缓冲区、深度缓冲区类似
    • 为屏幕上每个像素点保存一个uint8
    • 可以用这个值与预先设定的参考值比较,根据结果来决定是否更新对应像素的颜色。
    • 这个比较的过程被称为模板测试
  • 发生在透明度测试后,深度测试前
  • 通过测试,则更新像素点,否则不更新语法表示
1
2
3
4
5
6
7
8
9
stencil{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation//模板测试通过了,但是深度测试没通过
}
1
2
3
4
if(referenceValue&readMask comparisonFunction stencilBufferValue&readMask)
pass
else
discard
1.2.1 比较函数Comparison Function
Greater GEqual Less LEqual Equal NotEqual Always Never
$>$ $\geq$ $<$ $\leq$ $=$ $\neq$ 总是通过 总是失败
1.2.2 模板(更新)操作stencilOperation
属性 含义
Keep 保留当前缓冲内容,即stencilBufferValue不变
Zero 将0写入缓冲,即stencilBufferValue变为0
Replace 将参考值写入缓冲,即将stencilBufferValue赋值为referenceValue
IncrSat stencilBufferValue加1,如果超过255,则保留为255
DecrSat stencilBufferValue减1,如果小于0,则保留为0
Invert 将stencilBufferValue按位取反
IncrWarp stencilBufferValue加1,如果超过255,则变成0(然后继续自增)
DecrWarp stencilBufferValue减1,如果小于0,则变成255(然后继续自减)

1.3 总结

  • 使用模板缓冲区最重要的两个值:当前模板缓冲值stencilBufferValue和模板参考值referenceValue

  • 模板测试主要就是对这两个值使用特定的比较操作

  • 模板测试之后要对模板缓冲区的值进行更新操作,Keep,Zero,Replace等

  • 模板测试之后可以根据结果对模板缓冲区做不同的更新操作,比如测试成功操作pass,测试失败操作fail,深度测试失败Zfail,还有正对正面和背面精确更新操作PassBack,PassFront,FailBack等

  • 应用

    • 描边
      • image-20220816205437129
    • 多边形填充
      • image-20220816205429461
    • 反射区域控制
      • image-20220816205345560
    • Shadow Volume

二、深度测试Depth Test

  • 一些特性

image-20220816205656742

在这里面值得注意的是最后一张,greater的部分,绿色的部分消失了。这是因为蓝色外面的部分还没有渲染,是无穷远,greater的比较无法通过,就不被渲染。像这种涉及无穷远比较的,比如天空盒,要多注意深度测试的比较方式。

2.1 渲染管线中的深度测试

Early-Z

现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。

片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。如果一个片段着色器对它的深度值进行了写入,提前深度测试是不可能的。OpenGL不能提前知道深度值。

——LearnOpengl

  • (Early-Z)

  • 片元着色器

  • 逐片元操作(可配置,不可编程)

    • 像素权限测试Pixel Ownership Test(屏幕窗口使用权限,如scene和game窗口区分)
    • 裁剪测试Scissor Test(可以对渲染部分区域进行控制)
    • 透明度测试Alpha Test(片元透明度大于阈值,则通过测试,否则剔除——只能实现不透明和全透明)
    • 模板测试Stencil Test
    • 深度测试Depth Test
    • 混合Blending
    • Dithering
    • Logic Op

2.2 深度测试理解

深度测试就是当前对象在屏幕(Frame Buffer)对应的像素点,将对象自身的深度值与该当前该像素点缓存的深度值进行比较,如果通过,本对象在该像素点才会将颜色写入颜色缓冲,否则不会写入。

1
2
3
4
5
//深度缓冲区
if(ZWrite On && (currentDepthValue ComparisonFunction DepthBufferValue))
写入深度
else
忽略深度
1
2
3
4
5
//颜色缓冲区
if(currentDepthValue Comparison Function DepthBufferValue)
写入颜色缓冲
else
不写入颜色缓冲
  • 从发展角度理解
    • 控制渲染顺序
      • 画家算法
      • Z-Buffer算法
    • 控制Z-Buffer对深度的储存
      • Z-Test
      • Z-Write
    • 控制不同类型物体渲染顺序
      • 透明物体
      • 不透明物体
      • 渲染队列
    • 减少overdraw
      • Early-Z
      • Z-Cull
      • Z-check
2.2.1 深度缓冲区Z-Buffer

深度缓冲就像颜色缓冲,在每个片段中储存了信息,并且和颜色缓冲一样拥有宽度和高度,深度缓冲是由窗口系统自动创建的,他会以16、24、32位float的形式储存深度值。在大部分系统中深度缓冲精度都是24位。(Z-Buffer中储存的是当前深度信息,对于每个像素储存一个深度值)

通过Z-Write和Z-Test来调用Z-Buffer,实现想要的渲染结果

2.2.2 深度写入Z-Write

深度写入包括两种状态:Z-Write On 与Z-Write Off

当我们开启深度写入时,物体被渲染时,对应每个像素的深度都写入到深度缓冲区。反之,不会写入。但是物体写入深度,除了需要深度写入开启,还需要通过深度测试。

Z-Test分为通过和不通过两种情况,Z-Write分为开启和关闭两种情况,一共就是四种情况

深度测试 深度写入 深度缓冲区 颜色缓冲区
通过 开启 写入 写入
通过 关闭 不写入 写入
失败 开启 不写入 不写入
失败 关闭 不写入 不写入

这里顺便附上之前用Opengl做天空盒时对深度测试的理解。(现在看来glDepthMask就是开启深度写入,glDepthFunc就是比较函数,只有开启深度测试,深度写入才有用)可以根据上面的表格来更深入地理解下面的描述。

此外,还额外进行了天空盒的绘制,天空盒其实也就是先绘制一个立方体,然后将其z坐标取为w,使得在坐标归一化除以w时,z=1位于无限远的位置,这样能够使得天空的面永远在无限远处。

期间还需要对深度缓存进行一些操作。以下是我的理解。

//在不进行天空盒z=w操作的基础上,我们先渲染一遍天空盒,并且在此期间关闭深度遮罩不写入深度缓存,渲染完毕后开启深度遮罩,渲染其他物体,此时,其他物体覆盖在天空盒上,正常渲染

//(是否写入深度缓存结果上也等同于是否进行深度测试,即可以关闭深度遮罩,也可以关闭深度测试)

//如果不关闭深度遮罩,天空盒深度将写入深度缓存,如果物体在天空盒后面,则被遮挡。

//理解测试:那么在不进行z=w的时候,先绘制天空盒,不关闭深度遮罩,但是在绘制完天空盒之后,清除深度缓存,能正常绘制其他物体。测试正确。

//现在进行z=w的操作,并且先渲染天空盒,采用关闭深度测试的方式,即便z=1.0,也能正常渲染(未写入深度缓冲)。但是如果把天空盒放在最后渲染,天空盒将覆盖所有物体。

//如果是采用关闭深度遮罩的方式呢?只是不写入深度缓存,但实际上还是会进行深度测试,z=1.0无法渲染天空盒,因为原本就是1,但如果用LEQUAL,则可以渲染天空盒(默认为LESS)

//实际上,天空盒深度值已经是1.0的情况,开启深度遮罩没有任何意义(开启深度遮罩只是为了避免天空盒深度小于其他物体的情况,1.0已经最大),渲染天空盒和物体的顺序也不会有影响

//但是,后渲染天空盒,保留深度测试,可以利用深度测试提升性能

2.2.3 比较函数Comparison Function
Greater GEqual Less LEqual Equal NotEqual Always Never
$>$ $\geq$ $<$ $\leq$ $=$ $\neq$ 总是通过 总是失败
2.2.4 默认状态

Z-Write On,Z-Test Lequal,深度缓存一开始为无穷大。(这些和Opengl是不一样的)

2.3 渲染队列

2.3.1 Unity中的渲染队列

Unity中设置队列

image-20220816220946497

1
Tags {"Queue" = "Transparent"}//默认是Geometry
渲染队列
Background(1000) 最早被渲染的物体的队列
Geometry(2000) 不透明物体的渲染队列。大多数物体都应该是用该队列进行渲染(默认渲染队列)
AlphaTest(2450) 有透明通道,需要进行AlphaTest的物体的队列,比在Geometry中更有效
Transparent(3000) 半透明物体。一般是不写入深度的物体,Alpha Blend等的在该队列渲染
Overlay(4000) 最后被渲染的物体。一般是覆盖效果,如镜头光晕,屏幕贴片。
  • 渲染队列的物体排序:根据深度排序,深度小的在最前,深度大的在最后
  • 不透明物体的渲染顺序:从前往后
  • 透明物体的渲染顺序:从后往前(OverDraw)
2.3.2 Tips

多pass shader中,unity会选择所有pass里队列最靠前的作为物体的队列,然后根据pass的编写顺序逐pass执行

2.4 Early-Z

传统的渲染管线中,Z-Test是在Blending阶段,这时的深度测试,所有对象都计算过了片元着色器。大量的计算是无用的。

现代GPU运用了Early-Z技术,在Vs和Fs之间,进行一次深度测试。如果深度测试失败,就不必进行片元着色器的计算。但是最终的Z-Test仍需进行,保证最终遮挡结果正确。前面一次主要是Z-Cull为了裁剪达到优化目的。后一次是Z-Check,为了检查。

image-20220816221825445

2.5 深度值

image-20220816222256273

这个非线性深度也能够回答GAMES101中闫老师在投影矩阵那部分,讲frustum中间的点在投影矩阵之后是变远了还是变近了的问题。

要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。花时间想想这个:我们真的需要对1000单位远的深度值和只有1单位远的充满细节的物体使用相同的精度吗?线性方程并不会考虑这一点。

由于非线性方程与 1/z 成正比,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,这就是一个float提供给我们的一半精度了,这在z值很小的情况下提供了非常大的精度。在50.0和100.0之间的z值将会只占2%的float精度,这正是我们所需要的。这样的一个考虑了远近距离的方程是这样的:
$$
F_{depth}=\frac{1/z-1/near}{1/far-1/near}
$$

img

2.5.1 深度冲突Z-fighting

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺(Fight)谁该处于顶端。

所以实际上z-fighting的问题还是精度的问题。

一般解决方法就是稍微移动一点,不要重合。Learnopengl里还基于深度储存的精度剔除,可以把近平面设置远一点。或者是用更大的精度来储存。(实际上永远不会有足够的精度来保证完全重合)

2.6 总结

  • 是用深度缓冲区最重要的两个值:当前深度缓冲currentDepthValue和深度缓冲值zbufferValue,并通过比较操作获取理想渲染结果
  • Unity中的渲染顺序:先渲染不透明物体(从前往后),再渲染透明物体(从后往前)
  • 通过深度写入和深度测试组合控制半透明物体的渲染
  • 引入Early-z技术后的深度测试渲染流程
  • 深度缓冲区储存0-1的浮点值(非线性深度)
  • 应用
    • 基于深度的着色(湖水)
      • (摄像机到地面距离和湖水深度作比较,以差值比例控制着色)
    • ShadowMap
    • 透明物体、粒子渲染
    • 透视X-ray效果
    • 切边效果

作业

根据课程内容,使用深度测试与模板测试做一些有意思的效果

模板测试-描边

正好撞见Unity的描边,我们就来试试模板测试的描边

image-20220819163419228

我们来拆解一下这个效果

  • 2pass
    • 一个pass绘制物体
      • 写入一个模板值
    • 一个pass绘制物体法线外扩
      • 模板比较

现在问题就在于,如何进行法线外扩。

https://www.laowangomg.com/?p=712

这篇文章总结的非常详细。

其实最重要的问题是,我们在什么时候进行法线外扩?从模型空间到齐次裁剪空间过程中,在哪一步?

问题在于,我们应该让法线如何外扩?对于模型上的所有顶点,沿法线方向移动距离s,这是模型空间、世界空间、相机空间的做法。这样做的问题在于,我们可以把这个距离看作都是世界空间的距离,在经过投影变换后,不同位置的尺度会发生变化,也就是说,导致描边不均匀。

上面的博客给出的办法是——在齐次裁剪空间进行法线外扩,并且我们可以忽略z轴上的变化。只考虑屏幕的xy方向。

但是在我的实践中发现了一个问题,就是其实描边的宽度还是会出现不一致。并且这和渲染窗口分辨率有关。

image-20220819220901399

于是我们就知道是怎么回事了:

齐次裁剪空间时宽度一致的外扩尺度,经过视口变换后不再一致,但是我们肯定没有办法在视口变换的阶段处理顶点的位置,因此我们仍然需要进行改变。

我们要解决的核心问题是,让它在屏幕上能保持尺度的一致,其实,就是==观察空间==保持一致。

之前说在MVP空间的外扩都会导致投影后尺度变化。事实的确如此,在观察空间也会出现这个问题。

那么,在观察空间如何解决这一问题呢?我们可以借用裁剪空间的方法,只考虑屏幕上xy的变化,而忽略z的尺度变化。

1
2
o.pos.xy += normalize(vNormal.xy) * _OutLine;
//o.pos.xyz += normalize(vNormal.xyz) * _OutLine;

我们可以看出使用XYZ和xy的细微差别,这也很好理解。

1660921557672

这里我还做了一项处理就是让描边的粗细不受距离的影响,或者说,距离增加,描边的粗细也应该增加。不然就会像这样

image-20220819222542483

这应该如何处理呢?

image-20220819232155564

我们的h1/z1是由参数控制的,不妨令z1为1,则h1就等于

1
normalize(o.pos.xy)*_OutLine

那么要令距离z2的点在投影上和h1具有相同的高度,h2就很好求了。

这里还要注意,将z值取绝对值,我们只需要正值的计算。这样再调一调粗细的参数,效果和unity的描边效果就是基本一致的。

但是这种做法的局限性在于顶点法线,如果比较尖锐的顶点不是插值的法线的话,就会出现面片的分离。这就需要对模型法线进行处理,涉及到相应的工具制作。这也就是之后下一步工作的伏笔了。

1660923128047

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
Pass{

Stencil{
Ref 0
Comp Equal
}
Cull front
CGPROGRAM

float _OutLine;
float _Attenuation;

v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MV,v.vertex);
float3 vNormal = mul((float3x3) UNITY_MATRIX_V, UnityObjectToWorldNormal(v.normal));
o.pos.xy += normalize(vNormal.xy) * _OutLine * abs(o.pos.z);
o.pos = mul(UNITY_MATRIX_P,o.pos);
return o;
};
fixed4 frag(v2f i) : SV_Target {
fixed4 color =fixed4(1.0,0.0,0.0,1.0);
return fixed4(color);
};
#pragma vertex vert
#pragma fragment frag

ENDCG
}

我们为了防止描边被遮挡,也可做如下处理

1
2
3
4
5
6
7
8
9
10
11
12
13
Stencil{
Ref 1
Comp Always
Pass replace
ZFail replace
}//物体pass

Stencil{
Ref 0
Comp Equal
}
Cull front
ZTest always//描边pass

深度测试-遮挡透视

首先当然是做一个遮挡扫描效果。

我们来拆解下这个效果的组成

  • 遮挡物
    • 正常的
  • 被遮挡物
    • 多pass
      • 以正常部分的队列为物体渲染队列从前往后
    • 正常部分
      • Geometry
      • 开启深度测试LESS,开启深度写入
    • 遮挡部分
      • Transparent
      • 开启深度测试Greater,不写入深度

我们首先来写一下遮挡部分

在透明的处理上,顺便看了下3.2部分的混合,用正常的透明混合方法Blend SrcAlpha OneMinusSrcAlpha

当然也可以选择其他的(感觉差不太多)

image-20220817153223447
大问题

出现了一个大问题。为遮挡物赋予一些材质时,会导致渲染顺序出错。

正常来说,遮挡物和被遮挡物都会处于Geometry的渲染队列,并且遮挡物先渲染,再渲透明Pass,最后是非透明Pass这是非常合理的.

image-20220817211331779

赋予某些材质后。

有些时候是正常的,有些时候,透明pass会在遮挡物之前渲染,导致透明物体被遮挡。

image-20220817211349707

image-20220817211648337

这还不是最麻烦的。在这个状态下,如果后退摄像机一点,这里两个透明物体中的其中一个被渲染出来了。

image-20220817211717544

如果再后退一点——透明物体都正常绘制了。

image-20220817211752817

**但是,这依然不是最麻烦的。**如果这个时候,再把摄像机后退一些会发生什么呢?

image-20220817211856345

这就是我所感觉到的绝望。

解决方法我想到了很多。

  1. 被遮挡物体物体两个Pass都放在了Geometry渲染。如果把两个Pass放在Geometry之后的其他Pass渲染,就没问题了。但要注意修改Tags的pass时,最好是subshader的pass,这样会比较稳定,如果只改两个Pass的tags,是不稳定的。
  2. 和上面的方法基本思想一样,做法刚好相反。就是一定要保证遮挡物在被遮挡物两个pass之前渲染,把遮挡物的队列设成Geometry-1(或者更前面)就好了。
  3. 换一个遮挡物的shader和材质。这个是比较麻烦的。因为,即便是一个原本能正常绘制的shader,在我完全复制所有代码以后,重新生成的材质,依然出现了上述问题。这就比较玄学了,这也是最头疼的部分。。。
  4. https://zhuanlan.zhihu.com/p/468122471用这篇文章的处理方法,把两个pass分开到两个shader,用UsePass的方法,这样各自的subshader可以设置各自的Tag。结果也是正常的。

但是,虽然这些方法能解决问题,我还是不知道为什么会出现这样的问题,以及,其实没有解决问题的根本——为什么在这些shader中渲染顺序会出错,不应该都是按深度排序的吗?

首先会考虑是否是z精度的问题?但是又怎么会随距离周期变化呢?

发现其实DepthPass顺序就错了,也就是说直接原因还是深度。但是为什么某些材质就没有这个问题了呢?并且SubShader和Pass的Tag也有影响。。。

image-20220817213753109

又找到了一个绝妙的解决方法

我直接重新做一个shader,然后用两次UsePass。。。当然,缺少原来CGINCLUDE的参数和属性,这一部分直接原样复制过去就行了。

果然能解决问题。

。。。但是令人崩溃的又来了。。。我继续把两个UsePass分别替换回原来的两个Pass的代码。。。竟然也是对的。。。问题是这样和原来的代码有啥区别。。。shader的名字的区别。。。为什么。。。已经麻了,就当是bug吧。

感觉跟unity的资源读取和shader编译的过程有一定关系。

https://www.cnblogs.com/zhlabcd/p/11767018.html这篇文章出现了一样的现象,但是看起来原因不太一样,不过可能有一定共性。

背后的真正原因,只能有缘再解决了

参考资料

[1] https://www.bilibili.com/video/BV1Tb4y1C7Qa

【技术美术百人计划】图形 3.1 深度与模板测试 传送门效果示例

[2] https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/01%20Depth%20testing/

[3] https://zhuanlan.zhihu.com/p/427742656

[4] https://www.laowangomg.com/?p=712

[5] https://zhuanlan.zhihu.com/p/468122471

【笔记】【Maya工具】Python For Maya(OpenMaya)(四)Locator & CallBack for IK

一、Creating Custom Locator

maya提供标准locator定位器,我们也可以自定义定位器

  1. Draw/Define a shape(Opengl-function->MGLFunctionTable)
  2. Define a Bounding Box?
    1. isBounded()
  3. Define size of Bounding Box(Size of the locator)
    1. boundingBox()

1.1 Locators

  • MPxLocatorNode

    • draw()
    • isBounded()
    • boundingBox()
  • MGLFunctionTable

    • 为OpenGL的api提供了包装器
    • 我们不用自己进行实例化,只需要获取maya中的一个指针
    • MHardwareRenderer() <<OpenMaya Render\
    • ptr = OpenMayaRender.MHardwareRenderer.theRenderer()
    • ptr link to HardwareRenderer
      • glFunctionTable()->Opengl functions

1.2 实现结果

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
#locators.py
import sys
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMayaRender as OpenMayaRender


nodeName = "LeftFoot"
nodeId = OpenMaya.MTypeId(0x100fff)

# ptr to gl function table
glRenderer = OpenMayaRender.MHardwareRenderer.theRenderer()
glFT = glRenderer.glFunctionTable()



class LocatorNode(OpenMayaMPx.MPxLocatorNode):

def __init__(self):
OpenMayaMPx.MPxLocatorNode.__init__(self)
def compute(self, plug, dataBlock):
return OpenMaya.kUnknownParameter
def draw(self, view, path, style, status):# drawing View,dagpath,(wireframe...)
view.beginGL()#这里的opengl依然是状态机

# Pushed current state
glFT.glPushAttrib(OpenMayaRender.MGL_CURRENT_BIT)

# Enable blend mode(to enable transparency)
glFT.glEnable(OpenMayaRender.MGL_BLEND)

# Defined Blend function
glFT.glBlendFunc(OpenMayaRender.MGL_SRC_ALPHA, OpenMayaRender.MGL_ONE_MINUS_SRC_ALPHA)

# Define colors for different selection modes
if status == view.kActive:
glFT.glColor4f(0.2, 0.5, 0.1, 0.3)
elif status == view.kLead:
glFT.glColor4f(0.5, 0.2, 0.1, 0.3)
else:# status == view.kDormant:
glFT.glColor4f(0.1, 0.1, 0.1, 0.3)

# Draw a shape
glFT.glBegin(OpenMayaRender.MGL_POLYGON)
glFT.glVertex3f(-0.031, 0.0, -2.875)
glFT.glVertex3f(-0.939, 0.1, -2.370)
glFT.glVertex3f(-1.175, 0.2, -1.731)
glFT.glVertex3f(-0.60, 0.3, 1.060)
glFT.glVertex3f(0.473, 0.3, 1.026)
glFT.glVertex3f(0.977, 0.2,-1.731)
glFT.glVertex3f(0.809, 0.1, -2.337)
glFT.glVertex3f(0.035, 0.0, -2.807)
glFT.glEnd()

glFT.glBegin(OpenMayaRender.MGL_POLYGON)
glFT.glVertex3f(-0.587, 0.3, 1.33)
glFT.glVertex3f(0.442, 0.3, 1.33)
glFT.glVertex3f(0.442, 0.3, 1.92)
glFT.glVertex3f(0.230, 0.3, 2.24)
glFT.glVertex3f(-0.442, 0.3, 2.25)
glFT.glVertex3f(-0.635, 0.3, 1.92)
glFT.glVertex3f(-0.567, 0.3, 1.35)

glFT.glEnd()

if status == view.kActive:
glFT.glColor4f(0.2, 0.5, 0.1, 1)
elif status == view.kLead:
glFT.glColor4f(0.5, 0.2, 0.1, 1)
else:# status == view.kDormant:
glFT.glColor4f(0.1, 0.1, 0.1, 1)
view.drawText("Left Foot", OpenMaya.MPoint(0,0,0), view.kLeft)
# Disable Blend mode
glFT.glDisable(OpenMayaRender.MGL_BLEND)

# Restore the state
glFT.glPopAttrib()

view.endGL()



def nodeCreator():
return OpenMayaMPx.asMPxPtr(LocatorNode())

def nodeInitializer():
pass


# 初始化,和maya pluginCommand是一样的
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerNode(nodeName, nodeId, nodeCreator, nodeInitializer, OpenMayaMPx.MPxNode.kLocatorNode)
except:
sys.stderr.write("Failed to register node:" + nodeName)

def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterNode(nodeId)
except:
sys.stderr.write("Failed to deregister node" + nodeName)



1
2
3
import maya.cmds as cmds
cmds.loadPlugin("D:\\Autodesk\\plugin\\locators.py")
cmds.createNode("LeftFoot")

有组件,但是无显示,查阅api后发现,其实这个有给出例程,但是也是分了api1.0和api2.0版本的,教程写的大概是api1.0版本,1.0在maya2023、python3.9.3版本上大概不能使用,因此只能后面参考api2.0版的例程了。

而后面的IK部分也是完全无法使用了,需要学习新版本的api。只能笔记记录下思路

二、Maya Callbacks

回调是一种链接方式,链接特定事件的特定函数,并时刻关注这个函数的发生

回调允许我们针对特定的事件注册该函数(和java中的listener、Oracle中的Trigger相似)

OpenMaya中基于事件类别,对回调进行了分类

基类【MMessage】,其他回调的类都是这个基类的派生类callback(event, function)

2.1 Message消息

  • Message[eg MAnim Message]

  • Message[eg MCommand Message]

2.2 Callback回调

  • Removable

  • Id(callback create)

  • MAnim Message

    • Animation Message Callback
      • animation curve editing
      • keyframe editing
      • baked result change
  • MCommand Message

    • Command Message Callback
      • MEL command excution
  • MDG Message

    • for Dependency Graph Message
    • Node added/removed
    • Connection established/removed
  • MDAG Message

    • for dependency Graph Message
    • (主要跟踪或检查dag节点的切换)
    • Parent/child/Instance/added/removed
  • Event Message

    • “script Job”
    • OpenMaya.MEventMessage.getEventNames()
  • Lock Message

    • for lock message
    • plug locked/unlocked
      • values cannot be changed
    • Node locked/unlocked
      • cannot Re-parented/Renamed/deleted
  • Node Message

    • for Dependency Node Message
    • Attribute added/removed
    • plug of a Node is dirty
    • name of a Node is changed
  • Scene Message

    • Scene related Message
    • Before/After
      • New scene
      • Existing scene
      • Reference is loaded/unloaded
      • Maya is initialized/exist
  • Timer Message

  • UI Message

  • User-event message

三、Inverse-Forward Kinematics Solution

在这个插件中使用回调将IK和FK的功能结合起来

  • FK mode

    • 移动父节点,驱动子结点
  • IK mode

    • 移动子节点,在约束的条件下,自动解算父节点位置
  1. 在IK和FK中切换
    1. 选择polar vector/IK handle/子结点关节时,IK mode
    2. 选择任意一个父节点关节时,FK mode
  2. 施加约束

Custom-Node (IK FK)<-> Event Callback <-> Selection Change

Custom-Node (delete)<->DG Callback <-> addition/creation/deletion the DG nodes

Creating I-F K solution

Auto IK-FK Switching

  1. Find out which “Mode” we want to switch to
  2. Find information of all seven objects of 3-joint chain IK-system
  3. Perform operation based on the “Mode” we are at

image-20220705144129456

我这里暂时没有poleVectorControl

默认Mode =FK(joint1,joint2)

  • Mode = IK
    1. IK -Handle selected
    2. Joint3 selected
    3. Pole-vectorcontrol selected

image-20220705201445310

  • If Mode = FK

    1. set ikBlend = 0
    2. set visibility of Pole Vector Control = 0
  • If Mode = IK

    1. set ikBlend = 1
    2. snap Pole Vector Control to Joint2
  • set attribute of IK handle to non-Keyable

  • ikBlend

  • ikBlend- non-Keyable

  • Add translation after snapping Pole Vector Control

【笔记】【Maya工具】Python For Maya(OpenMaya)(二)Dependency Graph

Dependency Graph

  • Dirty Propagation

  • Push & Pull Mechanism

  • Lazy Evaluation

image-20220704094816836

1. Dirty Propagation

Dependency Graph Node

image-20220704095016592

由有关联的输入、输出和对应的计算函数组成

image-20220704095141685

输入节点传播到对应的输出节点(Dirty Propagation),直到叶节点

Updation Request(对节点属性产生影响的操作)

  • View Port
  • Attribute Editor
  • Channel Box
  • getAttr

反向传播

image-20220704095445679

Clean Propagation

(不会对没有输出要求的节点进行Clean)

image-20220704095707170

注意涂色的是Attribute/Plug,而不是Node本身

总结:

Dirty Propagation向一个方向传播直到叶节点,当出现Updation Request时,传播开始返回并检查哪些节点是Dirty的,最后到达Dirty的根节点时开始计算,设置Attribute/Plug为Clean

2. Push & Pull Mechanism

在Dirty Propagation中,Dirty的标记被push至一个方向直到最后(Pushed Dirty Message)。

image-20220704100117924

3. Lazy Evaluation

在Clean Propagation中会存在依然Dirty的节点。

Maya只更新或执行请求过的属性。

4. Writing Dependency Graph/ Custom Node

4.1 Dependency Graph Node

可以执行计算(DG-Node+Computation)

  • DG-Node
    • Attribute(任何输入/输出)
      • Specialize Handle(Plug
    • Data Block
      • Specialize Handle(Data Handle
      • 保存节点所有数据

4.2 建立一个DG-Node Graph

建立一个DG-Node Graph和建立一个Maya command是很相似的

  • command

    • class PluginCommand
    • cmdCreators
    • initializePlugin
    • uninitializePlugin
  • DG-Node

    • class WheelNode

      • _init_
      • doIt compute
    • Creator function

    • initialize function

      • 初始化节点所有属性

        • MFn-Attribute使用函数集来创建属性

        • create Attribute(Create + set Properties)

          • MFn->create attribute return mObj Handle

          • set Properties

            1. readable->source(输出节点只可读)

            2. writable->destination[setAttr]

            3. Storable->store with file(input)

            4. Keyable ->can be keyed or not

        • attach Attribute to the Node

        • Design circuitry

    • initializePlugin

    • uninitializePlugin

在这个案例中,我们会写一个插件,能够让一个小车(两个圆柱和一个平板组成),空间位置发生变化的时候,轮子也发生对应的旋转。
$$
rotate = \frac{translate}{2\pi r}*(-360)
$$
Node Design

image-20220704102307567

4.3 完整流程

image-20220704112229490

1
2
3
4
from maya import cmds
cmds.loadPlugin(r"C:\Users\XZYW\文档\maya\2023\zh_CN\scripts\WheelNode.py")

cmds.createNode("WheelNode")

来贴个官网上找到的案例

Maya 帮助 | Python API 2.0 Reference: python/api1/py1CircleNode.py | Autodesk

完整代码

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
# -*- coding: utf-8 -*-  
import sys
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx


nodeName = "WheelNode"
nodeId = OpenMaya.MTypeId(0x0008004c)# Id是唯一的,定义一个唯一的节点

class WheelNode(OpenMayaMPx.MPxNode):# 继承依赖关系图的类
inRadius = OpenMaya.MObject()# 用于储存创建属性时mFn函数集返回的mObj
inTranslate = OpenMaya.MObject()
outRotate = OpenMaya.MObject()

def __init__(self):
OpenMayaMPx.MPxNode.__init__(self)

def compute(self, plug, dataBlock):# 只要输入节点发生改变,maya就会调用compute函数,并且会
# 发送outputNode,以及输入数据的数据块
'''
rotate = translate / (2* 3.1416 * radius) * (-360)
'''
if plug == WheelNode.outRotate:# 这个案例这里没用,但是对于多输出的复杂节点是需要的
dataHandleRadius = dataBlock.inputValue(WheelNode.inRadius)# 这是一个返回的指针
dataHandleTranslate = dataBlock.inputValue(WheelNode.inTranslate)

inRadiusVal = dataHandleRadius.asFloat()
inTranslateVal = dataHandleTranslate.asFloat()
outRotateVal = float(inTranslateVal) / float(2 * 3.1416 * inRadiusVal) * (-360)

dataHandleRotate = dataBlock.outputValue(WheelNode.outRotate)

dataHandleRotate.setFloat(outRotateVal)

# 结束计算,设置clean
dataBlock.setClean(plug)

else:
return OpenMaya.kUnknownParameter

def nodeCreator():
return OpenMayaMPx.asMPxPtr(WheelNode())

def nodeInitializer():
# 1.创建一个Function set for numeric attribute
mFnAttr = OpenMaya.MFnNumericAttribute()

# 2.创建属性
# Input Radius
WheelNode.inRadius = mFnAttr.create("radius", "r",
OpenMaya.MFnNumericData.kFloat, 0.0)# longname, shortname,attrType,defaultValue

# set properties
mFnAttr.setReadable(1)# bool
mFnAttr.setWritable(1)
mFnAttr.setStorable(1)
mFnAttr.setKeyable(1)
# Input Translate
WheelNode.inTranslate = mFnAttr.create("translate", "t",
OpenMaya.MFnNumericData.kFloat, 0.0)
mFnAttr.setReadable(1)# bool
mFnAttr.setWritable(1)
mFnAttr.setStorable(1)
mFnAttr.setKeyable(1)
# Output Rotation
WheelNode.outRotate = mFnAttr.create("rotate", "rot",
OpenMaya.MFnNumericData.kFloat)#输出值是计算的,不用设默认值
mFnAttr.setReadable(1)
mFnAttr.setWritable(0)
mFnAttr.setStorable(0)
mFnAttr.setKeyable(0)

# 3.attach Attribute to the Node
WheelNode.addAttribute(WheelNode.inRadius)
WheelNode.addAttribute(WheelNode.inTranslate)
WheelNode.addAttribute(WheelNode.outRotate)

# 4.Design the circuitry如何让maya知道节点之间的关系
WheelNode.attributeAffects(WheelNode.inRadius, WheelNode.outRotate)# input,output
WheelNode.attributeAffects(WheelNode.inTranslate, WheelNode.outRotate)


# 初始化,和maya pluginCommand是一样的
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerNode(nodeName, nodeId, nodeCreator, nodeInitializer)
except:
sys.stderr.write("Failed to register node:" + nodeName)

def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterNode(nodeId)
except:
sys.stderr.write("Failed to deregister node" + nodeName)

【笔记】【Maya工具】Python For Maya(OpenMaya)(一)API基础与插件

Python Programming in maya

1
2
3
4
5
6
7
8
9
10
import maya.cmds as cmd
cmds.select(all=True)
cmds.delete()
for step in range(40):
cmds.polyCube(w=10,d=3,h=0.5)
cmds.move(3,step*0.5,0)
cmds.xform(ws = True,rotatePivot=(0,0,0))
cmds.rotate(0,step*10,0)


1. DAG——描述Maya中的功能节点:

DAG(Directed Acyclic Graph有向无环图)

  • Dag Node:Transform
  • Dag Node:Shape

DG:Dependency Graph依赖图

DAG path,某个对象的DAG path描述从根节点到物体的完整查找路径

MObject(Model Object)

  • maya.OpenMaya.MObject

MSelectionList(选择的模型对象列表)

  • maya.OpenMaya.MSelectionList
    • 创建一个选择列表
    • 在选择列表中添加、移除对象
    • 可遍历
    • etc
1
2
3
4
5
6
7
8
9
10
11
import maya.OpenMaya as OpenMaya
mSelectionList = OpenMaya.MSelectionList()
mSelectionList.add("<something_Name>")

mDagPath = OpenMaya.MDagPath()
mSelectionList.getDagPath(0,mDagPath)#返回0位置的path
print(mDagPath.fullPathName())

mObj = OpenMaya.MObject()
mSelectionList.getDependNode(0,mObj)
print(mObj.apiTypeStr())#kTransform

2. Traversing Hyper Graph in Maya Scene

MFn = Model Function Set

MFnMesh表示接收Mesh类型的函数集

  • create
  • modify
  • retrave
  • add Edge\face,subdivision

MFnDependencyNode:一组函数需要Dependency Node Type的数据,提供函数

  • create(dependency graph)
  • modify(dependency graph)
  • retrave(dependency graph)

可以通过MObj和MDagpath来提供到函数集的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import maya.OpenMaya as OpenMaya
# Create a Selection List
mSel = OpenMaya.MSelectionList()
mSel.add("pPlane1")
# Create MObject
mObj = OpenMaya.MObject()
mDagPath = OpenMaya.MDagPath()
# Request Dependency Node and DagPath of the Obj
mSel.getDependNode(0,mObj)
mSel.getDagPath(0,mDagPath)
# Create Mesh function set
mFnMesh = OpenMaya.MFnMesh(mDagPath)#可以用mObj和mDagpath两种
print(mFnMesh.fullPathName())#|pPlane1|pPlaneShape1
#说明mFnMesh是在ShapeNode下运作的

# Dependency function set
mFnDependNode = OpenMaya.MFnDependencyNode(mObj)#只能用mObj
print(mFnDependNode.name())

MPlug提供操作功能的类(操作Plug Type)

Plug:节点属性的接口

  • create
  • modify
  • access(通过plug访问属性)
  1. Network Plug: Dependency Node Plug

  2. Non-Network Plug: User Defined Plug

MPlugArray

image-20220627211430302
1
2
3
4
5
6
7
# Get all the connections of a Shape node
mPlugArray = OpenMaya.MPlugArray()
mFnMesh.getConnections(mPlugArray)

print(mPlugArray.length())#2
print(mPlugArray[0].name())#pPlaneShape1.instObjGroups[0]
print(mPlugArray[1].name())#pPlaneShape1.inMesh
image-20220627212637049 image-20220627212705325

image-20220627213320170

看看这个就知道是在干嘛了,我们的最终目的是操纵polyPlane1进行细分

通过pPlaneShape1的plug接口,就可以完成

1
2
3
4
5
6
mPlugArray2 = OpenMaya.MPlugArray()
mPlugArray[1].connectedTo(mPlugArray2,True,False)#后面这两个bool值不太明白

print(mPlugArray2.length())#1
#len()不能用在这个对象上
print(mPlugArray2[0].name())#polyPlane1.output

image-20220627214040508

完整代码

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
import maya.OpenMaya as OpenMaya
# Create a Selection List
mSel = OpenMaya.MSelectionList()
mSel.add("pPlane1")
# Create MObject
mObj = OpenMaya.MObject()
mDagPath = OpenMaya.MDagPath()
# Request Dependency Node and DagPath of the Obj
mSel.getDependNode(0,mObj)
mSel.getDagPath(0,mDagPath)
# Create Mesh function set
mFnMesh = OpenMaya.MFnMesh(mDagPath)#可以用mObj和mDagpath两种
print(mFnMesh.fullPathName())#|pPlane1|pPlaneShape1
#说明mFnMesh是在ShapeNode下运作的

# Dependency function set
mFnDependNode = OpenMaya.MFnDependencyNode(mObj)#只能用mObj
print(mFnDependNode.name())

# Get all the connections of a Shape node
mPlugArray = OpenMaya.MPlugArray()
mFnMesh.getConnections(mPlugArray)

print(mPlugArray.length())
print(mPlugArray[0].name())
print(mPlugArray[1].name())

mPlugArray2 = OpenMaya.MPlugArray()
mPlugArray[1].connectedTo(mPlugArray2,True,False)#后面这两个bool值不太明白

print(mPlugArray2.length())#len()不能用在这个对象上
print(mPlugArray2[0].name())

mObj2 = mPlugArray2[0].node()
mFnDependNode2 = OpenMaya.MFnDependencyNode(mObj2)
print(mFnDependNode2.name())

mPlug_width = mFnDependNode2.findPlug("width")
mPlug_height = mFnDependNode2.findPlug("height")
print(mPlug_width.asInt(),mPlug_height.asInt())

mPlug_subWidth = mFnDependNode2.findPlug("subdivisionsWidth")
mPlug_subHeight = mFnDependNode2.findPlug("subdivisionsHeight")

mPlug_subWidth.setInt(20)
mPlug_subHeight.setInt(20)


3. Command Communication Flow

image-20220627220119395
  • Command Plugin
    • 函数
    • 初始化/注册Initialization,Registration
    • Uninitialization/De-registration

把东西注入到Maya core,就是注册

  • 命令插件编写基本流程
    • 定义一个类
    • 创建一个实例
    • 用一个指针指向这个实例
    • Maya获取这个指针

4. Writing command plugin

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
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import sys

commandName = "pluginCommand"

class pluginCommand(OpenMayaMPx.MPxCommand):
def __init__(self):
OpenMayaMPx.MPxCommand.__init__(self)

def doIt(self,argList):
print("doIt")
#创建实例并添加指针
def cmdCreator():
return OpenMayaMPx.asMPxPtr(pluginCommand())

#初始化
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerCommand(commandName,cmdCreator)#mayaCore handle
except:
sys.stderr.write("Failed to register command:" + commandName)

def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterCommand(commandName)
except:
sys.stderr.write("Failed to deregister command" + commandName)

保存脚本后,在maya中加载插件

Mel,python,mayaUI三种方法

window-setting preferences-pluginManager

image-20220627222747095

image-20220627222850467

1
cmds.loadPlugin("./pluginCommand.py")

5. Maya API Iterators

Iterator是一个对象,对数据结构进行遍历

  • MItDag
    • Dag上的iterator
    • 深度优先
    • 广度优先
image-20220627230758404
  • MItDependencyGraph
image-20220627230836150
  • MItMeshEdge
image-20220627230927076
  • MItMeshVertex
image-20220627231006974
  • MItMeshPolygon(face)
image-20220627231033010
  • MItSurfaceCV(CVs of Nurbs)
image-20220627231104357
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
import maya.OpenMaya as OpenMaya

dagIterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst, OpenMaya.MFn.kInvalid)#arg1=树搜索算法,arg2 = Filtering
dagNodeFn = OpenMaya.MFnDagNode()

while (not dagIterator.isDone()):
currentObj = dagIterator.currentItem()#return dagNode
depth = dagIterator.depth()
dagNodeFn.setObject(currentObj)

name = dagNodeFn.name()
type = currentObj.apiTypeStr()
path = dagNodeFn.fullPathName()

printOut = ""
for i in range(0,depth):
printOut += "----->"
printOut += name + " : " + type
print(printOut)
dagIterator.next()

===========
World : kWorld
----->persp : kTransform
----->----->perspShape : kCamera
----->top : kTransform
----->----->topShape : kCamera
----->front : kTransform
----->----->frontShape : kCamera
----->side : kTransform
----->----->sideShape : kCamera
----->pCube1 : kTransform
----->----->pCubeShape1 : kMesh
----->----->pCylinder1 : kTransform
----->----->----->pCylinderShape1 : kMesh
----->----->----->pCone1 : kTransform
----->----->----->----->pConeShape1 : kMesh
----->----->----->----->pPlane1 : kTransform
----->----->----->----->----->pPlaneShape1 : kMesh
----->----->----->----->----->pPipe1 : kTransform
----->----->----->----->----->----->pPipeShape1 : kMesh

放到插件的doIt方法里面

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
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import sys

commandName = "printHierarchy"

class pluginCommand(OpenMayaMPx.MPxCommand):
def __init__(self):
OpenMayaMPx.MPxCommand.__init__(self)

def doIt(self,argList):
print("Scene Heirarchy")
dagIterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst, OpenMaya.MFn.kInvalid)#arg1=树搜索算法,arg2 = Filtering
dagNodeFn = OpenMaya.MFnDagNode()

while (not dagIterator.isDone()):
currentObj = dagIterator.currentItem()#return dagNode
depth = dagIterator.depth()
dagNodeFn.setObject(currentObj)

name = dagNodeFn.name()
type = currentObj.apiTypeStr()
path = dagNodeFn.fullPathName()

printOut = ""
for i in range(0,depth):
printOut += "----->"
printOut += name + " : " + type
print(printOut)
dagIterator.next()
#创建实例并添加指针
def cmdCreator():
return OpenMayaMPx.asMPxPtr(pluginCommand())

#初始化
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerCommand(commandName,cmdCreator)
except:
sys.stderr.write("Failed to register command:" + commandName)

def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterCommand(commandName)
except:
sys.stderr.write("Failed to deregister command" + commandName)

保存脚本然后

1
2
3
import maya.cmds as cmds
cmds.loadPlugin("c:\\Users\\XZYW\\Desktop\\printHierarchy.py")
cmds.printHierarchy()

插件就可用了,这就是编写插件的方式。

6. Command with Arguments

1
cmds.polySphere("mySphere",q = True,sx = True)

image-20220628205624179

  • MSyntax
    • flags
    • Args
    • Object
  • MArgDatabase
    • 是MArgParser的派生类
    • Parsing
    • Storing
    • Retreving
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
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import sys
import maya.OpenMayaFX as OpenMayaFX
#使用粒子系统

commandName = "vertexParticle"
kHelpFlag = "-h"
kHelpLongFlag = "-help"
kSparseFlag = "-s"
kSparseLongFlag = "-sparse"
helpMessage = "This command is used to attach a particle on each Vertex of a poly mesh"

class pluginCommand(OpenMayaMPx.MPxCommand):
sparse = None
def __init__(self):
OpenMayaMPx.MPxCommand.__init__(self)


def argumentParser(self,argList):
syntax = self.syntax()#MSyntax Object
parsedArguments = OpenMaya.MArgDatabase(syntax,argList)
if parsedArguments.isFlagSet(kSparseFlag):
self.sparse = parsedArguments.flagArgumentDouble(kSparseFlag,0)
return
if parsedArguments.isFlagSet(kSparseLongFlag):
self.sparse = parsedArguments.flagArgumentDouble(kSparseLongFlag,0)
return
if parsedArguments.isFlagSet(kHelpFlag):
self.setResult(helpMessage)
return
if parsedArguments.isFlagSet(kHelpLongFlag):
self.setResult(helpMessage)
return# OpenMaya.MStatus.kSuccess这个方法已经被取消了,其实不用返回任何东西
def isUndoable(self):
return True
def undoIt(self):
print("Undo")
mFnDagNode = OpenMaya.MFnDagNode(self.mObj_particle)

mDagMod = OpenMaya.MDagModifier()
mDagMod.deleteNode(mFnDagNode.parent(0))#粒子系统创建是带有Transform的

mDagMod.doIt()
return

def redoIt(self):
mSel = OpenMaya.MSelectionList()
mDagPath = OpenMaya.MDagPath()
mFnMesh = OpenMaya.MFnMesh()
OpenMaya.MGlobal.getActiveSelectionList(mSel)#处理所有主动选择的对象,并添加到提供的选择列表中
if mSel.length() >=1:
try:
mSel.getDagPath(0,mDagPath)
mFnMesh.setObject(mDagPath)
except:#如果选中的不是polymesh
print("Select a poly mesh")

return
else:#如果没选东西
print("Select a poly mesh")
return
mPointArray = OpenMaya.MPointArray()
mFnMesh.getPoints(mPointArray, OpenMaya.MSpace.kWorld)#提供所有顶点世界坐标

# create a particle system
mFnParticle = OpenMayaFX.MFnParticleSystem()
self.mObj_particle = mFnParticle.create()

# Fix Maya Bug
mFnParticle = OpenMayaFX.MFnParticleSystem(self.mObj_particle)

counter = 0
# attach particle to vertex
for i in range(mPointArray.length()):
if i%self.sparse == 0:
mFnParticle.emit(mPointArray[i])
counter += 1
print("Total Points:" + str(counter))
mFnParticle.saveInitialState()#如果应用动力学,确保粒子在原来位置上去应用
return

def doIt(self,argList):
print("doIt")
self.argumentParser(argList)
if self.sparse != None:
self.redoIt()
return

#创建实例并添加指针
def cmdCreator():
return OpenMayaMPx.asMPxPtr(pluginCommand())
#创建命令语法
def syntaxCreator():
# create MSyntax object
syntax = OpenMaya.MSyntax()
# collect/add the flags
syntax.addFlag(kHelpFlag,kHelpLongFlag)#short name,long name,data type
syntax.addFlag(kSparseFlag,kSparseLongFlag,OpenMaya.MSyntax.kDouble)
# return MSyntax
return syntax


#初始化
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerCommand(commandName,cmdCreator,syntaxCreator)#同时注册语法
except:
sys.stderr.write("Failed to register command:" + commandName)

def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterCommand(commandName)
except:
sys.stderr.write("Failed to deregister command" + commandName)


1
2
3
4
range 是生成一个列表
xrange用法与range完全相同,不同的是生成的不是一个list对象,而是一个生成器
在生成很大的数字序列时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间
xrange和range都在循环的时候使用。
  • Command Execution
  • Undo
  • Redo

每执行一个Command Execution,都会把对应的命令添加到Undo队列

如果Command Execution使用Undo命令,就会把Undo队列中最后面的移到Redo队列上

如果Command Execution使用Redo命令,就会把Redo队列中最后面的移到Undo队列上

image-20220628211342017

image-20220628213019559

好了,写完了这个脚本,看完这集教程,终于发现,它是过时的版本,作者用的还是maya2013,非常恐怖

【笔记】Cherno C++ Tutorial note 03

Templates模板

模板就像一个蓝图

1
2
3
4
5
6
7
8
9
10
11
void Print(int value) {
std::cout<<value<<std::endl;
}
void Print(std::string value) {
std::cout<<value<<std::endl;
}
int main() {
print(5)
print("Hellow")
print(5.0f)
}

这种不断重复重载的函数可以用一个函数包裹,

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>

void Print(T value) {
std::cout<<value<<std::endl;
}//这个函数是一个模板,只有当调用时,它才创建一个对应的函数
//它不参与编译,只有它创建的函数才参与编译
int main() {
print(5)
print<int>(5)
print("Hellow")
print(5.0f)
}

c++的标准模板库STL就是templates

1
2
3
4
5
6
7
8
9
10
11
template<typename T, int N>
class Array {
private:
T m_Array[N];
public:
int GetSize() const {return N;}
};

int main() {
Array<int,5> array;
}

Stack vs Heap Memory堆内存与栈内存

stack和heap地址都是RAM上的两部分区域

stack定好了区域大小 2MegaBytes

heap的大小是动态改变的

区别在于他们如何分配内存

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
struct Vector3 {
float x,y,z;
Vector3()
: x(10),y(5),z(1) {}
}

int main() {
//stack分配
int value = 5;
int array[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
Vector3 vector;
//观察内存可以发现,stack所有的分配,都在stack的指针位置进行,分配出多少,stack指针就往后移多少,并返回这个指针
//scope结束,栈内存销毁

//heap分配
int* hvalue = new int;
*hvalue = 5;
int* harray = new int[5];
harray[0] = 1;
harray[1] = 2;
harray[2] = 3;
harray[3] = 4;
harray[4] = 5;
Vector3* hvector = new Vector3();

delete havlue;
delete[] harray;
delete hvector;
//heap分配
//new其实就是调用malloc
//当malloc分配内存时,会根据所需要的内存大小,在内存上寻找足够的一块大小的位置分配,并提供这个地址的指针
}

使用堆分配可能造成Cache Miss缓存丢失,少数的缓存丢失没有影响

堆栈分配最大的区别就是在分配方式上。

堆分配会更慢。

只有无法在栈上分配,如需要更多的生命周期、更大的数据,再使用堆分配,一般没事最好栈分配,毕竟快很多。

(所以内存碎片大概就是指堆上各个内存之间存在的小间隙?)

Macros宏指令

预处理过程的宏

编译C++时

  • 首先是预处理pass
    • 带#符号的语句
    • 实际是文本编辑阶段,进行文本替换
    • 控制哪些程序语句进入编译器

并不是总是要用宏

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

#define WAIT std::cin.get()//只在当前文档中生效
#define LOG(x) std::cout<< x << std::endl
int main() {
LOG("Hello")
WAIT;// 根据define进行文本替换
}

你可以在项目设置的预处理命令看到一些定义

Debug

image-20220701143548474

Release

image-20220701143602730

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>

#define WAIT std::cin.get()

#ifdef _DEBUG//也可以在预处理命令上赋值,#ifdef _DEBUG == 1,
#define LOG(x) std::cout<< x << std::endl
#else//也有elif
#define LOG(x)
#endif

#define MAIN int main() \//反斜杠可以在宏命令中换行
{\
std::cin.get();\
}
//这样只有debug模式才会log
int main() {
LOG("Hello")
WAIT;
}

待补充:

auto关键字

Static Arrays(std::array)静态数组

Functions Pointers函数指针

Lambdas

using namespace std

Namespaces命名空间

Threads线程

Timing计时器

Multidimensional Arrays(2D arrays)多维数组

Sorting

Type Punning类型双关