VEX

创建新属性

@foo; //定义一个新的点浮点属性 'foo'
@foo=1; //初始化值为1

//向量
在“@”之前加上“v”。要初始化,如果只是数字,请使用大括号,或者如果您正在使用函数或其他属性执行某些作,请使用 set():
v@myvector={1,0,3};
v@other=set(0, @P.x, 1);

可以使用适当的前缀设置属性类型。默认值为 float,因此您很少需要 f@ 前缀。
// floats and integers
f@myfloat = 12.234; // float
i@myint = 5; // integer

// vectors
u@myvector2 = {0.6, 0.5}; // vector2 (2 floats)
v@myvector = {1,2,3}; // vector (3 floats)
p@myquat = {0,0,0,1}; // quaternion / vector4 / 4 floats

// matricies
2@mymatrix2 = {1,2,3,4}; // matrix2 (2x2 floats)
3@mymatrix3 = {1,2,3,4,5,6,7,8,9}; // matrix3 (3x3 floats)
4@mymatrix4 = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; // matrix (4x4 floats)

// strings and dictionaries
s@mystring = 'a string'; // string
d@mydict = {}; // dict, can only instantiate as empty type
d@mydict['key'] = 'value'; // can set values once instantiated


获取现有属性值

v@myvector=@P; // myvector 从点位置获取其值
//@ 符号用于获取属性和设置属性。您在几何电子表格中看到的所有属性,都可以通过在其名称前面粘贴 @ 来访问它们。

v@myvector=@N*1.5; // 1.5 倍 N

//访问矢量或颜色的单个属性,请使用 @attribute.channel。例如,仅获取每个点的 y 值:
@foo=@P.y;

//设置它类似。例如,将每个点的红色通道设置为 x 位置的两倍的正弦:
@Cd.r = sin( @P.x * 2 );

//Cd 或 P 等矢量属性允许您以多种方式引用组件,使用任何方便的方式。组件可以是 r/g/b、x/y/z、[0]/[1]/[2]。
// These all do the same thing, set the first component of the vector:
@Cd.r = 1;
@Cd.x = 1;
@Cd[0] = 1;

// As are these, all setting the third component:

@P.z = 6;
@P.b = 6;
@P[2] = 6;

隐式属性类型与显式属性类型

wrangle 隐式知道某些常见的属性类型(@P、@Cd、@N、@v、@orient、@id、@name 等),但如果你有自己的属性,Houdini 会假设它是一个浮点数,除非另有说明,这很容易犯错误。

为确保 Houdini 做正确的事情,请在前面添加类型。例如,您已将 @mycolour 设置为向量,并尝试在 wrangle 中使用它:

// BAD
@Cd = @mycolour; // This will treat it as a float and only read the first value (ie, just the red channel of @mycolour)

// GOOD
@Cd = v@mycolour; // Explicitly tells the wrangle that its a vector.

创建 UI 控件

@Cd.r = sin( @P.x *  5  );

//想将 5 更改为另一个数字。您可以不断更改不同的数字代码,或将 5 替换为 ch('scale')
@Cd.r = sin( @P.x *  ch('scale') );

// An int channel
i@myint = chi('myint');

// A vector channel
v@myvector = chv('awesome');

// A ramp channel
f@foo = chramp('myramp',@P.x);
//在一行中您可以读取一个值 (@P.x),通过 UI 渐变小部件重新映射它,并将其提供给@foo。它假设传入值在 0-1 范围内,您通常必须在馈送到斜坡之前拟合这些值。fit 函数 goes fit( value, oldmin, oldmax, newmin, newmax)。例如,在 -0.5 和 6 之间重新映射 X,然后使用斜坡将这些值输入红色通道:
float tmp = fit(@P.x, -0.5,6,0,1);
@Cd.r = chramp('myramp',tmp);
//使用 UI 滑块定义起点和终点
float min = ch('min');
float max = ch('max');
float tmp = fit(@P.x, min,max,0,1);
@Cd = 0;  // lazy way to reset the colour to black before the next step
@Cd.r = chramp('myramp',tmp);

ch() 告诉 Houdini 寻找一个通道,这就是 Houdini 所说的 UI 组件,通常是一个滑块。点击文本编辑器右侧的小插头图标,Houdini 扫描 vex 代码,意识到您引用了一个尚不存在的频道,并在 Wrangle UI 的底部创建一个名为“scale”的频道。开始滑动它,您会看到颜色更新。

