模型局部剖切、裁剪简介,以three为例

模型局部剖切、裁剪简介,以three为例

六月 23, 2022

模型局部剖切、裁剪简介,以three为例

参考资料

  1. https://github.com/mrdoob/three.js
  2. https://blog.csdn.net/tianyapai/article/details/102502525
  3. https://www.cnblogs.com/xiaxiangx/p/13873037.html

剖切

如果在three功能中需要实现剖切功能,通常的做法是使用three材质或者renderer中的clippingPlanes属性,如果Material使用则对该材质对象生效,如果对renderer使用则对场景内的所有物体生效

three相关接口描述

Material

.clipIntersection : Boolean

更改剪裁平面的行为,以便仅剪切其交叉点,而不是它们的并集。默认值为 false。

.clippingPlanes : Array

用户定义的剪裁平面,在世界空间中指定为THREE.Plane对象。这些平面适用于所有使用此材质的对象。空间中与平面的有符号距离为负的点被剪裁(未渲染)。 这需要WebGLRenderer.localClippingEnabled为true。 示例请参阅WebGL / clipping /intersection。默认值为 null。

.clipShadows : Boolean

定义是否根据此材质上指定的剪裁平面剪切阴影。默认值为 false。

WebGLRenderer

.clippingPlanes : Array

用户自定义的剪裁平面,在世界空间中被指定为THREE.Plane对象。 这些平面全局使用。空间中与该平面点积为负的点将被切掉。 默认值是[]

.localClippingEnabled : Boolean

定义渲染器是否考虑对象级剪切平面。 默认为false.

例子

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

// 初始化测试模型
initMesh(): void {
let box11 = new THREE.BoxGeometry(10, 10, 10);
let box12 = new THREE.BoxGeometry(10, 10, 10);

let material11 = new THREE.MeshLambertMaterial({
color: '#ff0000',
side: THREE.DoubleSide,
});
let material12 = new THREE.MeshLambertMaterial({
color: '#ff0000',
side: THREE.DoubleSide,
});

let mesh11 = new THREE.Mesh(box11, material11);
mesh11.position.set(30, 50, 0);
let mesh12 = new THREE.Mesh(box12, material12);
mesh12.position.set(60, 50, 0);

// 同材质但不同对象
let box21 = new THREE.BoxGeometry(30, 30, 30);
let box22 = new THREE.BoxGeometry(30, 30, 30);
let material2 = new THREE.MeshPhongMaterial({
color: '#00ff00',
side: THREE.DoubleSide,
});

// 同一对象材质
let mesh21 = new THREE.Mesh(box21, material2);
mesh21.position.set(0, 50, 50);
let mesh22 = new THREE.Mesh(box22, material2);
mesh22.position.set(0, 50, 100);

this.scene.add(mesh11, mesh12, mesh21, mesh22);

// 分割平面
let plane1 = new THREE.Plane(new THREE.Vector3(0, -1, 0), 50);

mesh11.material.clippingPlanes = [plane1];

mesh21.material.clippingPlanes = [plane1];
}

// 可视分割面
initPlane(): void {
let planeGeometry = new THREE.PlaneGeometry(300, 300);
//平面使用颜色为0xcccccc的基本材质
let planeMaterial = new THREE.MeshLambertMaterial({
// 生成材质
transparent: true,
opacity: 0.4,
color: 0xdddddd,
side: THREE.DoubleSide,
});
let plane = new THREE.Mesh(planeGeometry, planeMaterial);
//设置屏幕的位置和旋转角度
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, 50, 0);
plane.receiveShadow = true;
//将平面添加场景中
this.scene.add(plane);
}

效果图

可以发现对于相同对象的Material,设置剖切平面会对多个模型都产生影响,不能精准的控制到每一个模型,同时无法处理材质合并后对于batch的情况。

THREE剖切源码分析、

通过参考three源码中对于剖切处理的shader就可以找到解决方案。下面是对于源码的解析,只关心应用的可以跳过这一部分.

sahder解析

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
// clipping_planes_fragment.glsl
/* glsl */`
#if NUM_CLIPPING_PLANES > 0

varying vec3 vClipPosition;

uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];

#endif

// 有裁剪平面才执行
#if NUM_CLIPPING_PLANES > 0
// 声明裁剪平面
vec4 plane;

//自定义裁切 物体的材质中定义裁切面
#pragma unroll_loop_start //目的是将下面的循环展开为一段段的代码,空间换时间
for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {
// 赋值
plane = clippingPlanes[ i ];
// 模型 片段点 与 平面的 向量积 如果大于平面的W 则不渲染
if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;

}
#pragma unroll_loop_end

//全局裁切 WebGLRender中定义裁切面
#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES

bool clipped = true;

#pragma unroll_loop_start
for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {

plane = clippingPlanes[ i ];
// 需要都为 true
clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;

}
#pragma unroll_loop_end

if ( clipped ) discard;

#endif

#endif
`;

