@woosh/meep-engine 2.38.1 → 2.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/geom/Quaternion.js +70 -0
- package/core/geom/Vector2.js +17 -0
- package/core/geom/v3_angle_between.js +17 -3
- package/engine/ecs/EntityBuilder.d.ts +3 -1
- package/engine/ecs/EntityBuilder.js +10 -1
- package/engine/ecs/EntityComponentDataset.js +1 -1
- package/engine/ecs/parent/ChildEntities.d.ts +3 -0
- package/engine/ecs/parent/ChildEntities.js +41 -0
- package/engine/ecs/system/AbstractContextSystem.js +4 -2
- package/engine/graphics/ecs/mesh-v2/{sg_compute_hierarchy_bounding_box_by_parent_entity.js → sg_hierarchy_compute_bounding_box_via_parent_entity.js} +1 -1
- package/engine/graphics/ecs/path/PathDisplayEvents.js +3 -2
- package/engine/graphics/ecs/path/PathDisplaySystem.js +5 -0
- package/engine/graphics/ecs/path/highlight/PathDisplayHighlightSystem.d.ts +7 -0
- package/engine/graphics/ecs/path/highlight/PathDisplayHighlightSystem.js +141 -0
- package/engine/graphics/ecs/path/testPathDisplaySystem.js +78 -25
- package/engine/graphics/ecs/path/tube/TubePathStyle.d.ts +12 -0
- package/engine/graphics/ecs/path/tube/TubePathStyle.js +53 -8
- package/engine/graphics/ecs/path/tube/build/TubePathBuilder.js +114 -4
- package/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.js +12 -2
- package/engine/graphics/ecs/path/tube/build/build_geometry_linear.js +13 -2
- package/engine/graphics/ecs/path/tube/build/computeFrenetFrames.js +33 -28
- package/engine/graphics/ecs/path/tube/build/makeTubeGeometry.js +123 -347
- package/engine/graphics/ecs/path/tube/build/make_cap.js +274 -0
- package/engine/graphics/ecs/path/tube/build/make_ring_faces.js +40 -0
- package/engine/graphics/ecs/path/tube/build/make_ring_vertices.js +152 -0
- package/package.json +1 -1
- package/view/View.js +2 -2
- package/view/{compose3x3transform.js → m3_cm_compose_transform.js} +11 -3
- package/view/m3_rm_compose_transform.js +45 -0
- package/view/multiplyMatrices3.js +4 -4
|
@@ -37,19 +37,25 @@ export class TubePathStyle {
|
|
|
37
37
|
*/
|
|
38
38
|
this.width = 1;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
*
|
|
42
|
+
* @type {number[]}
|
|
43
|
+
*/
|
|
44
|
+
this.shape = build_circle_shape(16);
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
*
|
|
44
|
-
* @type {number}
|
|
48
|
+
* @type {number[]|Float32Array|null}
|
|
45
49
|
*/
|
|
46
|
-
this.
|
|
50
|
+
this.shape_normals = null;
|
|
51
|
+
|
|
52
|
+
// TODO expose shading style "flat/smooth"
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
*
|
|
50
56
|
* @type {number}
|
|
51
57
|
*/
|
|
52
|
-
this.
|
|
58
|
+
this.resolution = 10;
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
61
|
*
|
|
@@ -79,6 +85,16 @@ export class TubePathStyle {
|
|
|
79
85
|
this.cap_type = CapType.Flat;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @deprecated
|
|
90
|
+
* @param {number} v
|
|
91
|
+
*/
|
|
92
|
+
set radial_resolution(v) {
|
|
93
|
+
console.warn(".radial_resolution property is deprecated, use .shape directly instead");
|
|
94
|
+
|
|
95
|
+
this.shape = build_circle_shape(v);
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
static fromJSON(j) {
|
|
83
99
|
const r = new TubePathStyle();
|
|
84
100
|
|
|
@@ -93,18 +109,21 @@ export class TubePathStyle {
|
|
|
93
109
|
material,
|
|
94
110
|
opacity = 1,
|
|
95
111
|
width = 1,
|
|
96
|
-
|
|
112
|
+
/**
|
|
113
|
+
* @deprecated
|
|
114
|
+
*/
|
|
97
115
|
radial_resolution = 16,
|
|
116
|
+
resolution = 10,
|
|
98
117
|
cast_shadow = false,
|
|
99
118
|
receive_shadow = false,
|
|
100
119
|
path_mask = [0, 1],
|
|
101
|
-
cap_type = CapType.None
|
|
120
|
+
cap_type = CapType.None,
|
|
121
|
+
shape
|
|
102
122
|
}) {
|
|
103
123
|
assert.enum(material_type, TubeMaterialType, 'material_type');
|
|
104
124
|
assert.isNumber(opacity, 'opacity');
|
|
105
125
|
assert.isNumber(width, 'width');
|
|
106
126
|
assert.isNumber(resolution, 'resolution');
|
|
107
|
-
assert.isNumber(radial_resolution, 'radial_resolution');
|
|
108
127
|
assert.isBoolean(cast_shadow, 'cast_shadow');
|
|
109
128
|
assert.isBoolean(receive_shadow, 'receive_shadow');
|
|
110
129
|
|
|
@@ -115,6 +134,14 @@ export class TubePathStyle {
|
|
|
115
134
|
assert.ok(path_mask.length % 2 === 0, 'path_mask length bust be multiple of 2');
|
|
116
135
|
assert.enum(cap_type, CapType, 'cap_type');
|
|
117
136
|
|
|
137
|
+
if (shape === undefined) {
|
|
138
|
+
assert.isNumber(radial_resolution, 'radial_resolution');
|
|
139
|
+
|
|
140
|
+
shape = build_circle_shape(radial_resolution);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.shape = shape;
|
|
144
|
+
|
|
118
145
|
if (typeof color === 'string') {
|
|
119
146
|
this.color.parse(color);
|
|
120
147
|
} else {
|
|
@@ -125,7 +152,6 @@ export class TubePathStyle {
|
|
|
125
152
|
this.opacity = opacity;
|
|
126
153
|
this.width = width;
|
|
127
154
|
this.resolution = resolution;
|
|
128
|
-
this.radial_resolution = radial_resolution;
|
|
129
155
|
|
|
130
156
|
this.cast_shadow = cast_shadow;
|
|
131
157
|
this.receive_shadow = receive_shadow;
|
|
@@ -144,3 +170,22 @@ export class TubePathStyle {
|
|
|
144
170
|
this.material.fromJSON(material);
|
|
145
171
|
}
|
|
146
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
*
|
|
176
|
+
* @param {number} radial_segments
|
|
177
|
+
* @returns {Float32Array}
|
|
178
|
+
*/
|
|
179
|
+
function build_circle_shape(radial_segments) {
|
|
180
|
+
const shape = new Float32Array(2 * radial_segments);
|
|
181
|
+
for (let i = 0; i < radial_segments; i++) {
|
|
182
|
+
const angle = Math.PI * 2 - Math.PI * 2 * (i / radial_segments);
|
|
183
|
+
|
|
184
|
+
const i2 = i * 2;
|
|
185
|
+
|
|
186
|
+
shape[i2] = Math.cos(angle);
|
|
187
|
+
shape[i2 + 1] = Math.sin(angle);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return shape;
|
|
191
|
+
}
|
|
@@ -8,6 +8,8 @@ import { build_geometry_linear } from "./build_geometry_linear.js";
|
|
|
8
8
|
import { build_geometry_catmullrom } from "./build_geometry_catmullrom.js";
|
|
9
9
|
import { ShadedGeometry } from "../../../mesh-v2/ShadedGeometry.js";
|
|
10
10
|
import { ShadedGeometryFlags } from "../../../mesh-v2/ShadedGeometryFlags.js";
|
|
11
|
+
import { m3_rm_compose_transform } from "../../../../../../view/m3_rm_compose_transform.js";
|
|
12
|
+
import { v2_bearing_angle_towards } from "../../../../../../core/geom/Vector2.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
*
|
|
@@ -16,15 +18,22 @@ import { ShadedGeometryFlags } from "../../../mesh-v2/ShadedGeometryFlags.js";
|
|
|
16
18
|
* @returns {BufferGeometry}
|
|
17
19
|
* @param {Path} path_component
|
|
18
20
|
* @param {TubePathStyle} style
|
|
21
|
+
* @param {number[]|Float32Array} shape_normal
|
|
22
|
+
* @param {number[]|Float32Array} shape
|
|
23
|
+
* @param {number[]|Float32Array} shape_transform
|
|
19
24
|
*/
|
|
20
|
-
function make_geometry_segment(
|
|
25
|
+
function make_geometry_segment(
|
|
26
|
+
path_component, style,
|
|
27
|
+
shape, shape_normal, shape_transform,
|
|
28
|
+
segment_start, segment_end
|
|
29
|
+
) {
|
|
21
30
|
|
|
22
31
|
const interpolation = path_component.interpolation;
|
|
23
32
|
|
|
24
33
|
if (interpolation === InterpolationType.Linear) {
|
|
25
|
-
return build_geometry_linear(path_component, style, segment_start, segment_end);
|
|
34
|
+
return build_geometry_linear(path_component, style, shape, shape_normal, shape_transform, segment_start, segment_end);
|
|
26
35
|
} else if (interpolation === InterpolationType.CatmullRom) {
|
|
27
|
-
return build_geometry_catmullrom(path_component, style, segment_start, segment_end);
|
|
36
|
+
return build_geometry_catmullrom(path_component, style, shape, shape_normal, shape_transform, segment_start, segment_end);
|
|
28
37
|
} else {
|
|
29
38
|
throw new Error(`Unsupported interpolation type '${interpolation}'`);
|
|
30
39
|
}
|
|
@@ -132,13 +141,28 @@ export class TubePathBuilder {
|
|
|
132
141
|
|
|
133
142
|
const segment_count = style.path_mask.length / 2;
|
|
134
143
|
|
|
144
|
+
const shape = fix_shape_normal_order(style.shape);
|
|
145
|
+
|
|
146
|
+
let shape_normals = style.shape_normals;
|
|
147
|
+
if (shape_normals === undefined || shape_normals === null) {
|
|
148
|
+
shape_normals = new Float32Array(shape.length);
|
|
149
|
+
compute_smooth_profile_normals(shape, shape_normals);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const shape_transform = new Float32Array(9);
|
|
153
|
+
m3_rm_compose_transform(shape_transform, 0, 0, style.width, style.width, 0, 0, 0);
|
|
154
|
+
|
|
135
155
|
for (let i = 0; i < segment_count; i++) {
|
|
136
156
|
const i2 = i * 2;
|
|
137
157
|
|
|
138
158
|
const segment_start = style.path_mask[i2];
|
|
139
159
|
const segment_end = style.path_mask[i2 + 1];
|
|
140
160
|
|
|
141
|
-
const geometry = make_geometry_segment(
|
|
161
|
+
const geometry = make_geometry_segment(
|
|
162
|
+
path_component, style,
|
|
163
|
+
shape, shape_normals, shape_transform,
|
|
164
|
+
segment_start, segment_end
|
|
165
|
+
);
|
|
142
166
|
|
|
143
167
|
const entityBuilder = new EntityBuilder();
|
|
144
168
|
|
|
@@ -157,3 +181,89 @@ export class TubePathBuilder {
|
|
|
157
181
|
|
|
158
182
|
}
|
|
159
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
*
|
|
187
|
+
* @param {number[]|Float32Array} shape
|
|
188
|
+
* @param {number[]|Float32Array} normals
|
|
189
|
+
*/
|
|
190
|
+
function compute_smooth_profile_normals(shape, normals) {
|
|
191
|
+
const n = shape.length / 2;
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < n; i++) {
|
|
194
|
+
const i0 = (n + i - 1) % n;
|
|
195
|
+
const i1 = i;
|
|
196
|
+
const i2 = (i + 1) % n;
|
|
197
|
+
|
|
198
|
+
const i0_x = shape[i0 * 2];
|
|
199
|
+
const i0_y = shape[i0 * 2 + 1];
|
|
200
|
+
|
|
201
|
+
const i1_x = shape[i1 * 2];
|
|
202
|
+
const i1_y = shape[i1 * 2 + 1];
|
|
203
|
+
|
|
204
|
+
const i2_x = shape[i2 * 2];
|
|
205
|
+
const i2_y = shape[i2 * 2 + 1];
|
|
206
|
+
|
|
207
|
+
const d0_x = i0_x - i1_x;
|
|
208
|
+
const d0_y = i0_y - i1_y;
|
|
209
|
+
|
|
210
|
+
const d1_x = i1_x - i2_x;
|
|
211
|
+
const d1_y = i1_y - i2_y;
|
|
212
|
+
|
|
213
|
+
const d0_l_inv = 1 / Math.hypot(d0_x, d0_y);
|
|
214
|
+
const d1_l_inv = 1 / Math.hypot(d1_x, d1_y);
|
|
215
|
+
|
|
216
|
+
const nx = d0_x * d0_l_inv + d1_x * d1_l_inv;
|
|
217
|
+
const ny = d0_y * d0_l_inv + d1_y * d1_l_inv;
|
|
218
|
+
|
|
219
|
+
const dn_l_inv = 1 / Math.hypot(nx, ny);
|
|
220
|
+
|
|
221
|
+
// write out normal
|
|
222
|
+
normals[i * 2] = nx * dn_l_inv;
|
|
223
|
+
normals[i * 2 + 1] = ny * dn_l_inv;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Depending on the direction in which shape winds, normals of the generated geometry faces might end up flipped,
|
|
229
|
+
* so we pre-process the shape to avoid that here
|
|
230
|
+
* @param {number[]|Float32Array} shape
|
|
231
|
+
* @returns {Float32Array}
|
|
232
|
+
*/
|
|
233
|
+
function fix_shape_normal_order(shape) {
|
|
234
|
+
const shape_array_length = shape.length;
|
|
235
|
+
|
|
236
|
+
if (shape_array_length <= 2) {
|
|
237
|
+
// less than 2 points in the shape
|
|
238
|
+
return shape;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// compute initial direction
|
|
242
|
+
const x0 = shape[0];
|
|
243
|
+
const y0 = shape[1];
|
|
244
|
+
const x1 = shape[2];
|
|
245
|
+
const y1 = shape[3];
|
|
246
|
+
|
|
247
|
+
const angle = v2_bearing_angle_towards(x0, y0, x1, y1);
|
|
248
|
+
|
|
249
|
+
if (angle > Math.PI) {
|
|
250
|
+
// order is fine
|
|
251
|
+
return shape;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// reverse order
|
|
255
|
+
const reversed = new Float32Array(shape_array_length);
|
|
256
|
+
|
|
257
|
+
const n = shape_array_length / 2;
|
|
258
|
+
|
|
259
|
+
for (let i = 0; i < n; i++) {
|
|
260
|
+
const i2 = i * 2;
|
|
261
|
+
|
|
262
|
+
const j2 = (n - (i + 1)) * 2;
|
|
263
|
+
|
|
264
|
+
reversed[i2] = shape[j2];
|
|
265
|
+
reversed[i2 + 1] = shape[j2 + 1];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return reversed;
|
|
269
|
+
}
|
|
@@ -6,11 +6,18 @@ import Vector3 from "../../../../../../core/geom/Vector3.js";
|
|
|
6
6
|
*
|
|
7
7
|
* @param {Path} path
|
|
8
8
|
* @param {TubePathStyle} style
|
|
9
|
+
* @param {number[]} shape
|
|
10
|
+
* @param {number[]|Float32Array} shape_normal
|
|
11
|
+
* @param {number[]} shape_transform
|
|
9
12
|
* @param {number} segment_start
|
|
10
13
|
* @param {number} segment_end
|
|
11
14
|
* @return {THREE.BufferGeometry}
|
|
12
15
|
*/
|
|
13
|
-
export function build_geometry_catmullrom(
|
|
16
|
+
export function build_geometry_catmullrom(
|
|
17
|
+
path, style,
|
|
18
|
+
shape, shape_normal, shape_transform,
|
|
19
|
+
segment_start, segment_end
|
|
20
|
+
) {
|
|
14
21
|
|
|
15
22
|
const point_count = path.getPointCount();
|
|
16
23
|
|
|
@@ -58,5 +65,8 @@ export function build_geometry_catmullrom(path, style, segment_start, segment_en
|
|
|
58
65
|
|
|
59
66
|
const frames = computeFrenetFrames(points_f32, false);
|
|
60
67
|
|
|
61
|
-
return makeTubeGeometry(
|
|
68
|
+
return makeTubeGeometry(
|
|
69
|
+
points_f32, frames.normals, frames.binormals, frames.tangents,
|
|
70
|
+
shape, shape_normal, shape.length / 2, shape_transform, false, style.cap_type
|
|
71
|
+
);
|
|
62
72
|
}
|
|
@@ -12,11 +12,18 @@ const scratch_v3 = new Vector3();
|
|
|
12
12
|
*
|
|
13
13
|
* @param {Path} path
|
|
14
14
|
* @param {TubePathStyle} style
|
|
15
|
+
* @param {number[]} shape
|
|
16
|
+
* @param {number[]|Float32Array} shape_normal
|
|
17
|
+
* @param {number[]} shape_transform
|
|
15
18
|
* @param {number} segment_start
|
|
16
19
|
* @param {number} segment_end
|
|
17
20
|
* @return {THREE.BufferGeometry}
|
|
18
21
|
*/
|
|
19
|
-
export function build_geometry_linear(
|
|
22
|
+
export function build_geometry_linear(
|
|
23
|
+
path, style,
|
|
24
|
+
shape, shape_normal, shape_transform,
|
|
25
|
+
segment_start, segment_end
|
|
26
|
+
) {
|
|
20
27
|
const points = [];
|
|
21
28
|
|
|
22
29
|
const path_length = path.length;
|
|
@@ -52,5 +59,9 @@ export function build_geometry_linear(path, style, segment_start, segment_end) {
|
|
|
52
59
|
|
|
53
60
|
const frames = computeFrenetFrames(points, false);
|
|
54
61
|
|
|
55
|
-
return makeTubeGeometry(
|
|
62
|
+
return makeTubeGeometry(
|
|
63
|
+
points, frames.normals, frames.binormals, frames.tangents,
|
|
64
|
+
shape, shape_normal, shape.length / 2, shape_transform,
|
|
65
|
+
false, style.cap_type
|
|
66
|
+
);
|
|
56
67
|
}
|
|
@@ -9,6 +9,7 @@ import { array_copy } from "../../../../../../core/collection/array/copyArray.js
|
|
|
9
9
|
* @see https://github.com/mrdoob/three.js/blob/c12c9a166a1369cdd58622fff2aff7e3a84305d7/src/extras/core/Curve.js#L260
|
|
10
10
|
* @param {Float32Array|number[]} points
|
|
11
11
|
* @param {boolean} [closed]
|
|
12
|
+
* @returns {{normals:Vector3[], binormals:Vector3[], tangents:Vector3[]}}
|
|
12
13
|
*/
|
|
13
14
|
export function computeFrenetFrames(points, closed = false) {
|
|
14
15
|
// see http://www.cs.indiana.edu/pub/techreports/TR425.pdf
|
|
@@ -32,12 +33,10 @@ export function computeFrenetFrames(points, closed = false) {
|
|
|
32
33
|
const pR_a = [];
|
|
33
34
|
const p1_a = [];
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*/
|
|
40
|
-
function getTangent(i, destination) {
|
|
36
|
+
// compute the tangent vectors for each segment on the curve
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i <= segments; i++) {
|
|
39
|
+
|
|
41
40
|
// get points on either side
|
|
42
41
|
const i0 = max2(i - 1, 0);
|
|
43
42
|
const i1 = min2(i + 1, segments);
|
|
@@ -54,22 +53,27 @@ export function computeFrenetFrames(points, closed = false) {
|
|
|
54
53
|
vec3.sub(v3_1, p1_a, pR_a);
|
|
55
54
|
vec3.normalize(v3_1, v3_1);
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
v3_1[1] - v3_0[1],
|
|
60
|
-
v3_1[2] - v3_0[2],
|
|
61
|
-
);
|
|
62
|
-
destination.normalize();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// compute the tangent vectors for each segment on the curve
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i <= segments; i++) {
|
|
56
|
+
vec3.sub(v3_1, v3_1, v3_0);
|
|
57
|
+
vec3.normalize(v3_1, v3_1);
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
// write out
|
|
60
|
+
const tangent_x = v3_1[0];
|
|
61
|
+
const tangent_y = v3_1[1];
|
|
62
|
+
const tangent_z = v3_1[2];
|
|
63
|
+
|
|
64
|
+
if (tangent_x === 0 && tangent_y === 0 && tangent_z === 0) {
|
|
65
|
+
// no tangent, copy previous one
|
|
66
|
+
if (i > 0) {
|
|
67
|
+
tangents[i] = tangents[i - 1];
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
71
|
|
|
72
|
-
tangents[i] =
|
|
72
|
+
tangents[i] = new Vector3(
|
|
73
|
+
tangent_x,
|
|
74
|
+
tangent_y,
|
|
75
|
+
tangent_z
|
|
76
|
+
);
|
|
73
77
|
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -83,13 +87,6 @@ export function computeFrenetFrames(points, closed = false) {
|
|
|
83
87
|
const ty = Math.abs(tangents[0].y);
|
|
84
88
|
const tz = Math.abs(tangents[0].z);
|
|
85
89
|
|
|
86
|
-
if (tx <= min) {
|
|
87
|
-
|
|
88
|
-
min = tx;
|
|
89
|
-
normal.set(1, 0, 0);
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
90
|
if (ty <= min) {
|
|
94
91
|
|
|
95
92
|
min = ty;
|
|
@@ -97,12 +94,20 @@ export function computeFrenetFrames(points, closed = false) {
|
|
|
97
94
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
if (tz
|
|
97
|
+
if (tz < min) {
|
|
98
|
+
min = tz;
|
|
101
99
|
|
|
102
100
|
normal.set(0, 0, 1);
|
|
103
101
|
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
if (tx < min) {
|
|
105
|
+
|
|
106
|
+
min = tx;
|
|
107
|
+
normal.set(1, 0, 0);
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
vec.crossVectors(tangents[0], normal).normalize();
|
|
107
112
|
|
|
108
113
|
normals[0].crossVectors(tangents[0], vec);
|