您可以使用多种渠道类型。ch() 和 chf() 都创建一个浮点通道。chramp() 渐变映射 chi()整数 chv()向量,颜色也是用chv,但需要调整参数界面的类型,将默认矢量通道转换为彩色通道。创建通道后,您可以右键单击 Wrangle 节点并选择“编辑参数界面”(或使用参数面板中的齿轮菜单),然后更改浮点范围、默认值、标签等您想要的任何内容。

点击插入按钮,编辑参数界面,按住 Shift 键选择我制作的所有通道,并将类型字段更改为“颜色”,为了更加俏皮,将“将颜色显示为”更改为“hsv 滑块”。

常用功能

  • fit() – 取一个介于 2 个值之间的数字,将其拟合在 2 个其他值之间,通常为 0-1。例如,将@u范围从 -5,20 重新映射到 0-1:foo = fit(@u, -5, 20, 0, 1);
  • rand() – 生成一个介于 0 和 1 之间的随机数。通常给它提供点 id,所以每个点得到一个随机数:foo = rand(@ptnum);
  • sin(), cos() – 正如你所期望的,但以弧度为单位。
  • Radians() – 将数字从度转换为弧度: foo = 弧度(90);
  • length() – 测量向量的长度。例如,测量一个点与原点的距离:dist = length(@P);
  • distance() – 测量两点之间的距离: dist = distance(@P, v@mypoint);

内置属性

您可以使用一些内置变量,查看它们的最简单方法是放下一个点 vop,然后查看全局参数。以下是更常见的:

  • @ptnum – 点 ID
  • @numpt – 总积分
  • @Time – 当前时间,以秒为单位
  • @Frame – 当前帧
  • @primnum – 原始 ID
  • @numprim – 基元的总数

示例:按阈值随机删除点

if ( rand(@ptnum) > ch('threshold') ) {
   removepoint(0,@ptnum);
}

使用插头按钮创建阈值滑块,现在在 0 和 1 之间向上滑动,您会看到点被随机删除。这是怎么回事:

  • rand(@ptnum) — 每个点根据其 ID 获得一个介于 0 和 1 之间的随机数
  • > ch(’threshold’) — 将该随机数与阈值滑块进行比较。如果它大于阈值…
  • removepoint(0,@ptnum) — …删除该点。0 表示第一个输入,即您连接到此争吵中的任何内容,@ptnum 是对该点的引用。

示例:波形变形器

 40×40 网格 从中心辐射的同心正弦波。这意味着我们需要测量每个点到原点的距离:

@d = length(@P);

//我们将其输入给 sin(),并使用它直接设置每个点的 y 位置:
@P.y = sin(@d);

//对于我们的默认网格来说,波浪太大了。让我们乘以@d一些系数,这将给我们带来更多的波浪。将上一行更改为:
@P.y = sin(@d*4);

//改为由滑块驱动
@P.y = sin(@d*ch('scale'));

//点击插头按钮,玩滑块(您可以将滑块推过 1),查看波浪移动。要使波浪生动,请在内项中添加时间。
@P.y = sin(@d*ch('scale')+@Time);//正在向中心移动。
@P.y = sin(@d*ch('scale')-@Time); //负数时间

//通过滑块控制它,这样您就可以提高速度:
@P.y = sin(@d*ch('scale')+(ch('speed')*@Time));

//最终
@d = length(@P);
@d *= ch('scale');
@speed = @Time*ch('speed');
@P.y = sin(@d+@speed);

//添加:一个最大高度的控件,以及一个基于距离的衰减
//高度很容易,无论最终结果是什么,我们都会将其乘以通道参考;如果其为 1,则保持不变,0 将抵消,其他数字将适当缩放:
@P.y *= ch('height');

//控制衰减的斜坡。斜坡小部件需要一个 0-1 范围内的变量,因此我们要做的第一件事是获取距离变量,并使用 fit() 将其映射到 1 和 0 之间(请注意,我们选择 1 0,而不是 0 1,我们希望中心是最大高度)。起点和终点将由通道控制:
@falloff = fit(@d, ch('start') , ch('end') , 1,0);

//斜坡通道需要一个名称和一个变量,该变量将通过斜坡曲线重新映射。我们将获取结果并将其直接乘以 P.y:
@P.y *= chramp('falloff', @falloff);

