antvis / antvis/G

[g-webgl] 支持裁剪区域

Open
#825 0 comments 0 reactions 1 assignee Claimed by @xiaoiver View on GitHub
enhancement
Dominant language
TypeScript
Stars
1.2k
Forks
230
PR merge metrics
No merged PRs in 30d

Description

http://g-next.antv.vision/zh/docs/api/basic/display-object#clippath

参考 Oasis 团队的实现,使用 Stencil,仅支持内遮照:https://zhuanlan.zhihu.com/p/394716496

G 之前支持通过 `setClip` 为一个图形设置裁切区域(区域内的部分显示,区域外的隐藏),例如画一张圆形图片。可该属性值可以是任意基础图形,例如 Circle、Rect 等等。

![](https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*Iy4RQZgT3EUAAAAAAAAAAAAAARQnAQ#crop=0&crop=0&crop=1&crop=1&id=Q1waK&originHeight=242&originWidth=220&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
# API
在新版中我们参考 CSS 的 [clip-path](https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path)。例如我们想创建一个圆形的图片:

```javascript
const image = new Image({
attrs: {
x: 100,
y: 100,
width: 200,
height: 200,
clipPath: new Circle({ // 裁剪区域
attrs: {
x: 100, // 相对于 Image 的局部坐标系
y: 100,
r: 50,
},
}),
}
});
```

也可以在创建图形之后设置裁剪区域,因此以上写法等价于:

```javascript
const image = new Image({
attrs: {
//... 省略其他属性
}
});

image.style.clipPath = new Circle({
attrs: {
r: 10,
},
});
// 或者兼容旧版写法
image.setClip(new Circle({
attrs: {
r: 10,
},
}));
```

## 注意事项
### 裁剪区域图形
理论上应当支持 Group 甚至是自定义图形,但看了下旧版 G 也不支持,暂时保持一致。

### 裁剪图形坐标系
相对于原始图形,因此在上面的例子中,在 200 * 200 的图片中挖一个“圆洞”,需要将 Circle 圆心设置为 `100, 100`。

### 拾取 & 包围盒
计算方式:原图形与裁剪区域的交集

### 修改裁剪图形
表示裁剪区域的图形虽然不在场景图中,但它的修改同样会触发重绘,例如修改 Circle 的半径,对裁剪区域进行变换等。此时都需要设置被裁剪图形的脏标志。

但大部分绘图属性对于裁剪图形都是无意义的,例如 fill 并不会影响裁剪区域。

# Canvas 实现
Canvas 的实现比较简单,在已有渲染流程中插入 clip。其实目前的脏矩阵渲染使用的就是 clearRect + clip:
```tsx
// 获取裁剪图形
const clipPathShape = object.style.clipPath;
if (clipPathShape) {
context.save();

// apply clip shape's RTS
this.applyTransform(context, clipPathShape.getLocalTransform());

// generate path in local space
const generatePath = this.pathGeneratorFactory(clipPathShape.nodeName);
if (generatePath) {
this.useAnchor(context, clipPathShape, () => {
// 绘制裁剪图形路径
context.beginPath();
generatePath(context, clipPathShape.parsedStyle);
context.closePath();
});
}

context.restore();
// 只有该区域内允许绘制,超出部分被裁剪
context.clip();
}

// 正常绘制被裁剪图形
```
# SVG 实现
声明 [clipPath](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/clipPath) 后,通过 SVG 元素的 [clip-path](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/clip-path) 属性引用:
```html



```

需要注意按照目前的规范 clipPath 内不能包含 `` ,但这一点的合理性仍在讨论中:

- ​[https://stackoverflow.com/questions/66417211/why-does-g-not-work-in-clippath-in-svg](https://stackoverflow.com/questions/66417211/why-does-g-not-work-in-clippath-in-svg)
- [https://github.com/w3c/fxtf-drafts/issues/17](https://github.com/w3c/fxtf-drafts/issues/17)
- [https://github.com/w3c/svgwg/issues/720](https://github.com/w3c/svgwg/issues/720)
# WebGL 实现
Oasis 团队写了一篇很好的文章:[https://zhuanlan.zhihu.com/p/394716496](https://zhuanlan.zhihu.com/p/394716496),下图来自文章内,G 中的裁剪图形其实是“内遮罩”效果:
![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642043753148-b151c719-718d-49c2-815d-4428a9d606da.png#clientId=u1bbdba74-b425-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=383&id=m9q81&margin=%5Bobject%20Object%5D&name=image.png&originHeight=766&originWidth=1076&originalType=binary&ratio=1&rotation=0&showTitle=false&size=511510&status=done&style=none&taskId=u31ce7899-79d5-4e43-a29d-00ff859595d&title=&width=538)
其他教程可参考:

- [https://webgl2fundamentals.org/webgl/lessons/webgl-qna-how-to-use-the-stencil-buffer.html](https://webgl2fundamentals.org/webgl/lessons/webgl-qna-how-to-use-the-stencil-buffer.html)
- [https://open.gl/depthstencils](https://open.gl/depthstencils)
- [http://www.jiazhengblog.com/blog/2016/04/05/2941/](http://www.jiazhengblog.com/blog/2016/04/05/2941/)

基本思路是使用 stencil buffer:
> 可以在Buffer中指定一个形状作为模板,接着通过stencil test(模板测试)过程让位于形状内部的物体显示,而外部不显示,类似遮罩的效果。当然也可以反过来,让形状内部不显示物体,而外部显示。Stencil buffer为每个fragment提供8位的存储空间,即可以存储256个不同的数值,但是如果要实现一个简单的模板剪裁效果,其实1位(0和1)就够用了。Stencil buffer的作用如下图所示:

![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642045349906-b98a84ee-c543-40d3-b060-fd9b971db821.png#clientId=u1bbdba74-b425-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=96&id=u1c90eda5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=189&originWidth=657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53642&status=done&style=none&taskId=u1429050b-fe32-4719-a51b-a7e4daef057&title=&width=334.5)![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642045471035-e8a78ed8-53cc-42bf-8fe7-66007a1b80e3.png#clientId=u1bbdba74-b425-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=95&id=u0c725310&margin=%5Bobject%20Object%5D&name=image.png&originHeight=189&originWidth=657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30211&status=done&style=none&taskId=u9f9e3637-0b2f-4c52-a1f2-60490f49906&title=&width=328.5)

## 开启 stencil
在创建 webgl 上下文时,需要开启`stencil`(这一步挺容易遗漏的),在 three.js 中是默认开启的:
```tsx
const options: WebGLContextAttributes = {
// @see https://webglfundamentals.org/webgl/lessons/webgl-qna-how-to-use-the-stencil-buffer.html
stencil: true,
antialias: false,
// @see https://stackoverflow.com/questions/27746091/preservedrawingbuffer-false-is-it-worth-the-effort
preserveDrawingBuffer: false,
};

// 获取 webgl 上下文
gl = $canvas.getContext('webgl2', options);
```

## 开启扩展
WebGL1 需要开启深度纹理扩展(WebGL2 内置):[https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_depth_texture](https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_depth_texture)
开启后在创建纹理、RT 时支持新的类型(见下一小节)。

## 创建 RenderTarget
关联 DepthStencil Slot,存储深度与模版缓冲数据:
```tsx
const mainDepthDesc = makeBackbufferDescSimple(
RGAttachmentSlot.DepthStencil,
renderInput,
opaqueWhiteFullClearRenderPassDescriptor,
);
```
```tsx
// 创建 rt
const gl_format = this.device.translateTextureInternalFormat(pixelFormat);
case Format.D24_S8:
return GL.DEPTH24_STENCIL8;

// 创建纹理
const gl_format = this.device.translateTextureFormat(this.pixelFormat);
const gl_type = this.device.translateTextureType(this.pixelFormat);
// format
case Format.D24_S8:
case Format.D32F_S8:
return GL.DEPTH_STENCIL;
// type
case FormatTypeFlags.D24S8:
// @see https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_depth_texture
return GL.UNSIGNED_INT_24_8;
```
从下图中可以看出 Framebuffer 关联的 attachment:1个 depth,1个 stencil,2个 color:
![截屏2022-01-13 上午11.10.30.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642043463100-0d2ab128-ff09-43f1-9dc7-948f1ab8fdfd.png#clientId=u1bbdba74-b425-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u4cd8e7e0&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-01-13%20%E4%B8%8A%E5%8D%8811.10.30.png&originHeight=1776&originWidth=3126&originalType=binary&ratio=1&rotation=0&showTitle=false&size=677766&status=done&style=none&taskId=uce8f83f2-e912-4f2c-8d48-de939c72acc&title=)

## 清空模版
在开始绘制模版前需要先清空,WebGL1 / 2 可以使用不同方式:
```tsx
if (isWebGL2(gl)) {
gl.clearBufferiv(gl.STENCIL, 0, [stencilClearValue]);
} else {
gl.clearStencil(stencilClearValue);
gl.clear(gl.STENCIL_BUFFER_BIT);
}
```

## 绘制模版
当一个图形作为模版绘制时,需要设置不同的材质属性([G 的材质系统](https://yuque.antfin-inc.com/antv/czqvg5/co0gz2)):
```tsx
if (this.isClipPath) {
// 作为模版被绘制
this.material.stencilWrite = true;
this.material.depthWrite = false;
this.material.stencilCompare = CompareMode.Always;
this.material.stencilPassOp = StencilOp.Replace;
this.material.stencilRef = 1;
} else {
// 作为正常图形绘制
}
```
其中`stencilWrite`表示需要写入模版缓冲,而`depthWrite`表示不需要写入深度缓冲。在绘制模版时,`Replace`表示直接写入模版缓冲,写入值使用`stencilRef`这里设置为 1,此时模版缓冲内容如下:
![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642045349906-b98a84ee-c543-40d3-b060-fd9b971db821.png#clientId=u1bbdba74-b425-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=150&id=rAY5z&margin=%5Bobject%20Object%5D&name=image.png&originHeight=189&originWidth=657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53642&status=done&style=none&taskId=u1429050b-fe32-4719-a51b-a7e4daef057&title=&width=520.5)
往 stencil buffer 中写时,我们不希望影响到 color buffer,通过 colorMask 可以关闭写入。等模版绘制完成后再打开:
```tsx
renderInst.setMegaStateFlags({
attachmentsState: [
{
channelWriteMask: this.material.stencilWrite
? ChannelWriteMask.None // 会导致 gl.colorMask(false, false, false, false)
: ChannelWriteMask.AllChannels,
}
]
});
```

## 绘制被裁剪图形
在正常绘制图形时,通过与模版缓冲中写入的值比较,决定是否写入 color buffer。如果从渲染管线的角度可以看到模版测试所处的位置,测试通过才输出。下图来自:[http://www.jiazhengblog.com/blog/2016/04/05/2941/](http://www.jiazhengblog.com/blog/2016/04/05/2941/)
![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642046118768-63240f02-da6a-46fe-be22-f3d3cd843c8c.png#clientId=u45f4cc6e-9967-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=458&id=u9e775e2e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=526&originWidth=571&originalType=binary&ratio=1&rotation=0&showTitle=false&size=57143&status=done&style=none&taskId=u59d8c76b-2df1-4e32-b5c0-13f288cc57e&title=&width=497.5)
关闭模版缓冲写入,正常开启深度测试。和之前的 ref 值(设置为 1)比较即可,相等就输出:
```tsx
this.material.stencilWrite = false;
this.material.depthWrite = true;
this.material.stencilCompare = CompareMode.Equal;
this.material.stencilPassOp = StencilOp.Keep;
this.material.stencilRef = 1; // 与写入模版缓冲时设置的值配套
```
不难发现,如果模版缓冲中只有一个值,当绘制多组裁剪图形时,会出现错误的重叠:
![截屏2022-01-13 下午12.37.32.png](https://intranetproxy.alipay.com/skylark/lark/0/2022/png/158945/1642048665421-487eb0a4-6130-482e-9b93-8a5afd2aa841.png#clientId=u24a4d834-7295-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=275&id=u5d28740a&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-01-13%20%E4%B8%8B%E5%8D%8812.37.32.png&originHeight=594&originWidth=492&originalType=binary&ratio=1&rotation=0&showTitle=false&size=281037&status=done&style=none&taskId=u54a74180-c446-45f1-ae80-326ceb0cfd4&title=&width=228)
## 多组 ref
解决办法当然是使用多组 ref。由于我们使用 D24_S8 格式(24 bit 存深度)纹理的,因此 stencil 能用 8 bit,取值范围 `0~255`
> A GLint specifying the reference value for the stencil test. This value is clamped to the range **0 to 2^n - 1** where n is the number of bitplanes in the stencil buffer. The default value is 0.

## 其他优化
在 Oasis 团队的文章中提到遮罩之间的 diff:[https://zhuanlan.zhihu.com/p/394716496](https://zhuanlan.zhihu.com/p/394716496)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.