应用层面

可以从sahder解析中看到clippingPlanes中传入的是vec4的变量,同时也没有做相机坐标系的变换,对于这个变换,three使用WebGLClipping对象以解决问题,同时在每次render刷新各个平面.

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
// 初始化剖切,这一步决定了当前视角剖切平面的vec4变量

this.init = function ( planes, enableLocalClipping, camera ) {

const enabled =
planes.length !== 0 ||
enableLocalClipping ||
// enable state of previous frame - the clipping code has to
// run another frame in order to reset the state:
numGlobalPlanes !== 0 ||
localClippingEnabled;

localClippingEnabled = enableLocalClipping;

globalState = projectPlanes( planes, camera, 0 );
numGlobalPlanes = planes.length;

return enabled;

};

// 阴影开始
this.beginShadows = function () {

renderingShadows = true;
projectPlanes( null );

};

// 阴影结束
this.endShadows = function () {

renderingShadows = false;
resetGlobalState();

};

// 设置剖切平面vec4变量
this.setState = function ( material, camera, useCache ) {

const planes = material.clippingPlanes,
clipIntersection = material.clipIntersection,
clipShadows = material.clipShadows;

const materialProperties = properties.get( material );

if ( ! localClippingEnabled || planes === null || planes.length === 0 || renderingShadows && ! clipShadows ) {

// there's no local clipping

if ( renderingShadows ) {

// there's no global clipping

projectPlanes( null );

} else {

resetGlobalState();

}

} else {

const nGlobal = renderingShadows ? 0 : numGlobalPlanes,
lGlobal = nGlobal * 4;

let dstArray = materialProperties.clippingState || null;

uniform.value = dstArray; // ensure unique state

dstArray = projectPlanes( planes, camera, lGlobal, useCache );

for ( let i = 0; i !== lGlobal; ++ i ) {

dstArray[ i ] = globalState[ i ];

}

materialProperties.clippingState = dstArray;
this.numIntersection = clipIntersection ? this.numPlanes : 0;
this.numPlanes += nGlobal;

}


};

batch剖切

直接上解决方案

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

vertex = `
#include <common>
#include <color_pars_vertex>
varying vec3 vClipPosition;

attribute int _nekoyuu;
flat out int nekoyuu;

// 自定义属性
void main() {
// 传输自定义变量
nekoyuu = _nekoyuu;
#include <color_vertex>

#include <begin_vertex>
#include <project_vertex>

// 设置剖切点
vClipPosition = - mvPosition.xyz;

}
`;

fragment = /* glsl */ `
uniform vec3 diffuse;
uniform float opacity;

#ifndef FLAT_SHADED

varying vec3 vNormal;

#endif

#include <common>
#include <color_pars_fragment>
#include <alphatest_pars_fragment>

varying vec3 vClipPosition;
uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];

// 自定义属性
flat in int nekoyuu;

void main() {
if(nekoyuu > 0){
vec4 plane;
plane = clippingPlanes[ nekoyuu - 1 ];
if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;
}

vec4 diffuseColor = vec4( diffuse, opacity );

#include <color_fragment>
#include <alphatest_fragment>

#include <encodings_fragment>
#include <premultiplied_alpha_fragment>

}
`;

// 自定义材质
getMyMaterial(): THREE.ShaderMaterial {
let m = new THREE.ShaderMaterial({
vertexShader: this.vertex,
fragmentShader: this.fragment,
side: THREE.DoubleSide,
});

return m;
}

batchGeo(): void {
let b = this.getGeometry();
let m = this.getMyMaterial();

let mesh = new THREE.Mesh(b, m);
mesh.position.set(0, 50, 0);

this.scene.add(mesh);

// 使用ShaderMaterial这里需要主动开启剖切
mesh.material.clipping = true;
// 这里没有单独处理剖切平面,直接借用了three原来的坐标系转换
mesh.material.clippingPlanes = [
new THREE.Plane(new THREE.Vector3(0, 1, 0), -50),
];
}