//开始和结束是世界空间单位,但“结束”通道是一个很大的数字。我意识到这是因为我们已经将 d 乘以缩放通道来控制波的数量。快速重新洗牌代码解决了这个问题,最终结果如下:
@d = length(@P);
@speed = @Time * ch('speed');
@falloff = fit(@d, ch('start'), ch('end'),1,0);

@P.y = sin(@d*ch('scale')+@speed);
@P.y *= ch('height');
@P.y *= chramp('falloff', @falloff);

属性与变量

使用 @ 语法来获取和设置点的属性。如果你有多个 wrangle,或者你不需要 wrangle 之外的这些属性,几何电子表格就会开始变得非常混乱,并且所有这些额外的点数据都会占用内存,尤其是在你有复杂场景的情况下。

相反,您可以创建仅存在于争放中的变量。语法与其他 C 风格语言类似;用完整的单词定义类型,并且不要在变量上使用前缀:

float foo;
vector bar = {0,0,0};
matrix m;

foo = 8;
bar.x += foo;

属性(@)和 vex 变量(不含@)存在于不同的世界中,因此您可以拥有一个具有相同名称的变量,并且它们愉快地共存。但尽量避免混淆。

float dist = length(@P);  // a local vex variable that only exists within this wrangle

@dist = dist;  // create a new point attribute, @dist, and assign the local variable 'dist' to it. Worlds collide!
float d;
float speed;
float falloff;

d = length(@P);
speed = @Time * ch('speed');
falloff = fit(d, ch('start'), ch('end'),1,0);

@P.y = sin(d*ch('scale')+speed);
@P.y *= ch('height');
@P.y *= chramp('falloff', falloff);

示例:旋转

旋转带有 sin 和 cos 的单个点

假设您在原点处有一个点,并且您想让它绕一圈。Sin() 将为您提供介于 0 和 1 之间的平滑波。Cos() 给出相同的波长,但偏移了半个波长。如果您用 sin 驱动 x,用 cos 驱动 y,并对两个@Time进行输入,则该点将绕一圈移动:

@P.x = sin(@Time);
@P.y = cos(@Time);

@P.x += sin(@Time);
@P.y += cos(@Time);
//平移成一个圆圈

使用 sin 和 cos 旋转几何体

问题是所有点都得到了相同的旋转值。我们需要做的是获得每个点的偏移量,但作为旋转偏移量。把你的思绪放回高中数学,你可以使用 tan(),如果给定 x 和 y,你会返回一个角度。如果你仔细想想,每个点都与其他点相距一个时间偏移,所以让我们把它添加到@Time,看看会发生什么:

float angle = atan(@P.x, @P.y);

@P.x += sin(@Time+angle);
@P.y += cos(@Time+angle);

//它在旋转,但在旋转时做了一个奇怪的比例。我们可能想要设置位置而不是添加到现有位置

float angle = atan(@P.x, @P.y);

@P.x = sin(@Time+angle);
@P.y = cos(@Time+angle);
//与原来的盒子相比,它的比例略有不同。发生的事情是,我们还需要获得半径(即,点与原点的距离),并将其乘以旋转以获得正确缩放的值:
float angle = atan(@P.x, @P.y);
float r = length(@P);

@P.x = sin(@Time+angle)*r;
@P.y = cos(@Time+angle)*r;


//一切都很奇怪。问题是半径;我们正在 3D 空间中测量距离,但我们所需要的只是到旋转轴(即 z 轴)的直接半径。如果我们仅使用 @P.x 和 P.y 测量长度,它应该会修复它:
float angle = atan(@P.x, @P.y);
float r = length(set(@P.x, @P.y));

@P.x = sin(@Time+angle)*r;
@P.y = cos(@Time+angle)*r;

这里所做的是将我们的常规坐标转换为极坐标(角度和 r),然后再转换回来。仅仅查看极坐标本身就很有趣:

float angle = atan(@P.x, @P.y);
float r = length(set(@P.x, @P.y));

@P.x = angle;
@P.y = r;
@P.z = 0;

一个简单的旋转工具

float angle = atan(@P.x, @P.y);
float r = length(set(@P.x, @P.y));
float amount = ch('amount');

@P.x = sin(amount+angle)*r;
@P.y = cos(amount+angle)*r;

单击按钮获取滑块,滑开。您会注意到,“1”的数量会旋转约 30 度,这是没有意义的。嗯,确实如此;sin/cos/atan 都以弧度为单位工作。如果我们假设滑块是度,您也需要将其转换为弧度:

float angle = atan(@P.x, @P.y);
float r = length(set(@P.x, @P.y));
float amount = radians(ch('amount'));

@P.x = sin(amount+angle)*r;
@P.y = cos(amount+angle)*r;

旋转成旋转或螺旋形

现在你可以尝试一些愚蠢的事情。与其平均旋转所有点,不如根据半径缩放它呢?即,靠近原点的点不会旋转,远处的点会旋转很多。你会旋转。如果您只添加“r”,您会得到一个微妙的旋转,因此我添加了另一个滑块来缩放旋转效果:

float angle = atan(@P.x, @P.y);
float r = length(set(@P.x, @P.y));
float amount = radians(ch('amount'));
float twirl = r*ch('twirl');

@P.x = sin(amount+angle+twirl)*r;
@P.y = cos(amount+angle+twirl)*r;

矩阵旋转

如果您想进行在 x/y/z 轴上未完全对齐的旋转,则会变得有点棘手。另一种旋转方法是使用矩阵。您使用的是 Houdini,因此您可能知道矩阵,但您正在阅读教程,因此,如果有人要求您将矩阵变换应用于某些几何体,您可能会像我一样感到恐慌。好吧,别担心,这比听起来容易。步骤如下:

  • 创建矩阵
  • 旋转矩阵
  • 将矩阵应用于我们的几何体

最后一步很简单,要将矩阵应用于一个点,只需将其相乘即可。

另外两件事在代码中更容易解释:所以我们需要的是一个矩阵,以及一种旋转它的方法。令人惊讶的是,这个函数被称为旋转。这是它的工作原理:

// create a matrix
matrix3 m = ident();

// rotate the matrix
vector axis = {0,0,1};
float angle = radians(ch('amount'));

rotate(m, angle, axis);

// apply the rotation
@P *= m;

我们创建一个空矩阵(即,旋转是 0 0 0,比例是 1 1 1)。然后,我们定义要旋转的轴和要旋转的量。然后我们调用rotate,它将直接修改矩阵m。最后,我们将其乘以 @P 以应用每个点的旋转。

与上面类似,您可以通过将角度乘以到轴的距离并缩放它来控制效果的强度来制作旋转效果:

vector axis = {0,0,1};
float angle = radians(ch('amount'));
angle *= ch('twirl')*length(set(@P.x, @P.y));
matrix3 m = ident();

rotate(m, angle, axis);

@P *= m;

因为我们使用轴心,所以它可以是我们想要的任何东西。随机向量 N,一些任意的其他东西,并不重要。

使用矩阵进行旋转的一个很好的好处是它很容易映射到@orient中。当使用复制 sop 或实例化时,如果他们找到一个@orient属性,每个复制的形状都会被旋转。@orient是一个四元数,一个 4 值向量,人类不容易作,但您可以使用 quaternion() 轻松地将矩阵转换为四元数。

您可以执行以下作来获得每个点的随机旋转,然后将其提供给副本 sop:

vector axis = vector(rand(@ptnum));
float angle = radians(ch('amount'));
matrix3 m = ident();

rotate(m, angle, axis);
@orient = quaternion(m);

围绕边旋转图元(Rotate prims around a edge)

根据我们目前所知道的,围绕边旋转图元应该不会太难。(我们假设我们现在有一个三角形或四边形)。将边视为旋转轴,并如前面所示旋转。如果您还记得向量数学,要获得 2 点之间的向量,请减去它们。因此,假设我们想要的边位于点 0 和点 1 之间,我们可以这样做:

vector p0 = point(0,'P',0);
vector p1 = point(0,'P',1);

vector axis = p0-p1;

并使用它来创建四元数方向,正如我们已经看到的那样:

float angle = ch('angle');
matrix3 rotm = ident();
rotate(rotm, angle, axis);
vector4 orient = quaternion(rotm);

现在,如果您将其直接应用于几何体,它会旋转,但它会以原点为中心旋转,这是错误的。旋转必须以边缘的中点为中心。再次将你的思绪放回高中,你可以通过相加得到 2 分的中点,然后除以 2:

// get midpoint of edge
vector pivot = (p0+p1)/2;

从这里开始,vex 中可能有几种方法可以让它完成我们需要的事情,在我的情况下,我使用 instance()。这是一个 vex 调用,它构造了一个类似于复制 sop 为每个副本创建的转换矩阵。这意味着您可以为其提供位置、方向、枢轴等,它会按照我们的意愿进行作。因此,我们将首先构造一个方向四元数,将其与枢轴一起用作 instance() 的输入之一,然后使用它来移动我们的点:

// create a transform matrix using orient and pivot, and other default values
vector N = 0;  // not important cos we're using orient
vector scale = 1; // ie, leave the scale as-is
vector postrotation = 0;  // also not important, we don't need extra rotation on our orient

matrix m = instance(pivot, N, scale, postrotation, orient, pivot);

// move the point!
@P *= m;

如果我们在一个形状中有很多 prim,那么就需要做更多的簿记工作。首先,必须将图元分开,因此请在唯一模式下使用熔断 sop 来执行此作。然后,要获得每个图元的 0 和 1 个点,您可以使用 primpoints(),它将按顺序返回图元中点的数组。您可以只获取该数组的 0 和 1 条目:

int points[] = primpoints(0,@primnum); // list of points in prim

// get @P of first and second point
vector p0 = point(0,'P',points[0]);
vector p1 = point(0,'P',points[1]);

这是完整的内容

int points[] = primpoints(0,@primnum); // list of points in prim

// get @P of first and second point
vector p0 = point(0,'P',points[0]);
vector p1 = point(0,'P',points[1]);

vector axis = p0-p1;

float angle = ch('angle');
matrix3 rotm = ident();
rotate(rotm, angle, axis);
vector4 orient = quaternion(rotm);

// get midpoint of edge
vector pivot = (p0+p1)/2;

// create a transform matrix using orient and pivot, and other default values
vector N = 0;  // not important cos we're using orient
vector scale = 1; // ie, leave the scale as-is
vector postrotation = 0;  // also not important, we don't need extra rotation on our orient

matrix m = instance(pivot, N, scale, postrotation, orient, pivot);

// move the point!
@P *= m;

替代版本

另一种方法单独使用旋转矩阵是行不通的,因为它应用了围绕原点的旋转。解决此问题的一个简单方法是将这些图元移动到原点,进行旋转,然后将其移回。看起来像这样:

int points[] = primpoints(0,@primnum); // list of points in prim

// get @P of first and second point
vector p0 = point(0,'P',points[0]);
vector p1 = point(0,'P',points[1]);

vector axis = p0-p1;

float angle = ch('angle');
matrix3 rotm = ident();
rotate(rotm, angle, axis);

// get midpoint of edge
vector pivot = (p0+p1)/2;

// move point to origin, rotate, move back

@P -= pivot;
@P *= rotm;
@P += pivot;

从其他点、其他地理位置获取值

如果要当前点的颜色,请使用 @Cd。但是,如果您想要第 5 点的颜色怎么办?

float otherCd = point(0, "Cd", 5);

point() 函数允许您查询其他点的属性(前 0 表示“该点的第一个输入”)。如果将另一个网格连接到点争吵的第二个输入,并且您想知道网格中点 5 的颜色,请更改地理编号:

float otherCd = point(1, "Cd", 5);

这通常用于在相似网格体之间传输或混合属性。例如,您有 2 个网格,并且想要将颜色设置为两者的相加。点编号将相同,因此您可以使用相同的 ptnum 来获取第二个网格,并执行您需要的作:

vector otherCd = point(1, "Cd", @ptnum);
@Cd += otherCd;

点云模糊属性

你在网格上有 Cd,你想模糊它。一种方法是迭代每个点,查看其邻居,将结果相加,然后除以邻居的数量。因为它是一个网格,并且点编号是一致的,所以你可以做类似的事情

int width = 50; // say the grid is 50 points wide
vector left = point(0,'Cd', @ptnum-1);
vector right = point(0,'Cd', @ptnum+1);
vector top = point(0,'Cd', @ptnum+50);
vector bottom = point(0,'Cd', @ptnum-50);
@Cd = left + right + top + bottom;
@Cd /= 4.0;

有效,但笨重,而且它只能与网格一起使用。使用 neighbour() 和 neighbourcount() 函数更通用(借用自 odforce post):