// 手搓二进制物体
getGeometry(): THREE.BufferGeometry {
const vertices = [
// front
{ pos: [-20, -20, 10], norm: [0, 0, 1], uv: [0, 0], nekoyuu: [1] }, // 0
{ pos: [40, -20, 10], norm: [0, 0, 1], uv: [1, 0], nekoyuu: [1] }, // 1
{ pos: [-20, 20, 10], norm: [0, 0, 1], uv: [0, 1], nekoyuu: [1] }, // 2
{ pos: [20, 20, 10], norm: [0, 0, 1], uv: [1, 1], nekoyuu: [1] }, // 3
// right
{ pos: [40, -20, 10], norm: [1, 0, 0], uv: [0, 0], nekoyuu: [1] }, // 4
{ pos: [60, -20, -20], norm: [1, 0, 0], uv: [1, 0], nekoyuu: [1] }, // 5
{ pos: [20, 20, 10], norm: [1, 0, 0], uv: [0, 1], nekoyuu: [1] }, // 6
{ pos: [20, 20, -20], norm: [1, 0, 0], uv: [1, 1], nekoyuu: [1] }, // 7
// back
{ pos: [60, -20, -20], norm: [0, 0, -1], uv: [0, 0], nekoyuu: [1] }, // 8
{ pos: [-20, -20, -40], norm: [0, 0, -1], uv: [1, 0], nekoyuu: [1] }, // 9
{ pos: [20, 20, -20], norm: [0, 0, -1], uv: [0, 1], nekoyuu: [1] }, // 10
{ pos: [-20, 10, -20], norm: [0, 0, -1], uv: [1, 1], nekoyuu: [1] }, // 11
// left
{ pos: [-20, -20, -40], norm: [-1, 0, 0], uv: [0, 0], nekoyuu: [1] }, // 12
{ pos: [-20, -20, 10], norm: [-1, 0, 0], uv: [1, 0], nekoyuu: [1] }, // 13
{ pos: [-20, 10, -20], norm: [-1, 0, 0], uv: [0, 1], nekoyuu: [1] }, // 14
{ pos: [-20, 20, 10], norm: [-1, 0, 0], uv: [1, 1], nekoyuu: [1] }, // 15
// top
{ pos: [20, 20, 10], norm: [0, 1, 0], uv: [0, 0], nekoyuu: [1] }, // 16
{ pos: [20, 20, -20], norm: [0, 1, 0], uv: [1, 0], nekoyuu: [1] }, // 17
{ pos: [-20, 20, 10], norm: [0, 1, 0], uv: [0, 1], nekoyuu: [1] }, // 18
{ pos: [-20, 10, -20], norm: [0, 1, 0], uv: [1, 1], nekoyuu: [1] }, // 19
// bottom
{ pos: [60, -20, -20], norm: [0, -1, 0], uv: [0, 0], nekoyuu: [1] }, // 20
{ pos: [40, -20, 10], norm: [0, -1, 0], uv: [1, 0], nekoyuu: [1] }, // 21
{ pos: [-20, -20, -40], norm: [0, -1, 0], uv: [0, 1], nekoyuu: [1] }, // 22
{ pos: [-20, -20, 10], norm: [0, -1, 0], uv: [1, 1], nekoyuu: [1] }, // 23
];

const positions = [];
const normals = [];
const uvs = [];
const colors = [];
const _nekoyuu = [];
for (const vertex of vertices) {
positions.push(...vertex.pos);
normals.push(...vertex.norm);
uvs.push(...vertex.uv);
colors.push(Math.random(), Math.random(), Math.random());
_nekoyuu.push(...vertex.nekoyuu);
}

const geometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
const normalNumComponents = 3;
const uvNumComponents = 2;
geometry.setAttribute(
'position',
new THREE.BufferAttribute(
new Float32Array(positions),
positionNumComponents,
),
);
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents),
);
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents),
);
geometry.setAttribute(
'color',
new THREE.BufferAttribute(new Float32Array(colors), 3),
);
// 设置自定义变量
// 使用那个剖切平面
geometry.setAttribute(
'_nekoyuu',
new THREE.BufferAttribute(new Int32Array(_nekoyuu), 1),
);
geometry.setIndex([
// front
0, 1, 2, 2, 1, 3,
// right
4, 5, 6, 6, 5, 7,
// back
8, 9, 10, 10, 9, 11,
// left
12, 13, 14, 14, 13, 15,
// top
16, 17, 18, 18, 17, 19,
// bottom
20, 21, 22, 22, 21, 23,
]);
return geometry;
}

参考效果