int neighbours = neighbourcount(0, @ptnum);
vector totalCd = 0;
for (int i = 0; i < neighbours; i++)
{
    int neighPtnum = neighbour(0, @ptnum, i);
    totalCd += point(0, "Cd", neighPtnum);
}
@Cd = totalCd/neighbours;

但是,还有一种更简短的清洁方法,即使用点云:

int mypc = pcopen(0, 'P', @P, 1, 8);
@Cd = pcfilter(mypc, 'Cd');

pcopen() 旨在打开磁盘上的点云 (.pc) 文件,但通过使用 0 引用,Wrangle 会将传入的 Geo 视为点云。您需要告诉它如何在云中查找值,99.999% 的时间您会使用当前点 (@P) 的位置在云中查找位置 (“P”)。它搜索到最大距离 (1),并返回最大点数 (8)。Pcopen() 返回一个句柄,以便您稍后可以参考点云,这里我将其存储在变量“mypc”中。

pcfilter() 的目的是查看点云的结果,并返回您指定的属性的过滤(模糊、平均,随心所欲地称呼)结果。在这里,我们告诉它使用我们刚刚打开的云,并查找 Cd 属性。由于 pcopen 返回了当前点附近最接近的 8 个 Cd 结果,因此 pcfilter 将返回这 8 个点的平均值。换句话说,我们刚刚模糊了 Cd 周围的 8 个点,本质上是 1 像素模糊。添加一些通道,我们可以使用滑块控制模糊:

float maxdist = ch('maxdist');
int blur = chi('blur');
int pc = pcopen(0, 'P', @P, maxdist, blur);
@Cd = pcfilter(pc, 'Cd');

maxdist 并没有真正影响结果,它更多的是为了速度;如果您知道搜索永远不必大于一定距离,那么这些点云函数的运行速度会快得多。另请注意,pcfilter() 比之前基于邻居的方法做得更聪明,它知道给远点的重要性低于近点。

另一个用途是基于 vex 的平滑功能,在具有大量轴划分的多边形网格模式下的盒子上尝试一下。这里唯一的区别是,我们不是模糊 Cd,而是模糊 P:

float maxdist = ch('maxdist');
int blur = chi('blur');
int pc = pcopen(0, 'P', @P, maxdist, blur);
@P = pcfilter(pc, 'P');

有多种函数可以作点云,大多数都以“pc”为前缀。如果您以前没有使用过点云,您可能想知道它们为什么存在,它们如何比执行 point() 或 neighour() 查找等更好。它们只是存储 3D 数据的另一种方式,但通过剥离边缘和多边形,可以采取一些捷径来使某些作更快。获取几何体子部分的过滤结果就是这样一种作,其他作正在快速迭代近点,或使用简单平均值近似远点块。

Wrangles vs hscript vs vops vs 其他一切

作为一般规则,Vex 总是比使用 hscript 更快,并且扩展性要好得多。

Vops 很棒,但通常情况下,您可以在 vex 中用 2 行完成的简单事情,在 vops 中需要 7 或 8 个节点。也就是说,某些作在 vops 中更容易,例如使用噪声或加载其他模式,或者在您还不知道自己的目标时构建东西。完成后,如果您认为更好,您可以随时在争吵中重写您的设置。

对于其他 SOP,一个 Wrangle 可以完成其他几个 SOP 的工作,通常效率更高,并且能够轻松制作 UI 元素。一些例子:

  • 点 SOP – 许多年长的 TUT 使用点 SOP。 不要。 就说不。
  • attribcreate sop – 在争吵中更快、更容易。你可能想要使用 attribcreate 的唯一原因是获取局部变量,但你只会将它们与 point sop 一起使用,我们已经同意这很糟糕。
  • VOP 绑定和 VOP 绑定导出 – 使用 @ 快捷键获取和设置点属性比在 VOP 中更容易。
  • VOP 中的提升参数 – 再次,ch(’foo’) 与 r.click,绑定,摆弄名称,意识到它的错误,在半挂起状态下获取参数,呃
  • HCript 中的通道引用 – 与 ch() 的自动绑定是蜜蜂的膝盖。那个小插头按钮是有史以来最好的东西。

这有点陈词滥调,所有的 sops 和 vop 在某些时候都是有用的,但争吵的速度和优雅是无与伦比的。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容