@adatechnology/game-vehicles 0.5.0 → 0.6.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/dist/index.d.ts +66 -3
- package/dist/index.global.js +947 -105
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +829 -52
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -892,7 +892,7 @@ function advance(geo, ti, deadline) {
|
|
|
892
892
|
}
|
|
893
893
|
return ti.builtBlocks === total;
|
|
894
894
|
}
|
|
895
|
-
function start(geo) {
|
|
895
|
+
function start(geo, store = cache) {
|
|
896
896
|
const pos = geo.attributes.position;
|
|
897
897
|
const triangles = trianglesOf(geo);
|
|
898
898
|
const ti = {
|
|
@@ -905,7 +905,7 @@ function start(geo) {
|
|
|
905
905
|
blocks: new Float64Array(blockCount(triangles) * 6),
|
|
906
906
|
builtBlocks: 0
|
|
907
907
|
};
|
|
908
|
-
|
|
908
|
+
store.set(geo, ti);
|
|
909
909
|
return ti;
|
|
910
910
|
}
|
|
911
911
|
function triIndexOf(geo) {
|
|
@@ -957,6 +957,106 @@ function boundsFromIndex(geo) {
|
|
|
957
957
|
}
|
|
958
958
|
return { box, version: ti.positionVersion };
|
|
959
959
|
}
|
|
960
|
+
var rayCache = /* @__PURE__ */ new WeakMap();
|
|
961
|
+
function rayIndexOf(geo) {
|
|
962
|
+
const shared = cache.get(geo);
|
|
963
|
+
if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
|
|
964
|
+
let ti = rayCache.get(geo);
|
|
965
|
+
if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
|
|
966
|
+
advance(geo, ti, Infinity);
|
|
967
|
+
return ti;
|
|
968
|
+
}
|
|
969
|
+
var BLOCK_SLACK = 1e-6;
|
|
970
|
+
var scratch = /* @__PURE__ */ new WeakMap();
|
|
971
|
+
function scratchOf(THREE) {
|
|
972
|
+
let s = scratch.get(THREE);
|
|
973
|
+
if (!s) {
|
|
974
|
+
s = {
|
|
975
|
+
inverse: new THREE.Matrix4(),
|
|
976
|
+
local: new THREE.Ray(),
|
|
977
|
+
box: new THREE.Box3(),
|
|
978
|
+
a: new THREE.Vector3(),
|
|
979
|
+
b: new THREE.Vector3(),
|
|
980
|
+
c: new THREE.Vector3(),
|
|
981
|
+
point: new THREE.Vector3(),
|
|
982
|
+
world: new THREE.Vector3(),
|
|
983
|
+
sphere: new THREE.Sphere()
|
|
984
|
+
};
|
|
985
|
+
scratch.set(THREE, s);
|
|
986
|
+
}
|
|
987
|
+
return s;
|
|
988
|
+
}
|
|
989
|
+
function indexable(THREE, mesh) {
|
|
990
|
+
const geo = mesh.geometry;
|
|
991
|
+
if (mesh.raycast !== THREE.Mesh.prototype.raycast) return false;
|
|
992
|
+
if (!mesh.isMesh || mesh.isSkinnedMesh || mesh.isInstancedMesh || mesh.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
|
|
993
|
+
if (!mesh.material || Array.isArray(mesh.material)) return false;
|
|
994
|
+
if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
|
|
995
|
+
return supports(geo);
|
|
996
|
+
}
|
|
997
|
+
function raycastMesh(THREE, raycaster, mesh, out) {
|
|
998
|
+
const geo = mesh.geometry;
|
|
999
|
+
const s = scratchOf(THREE);
|
|
1000
|
+
if (geo.boundingSphere === null) geo.computeBoundingSphere();
|
|
1001
|
+
s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh.matrixWorld);
|
|
1002
|
+
if (!raycaster.ray.intersectsSphere(s.sphere)) return;
|
|
1003
|
+
const ti = rayIndexOf(geo);
|
|
1004
|
+
if (!ti) return;
|
|
1005
|
+
s.inverse.copy(mesh.matrixWorld).invert();
|
|
1006
|
+
s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
|
|
1007
|
+
const material = mesh.material;
|
|
1008
|
+
const back = material.side === THREE.BackSide;
|
|
1009
|
+
const cull = material.side === THREE.FrontSide;
|
|
1010
|
+
const P = geo.attributes.position;
|
|
1011
|
+
const ix = geo.index;
|
|
1012
|
+
const count = ix ? ix.count : P.count;
|
|
1013
|
+
const first = Math.max(0, geo.drawRange.start);
|
|
1014
|
+
const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
|
|
1015
|
+
const t0 = Math.ceil(first / 3);
|
|
1016
|
+
const t1 = Math.floor(last / 3);
|
|
1017
|
+
const B = ti.boxes;
|
|
1018
|
+
const K = ti.blocks;
|
|
1019
|
+
for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
|
|
1020
|
+
const o = blk * 6;
|
|
1021
|
+
s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
|
|
1022
|
+
s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
|
|
1023
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
1024
|
+
const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
|
|
1025
|
+
for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
|
|
1026
|
+
const j = t * 6;
|
|
1027
|
+
s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
|
|
1028
|
+
s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
|
|
1029
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
1030
|
+
const a = ix ? ix.getX(t * 3) : t * 3;
|
|
1031
|
+
const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
|
|
1032
|
+
const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
|
|
1033
|
+
s.a.fromBufferAttribute(P, a);
|
|
1034
|
+
s.b.fromBufferAttribute(P, b);
|
|
1035
|
+
s.c.fromBufferAttribute(P, c);
|
|
1036
|
+
const hit = back ? s.local.intersectTriangle(s.c, s.b, s.a, true, s.point) : s.local.intersectTriangle(s.a, s.b, s.c, cull, s.point);
|
|
1037
|
+
if (hit === null) continue;
|
|
1038
|
+
s.world.copy(s.point).applyMatrix4(mesh.matrixWorld);
|
|
1039
|
+
const distance = raycaster.ray.origin.distanceTo(s.world);
|
|
1040
|
+
if (distance < raycaster.near || distance > raycaster.far) continue;
|
|
1041
|
+
const normal = new THREE.Vector3();
|
|
1042
|
+
THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
|
|
1043
|
+
out.push({ distance, point: s.world.clone(), object: mesh, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function collect(THREE, raycaster, object, recursive, out) {
|
|
1048
|
+
if (object.layers.test(raycaster.layers)) {
|
|
1049
|
+
if (indexable(THREE, object)) raycastMesh(THREE, raycaster, object, out);
|
|
1050
|
+
else object.raycast(raycaster, out);
|
|
1051
|
+
}
|
|
1052
|
+
if (recursive) for (const child of object.children) collect(THREE, raycaster, child, true, out);
|
|
1053
|
+
}
|
|
1054
|
+
function raycastIndexed(THREE, raycaster, objects, recursive = false) {
|
|
1055
|
+
const out = [];
|
|
1056
|
+
for (const object of objects) collect(THREE, raycaster, object, recursive, out);
|
|
1057
|
+
out.sort((x, y) => x.distance - y.distance);
|
|
1058
|
+
return out;
|
|
1059
|
+
}
|
|
960
1060
|
|
|
961
1061
|
// src/geometry.ts
|
|
962
1062
|
function installGeometry(kit) {
|
|
@@ -2290,6 +2390,7 @@ function installBody(kit) {
|
|
|
2290
2390
|
}
|
|
2291
2391
|
kit.profX = profX;
|
|
2292
2392
|
const _sr = new THREE.Raycaster();
|
|
2393
|
+
_sr.layers.enableAll();
|
|
2293
2394
|
kit.surfaceAt = function(c, org, dir, o) {
|
|
2294
2395
|
o = o || {};
|
|
2295
2396
|
c.root.updateMatrixWorld(true);
|
|
@@ -2306,7 +2407,7 @@ function installBody(kit) {
|
|
|
2306
2407
|
_sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
2307
2408
|
_sr.near = 0;
|
|
2308
2409
|
_sr.far = o.max || 20;
|
|
2309
|
-
const h =
|
|
2410
|
+
const h = raycastIndexed(THREE, _sr, cands)[0];
|
|
2310
2411
|
if (!h) return null;
|
|
2311
2412
|
const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
|
|
2312
2413
|
if (n.dot(d) > 0) n.negate();
|
|
@@ -2328,6 +2429,7 @@ function installGlass(kit) {
|
|
|
2328
2429
|
const V3 = THREE.Vector3;
|
|
2329
2430
|
const I = kit._;
|
|
2330
2431
|
const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
|
|
2432
|
+
I.paneSeals = /* @__PURE__ */ new WeakMap();
|
|
2331
2433
|
kit.addGlass = function(c, geo, label, type, o) {
|
|
2332
2434
|
o = o || {};
|
|
2333
2435
|
if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
|
|
@@ -2394,7 +2496,7 @@ function installGlass(kit) {
|
|
|
2394
2496
|
cy /= poly.length;
|
|
2395
2497
|
const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
|
|
2396
2498
|
const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
|
|
2397
|
-
kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
2499
|
+
const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
2398
2500
|
mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
|
|
2399
2501
|
pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
|
|
2400
2502
|
outline: shp.getSpacedPoints(56),
|
|
@@ -2410,6 +2512,7 @@ function installGlass(kit) {
|
|
|
2410
2512
|
pts.pop();
|
|
2411
2513
|
const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
|
|
2412
2514
|
seal.castShadow = false;
|
|
2515
|
+
I.paneSeals.set(pane, seal);
|
|
2413
2516
|
});
|
|
2414
2517
|
});
|
|
2415
2518
|
};
|
|
@@ -4119,6 +4222,47 @@ function installWheels(kit) {
|
|
|
4119
4222
|
Object.assign(I, {});
|
|
4120
4223
|
}
|
|
4121
4224
|
|
|
4225
|
+
// src/particles.ts
|
|
4226
|
+
import { warnOnce } from "@adatechnology/game-contracts";
|
|
4227
|
+
|
|
4228
|
+
// src/routed-particles.ts
|
|
4229
|
+
var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
|
|
4230
|
+
var PUFF_LOOKS = {
|
|
4231
|
+
smokewhite: [10133927, 0.42],
|
|
4232
|
+
smokeblack: [1776412, 0.62],
|
|
4233
|
+
smokegray: [5593436, 0.5],
|
|
4234
|
+
smokesteam: [11975873, 0.3],
|
|
4235
|
+
dust: [7235160, 0.34],
|
|
4236
|
+
fire: [16777215, 0.9]
|
|
4237
|
+
};
|
|
4238
|
+
var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
|
|
4239
|
+
var FIRE_COLOR = 16753968;
|
|
4240
|
+
var SMOKE_START_SCALE = 0.18;
|
|
4241
|
+
function routedParticleRequest(input) {
|
|
4242
|
+
const { type } = input;
|
|
4243
|
+
const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
|
|
4244
|
+
const life = input.life ?? LIFE[type] ?? 99;
|
|
4245
|
+
const size = (input.size ?? 1) * (input.grow ?? 1);
|
|
4246
|
+
const isFire = type === "fire";
|
|
4247
|
+
const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
|
|
4248
|
+
return {
|
|
4249
|
+
effect: ROUTED_PARTICLE_TYPES[type],
|
|
4250
|
+
point: input.position,
|
|
4251
|
+
values: {
|
|
4252
|
+
vx: input.velocity[0],
|
|
4253
|
+
vy: input.velocity[1],
|
|
4254
|
+
vz: input.velocity[2],
|
|
4255
|
+
life,
|
|
4256
|
+
size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
|
|
4257
|
+
grow: growth,
|
|
4258
|
+
color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
|
|
4259
|
+
alpha: input.alpha ?? look[1],
|
|
4260
|
+
gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
|
|
4261
|
+
drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
|
|
4262
|
+
}
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4122
4266
|
// src/particles.ts
|
|
4123
4267
|
function installParticles(kit) {
|
|
4124
4268
|
const THREE = kit.THREE;
|
|
@@ -4210,7 +4354,7 @@ function installParticles(kit) {
|
|
|
4210
4354
|
});
|
|
4211
4355
|
}
|
|
4212
4356
|
const list = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
|
|
4213
|
-
const
|
|
4357
|
+
const build = () => new THREE.SpriteMaterial({
|
|
4214
4358
|
map: list[Math.floor(Math.random() * list.length)],
|
|
4215
4359
|
color: new THREE.Color(col),
|
|
4216
4360
|
transparent: true,
|
|
@@ -4218,6 +4362,7 @@ function installParticles(kit) {
|
|
|
4218
4362
|
opacity: op,
|
|
4219
4363
|
blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
|
|
4220
4364
|
});
|
|
4365
|
+
const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
|
|
4221
4366
|
const dis = { value: 0 };
|
|
4222
4367
|
mat.userData.dis = dis;
|
|
4223
4368
|
mat.onBeforeCompile = function(sh) {
|
|
@@ -4245,11 +4390,37 @@ function installParticles(kit) {
|
|
|
4245
4390
|
function killParticle(c, i) {
|
|
4246
4391
|
const P = c.particles[i];
|
|
4247
4392
|
c.effects.remove(P.m);
|
|
4248
|
-
if (P.own)
|
|
4393
|
+
if (P.own) {
|
|
4394
|
+
if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
|
|
4395
|
+
else P.m.material.dispose();
|
|
4396
|
+
}
|
|
4249
4397
|
c.particles.splice(i, 1);
|
|
4250
4398
|
}
|
|
4399
|
+
function routeParticle(c, effects, type, pos, vel, o) {
|
|
4400
|
+
c.effects.updateWorldMatrix(true, false);
|
|
4401
|
+
const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
|
|
4402
|
+
const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
|
|
4403
|
+
const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
|
|
4404
|
+
effects.play(
|
|
4405
|
+
routedParticleRequest({
|
|
4406
|
+
type,
|
|
4407
|
+
position: [at.x, at.y, at.z],
|
|
4408
|
+
velocity: [to.x, to.y, to.z],
|
|
4409
|
+
...o.variant ? { variant: o.variant } : {},
|
|
4410
|
+
...colour !== void 0 ? { color: colour } : {},
|
|
4411
|
+
...o.op != null ? { alpha: o.op } : {},
|
|
4412
|
+
...o.s ? { size: o.s } : {},
|
|
4413
|
+
...o.life ? { life: o.life } : {},
|
|
4414
|
+
...o.grow ? { grow: o.grow } : {}
|
|
4415
|
+
})
|
|
4416
|
+
);
|
|
4417
|
+
return null;
|
|
4418
|
+
}
|
|
4251
4419
|
kit.spawn = function(c, type, pos, vel, o) {
|
|
4252
4420
|
o = o || {};
|
|
4421
|
+
const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
|
|
4422
|
+
if (effects) return routeParticle(c, effects, type, pos, vel, o);
|
|
4423
|
+
if (ROUTED_PARTICLE_TYPES[type] && !kit.effects) warnOnce("game-vehicles.own-particles", "game-vehicles drew its smoke, fire and dust with sprites of its own because the host lent no effects world (options.effects); pass the host effects so the puffs share one budget");
|
|
4253
4424
|
if (c.particles.length >= PARTICLE_LIMIT) {
|
|
4254
4425
|
const idx = c.particles.findIndex(function(q) {
|
|
4255
4426
|
return TRANSIENT_TYPES[q.type];
|
|
@@ -4521,6 +4692,7 @@ function installFluids(kit) {
|
|
|
4521
4692
|
};
|
|
4522
4693
|
kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
|
|
4523
4694
|
const _fr = new THREE.Raycaster();
|
|
4695
|
+
_fr.layers.enableAll();
|
|
4524
4696
|
kit.splat = function(c, p, n, r, o) {
|
|
4525
4697
|
o = o || {};
|
|
4526
4698
|
n = n.clone().normalize();
|
|
@@ -4962,31 +5134,55 @@ function installFluids(kit) {
|
|
|
4962
5134
|
}
|
|
4963
5135
|
|
|
4964
5136
|
// src/glass-damage.ts
|
|
5137
|
+
function crackedGlassMaterial(THREE, texture) {
|
|
5138
|
+
return new THREE.MeshStandardMaterial({
|
|
5139
|
+
color: 16777215,
|
|
5140
|
+
map: texture,
|
|
5141
|
+
emissive: 6976118,
|
|
5142
|
+
emissiveMap: texture,
|
|
5143
|
+
transparent: true,
|
|
5144
|
+
roughness: 0.25,
|
|
5145
|
+
metalness: 0.1,
|
|
5146
|
+
side: THREE.DoubleSide,
|
|
5147
|
+
depthWrite: false
|
|
5148
|
+
});
|
|
5149
|
+
}
|
|
4965
5150
|
function installGlassDamage(kit) {
|
|
4966
5151
|
const THREE = kit.THREE;
|
|
4967
5152
|
const V3 = THREE.Vector3;
|
|
4968
5153
|
const PI = Math.PI;
|
|
4969
5154
|
const I = kit._;
|
|
4970
5155
|
const document2 = kit.document;
|
|
4971
|
-
const {
|
|
4972
|
-
function glassUV(msh, pLocal) {
|
|
5156
|
+
const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
|
|
5157
|
+
function glassUV(c, msh, pLocal) {
|
|
4973
5158
|
const pos = msh.geometry.attributes.position;
|
|
4974
5159
|
const uv = msh.geometry.attributes.uv;
|
|
4975
5160
|
if (!uv) return [0.5, 0.5];
|
|
4976
|
-
|
|
4977
|
-
const q = pLocal.clone()
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5161
|
+
c.root.updateMatrixWorld(true);
|
|
5162
|
+
const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
|
|
5163
|
+
const idx = msh.geometry.index;
|
|
5164
|
+
const tri = new THREE.Triangle();
|
|
5165
|
+
const near = new V3();
|
|
5166
|
+
const bary = new V3();
|
|
5167
|
+
let best = Infinity;
|
|
5168
|
+
const out = [0.5, 0.5];
|
|
5169
|
+
const count = idx ? idx.count : pos.count;
|
|
5170
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
5171
|
+
const ia = idx ? idx.getX(f) : f;
|
|
5172
|
+
const ib = idx ? idx.getX(f + 1) : f + 1;
|
|
5173
|
+
const ic = idx ? idx.getX(f + 2) : f + 2;
|
|
5174
|
+
tri.a.fromBufferAttribute(pos, ia);
|
|
5175
|
+
tri.b.fromBufferAttribute(pos, ib);
|
|
5176
|
+
tri.c.fromBufferAttribute(pos, ic);
|
|
5177
|
+
tri.closestPointToPoint(q, near);
|
|
5178
|
+
const d = near.distanceToSquared(q);
|
|
5179
|
+
if (d >= best) continue;
|
|
5180
|
+
best = d;
|
|
5181
|
+
tri.getBarycoord(near, bary);
|
|
5182
|
+
out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
|
|
5183
|
+
out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
|
|
4988
5184
|
}
|
|
4989
|
-
return
|
|
5185
|
+
return out;
|
|
4990
5186
|
}
|
|
4991
5187
|
function drawWeb(x, cx, cy, s, ax, rnd) {
|
|
4992
5188
|
const n = 16 + Math.floor(rnd() * 8);
|
|
@@ -5063,19 +5259,9 @@ function installGlassDamage(kit) {
|
|
|
5063
5259
|
x0.fillStyle = "rgba(48,66,72,0.5)";
|
|
5064
5260
|
x0.fillRect(0, 0, 256, 256);
|
|
5065
5261
|
G.tex = new THREE.CanvasTexture(G.cv);
|
|
5066
|
-
msh.material =
|
|
5067
|
-
color: 16777215,
|
|
5068
|
-
map: G.tex,
|
|
5069
|
-
emissive: 6976118,
|
|
5070
|
-
emissiveMap: G.tex,
|
|
5071
|
-
transparent: true,
|
|
5072
|
-
roughness: 0.25,
|
|
5073
|
-
metalness: 0.1,
|
|
5074
|
-
side: DOUBLE_SIDE,
|
|
5075
|
-
depthWrite: false
|
|
5076
|
-
});
|
|
5262
|
+
msh.material = crackedGlassMaterial(THREE, G.tex);
|
|
5077
5263
|
}
|
|
5078
|
-
const uv = glassUV(msh, p);
|
|
5264
|
+
const uv = glassUV(c, msh, p);
|
|
5079
5265
|
const x = G.cv.getContext("2d");
|
|
5080
5266
|
drawWeb(x, uv[0] * 256, (1 - uv[1]) * 256, sc != null ? sc : 0.6 + force * 0.5, Math.max(0.4, G.aspect || 1), c.rand);
|
|
5081
5267
|
G.tex.needsUpdate = true;
|
|
@@ -5220,6 +5406,13 @@ function installGlassDamage(kit) {
|
|
|
5220
5406
|
}
|
|
5221
5407
|
const seatGeo = [];
|
|
5222
5408
|
const d = dir.clone().normalize();
|
|
5409
|
+
if (msh.parent && msh.parent !== c.vehicle) {
|
|
5410
|
+
c.root.updateMatrixWorld(true);
|
|
5411
|
+
const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
|
|
5412
|
+
cubesAt.forEach(function(q) {
|
|
5413
|
+
q.applyMatrix4(toVehicle);
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5223
5416
|
cubesAt.forEach(function(p, i) {
|
|
5224
5417
|
if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
|
|
5225
5418
|
let best = null;
|
|
@@ -5422,6 +5615,7 @@ function installImpact(kit) {
|
|
|
5422
5615
|
polygonOffsetUnits: -2
|
|
5423
5616
|
});
|
|
5424
5617
|
const _ray = new THREE.Raycaster();
|
|
5618
|
+
_ray.layers.enableAll();
|
|
5425
5619
|
function findSurface(c, p, dir) {
|
|
5426
5620
|
c.root.updateMatrixWorld(true);
|
|
5427
5621
|
const Mw = c.vehicle.matrixWorld;
|
|
@@ -5503,6 +5697,7 @@ function installImpact(kit) {
|
|
|
5503
5697
|
const v0 = new V3();
|
|
5504
5698
|
const w = new V3();
|
|
5505
5699
|
const sc = new V3();
|
|
5700
|
+
const local = new V3();
|
|
5506
5701
|
const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
|
|
5507
5702
|
c.deform.forEach(function(msh) {
|
|
5508
5703
|
if (!msh.parent || !msh.visible) return;
|
|
@@ -5517,7 +5712,15 @@ function installImpact(kit) {
|
|
|
5517
5712
|
irel.copy(rel).invert();
|
|
5518
5713
|
if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
|
|
5519
5714
|
const p0 = g.userData.p0;
|
|
5715
|
+
local.copy(p).applyMatrix4(irel);
|
|
5716
|
+
const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
|
|
5717
|
+
const reach2 = reach * reach;
|
|
5718
|
+
const arr = pos.array;
|
|
5520
5719
|
for (let i = 0; i < pos.count; i++) {
|
|
5720
|
+
const dx = arr[3 * i] - local.x;
|
|
5721
|
+
const dy = arr[3 * i + 1] - local.y;
|
|
5722
|
+
const dz = arr[3 * i + 2] - local.z;
|
|
5723
|
+
if (dx * dx + dy * dy + dz * dz >= reach2) continue;
|
|
5521
5724
|
v.fromBufferAttribute(pos, i).applyMatrix4(rel);
|
|
5522
5725
|
const a = dentField(v, H);
|
|
5523
5726
|
if (a <= 0) continue;
|
|
@@ -5533,10 +5736,15 @@ function installImpact(kit) {
|
|
|
5533
5736
|
}
|
|
5534
5737
|
if (changed) {
|
|
5535
5738
|
pos.needsUpdate = true;
|
|
5536
|
-
if (g.userData.crease)
|
|
5739
|
+
if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
|
|
5740
|
+
else if (g.userData.crease) crease(g, 42);
|
|
5537
5741
|
else g.computeVertexNormals();
|
|
5538
|
-
g.computeBoundingSphere();
|
|
5539
5742
|
g.computeBoundingBox();
|
|
5743
|
+
const sphere = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
|
|
5744
|
+
g.boundingBox.getCenter(sphere.center);
|
|
5745
|
+
let far2 = 0;
|
|
5746
|
+
for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
|
|
5747
|
+
sphere.radius = Math.sqrt(far2);
|
|
5540
5748
|
}
|
|
5541
5749
|
});
|
|
5542
5750
|
const dset = new Set(c.deform);
|
|
@@ -5620,13 +5828,14 @@ function installImpact(kit) {
|
|
|
5620
5828
|
kit.impact = impact;
|
|
5621
5829
|
kit.partEffects = {};
|
|
5622
5830
|
const _pr = new THREE.Raycaster();
|
|
5831
|
+
_pr.layers.enableAll();
|
|
5623
5832
|
function probe(c, o, d, far) {
|
|
5624
5833
|
c.root.updateMatrixWorld(true);
|
|
5625
5834
|
const Mw = c.vehicle.matrixWorld;
|
|
5626
5835
|
const inv = new THREE.Matrix4().copy(Mw).invert();
|
|
5627
5836
|
_pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
5628
5837
|
_pr.far = far || 10;
|
|
5629
|
-
const hits = _pr
|
|
5838
|
+
const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
|
|
5630
5839
|
for (let i = 0; i < hits.length; i++) {
|
|
5631
5840
|
const h = hits[i];
|
|
5632
5841
|
const ob = h.object;
|
|
@@ -6992,6 +7201,7 @@ function installZoneDecals(kit) {
|
|
|
6992
7201
|
return hole;
|
|
6993
7202
|
}
|
|
6994
7203
|
const _zr = new THREE.Raycaster();
|
|
7204
|
+
_zr.layers.enableAll();
|
|
6995
7205
|
const PROXY_MIN_TRIANGLES = 500;
|
|
6996
7206
|
const STROKE_REACH = 0.6;
|
|
6997
7207
|
const _pv = new V3();
|
|
@@ -7226,6 +7436,7 @@ function installZoneDecals(kit) {
|
|
|
7226
7436
|
return out;
|
|
7227
7437
|
}
|
|
7228
7438
|
const _rt = new THREE.Raycaster();
|
|
7439
|
+
_rt.layers.enableAll();
|
|
7229
7440
|
function scratchPath(c, s, len, ang, cands, B) {
|
|
7230
7441
|
const n = s.n.clone().normalize();
|
|
7231
7442
|
const t = Math.abs(n.y) < 0.9 ? new V3().crossVectors(new V3(0, 1, 0), n) : new V3().crossVectors(new V3(1, 0, 0), n);
|
|
@@ -10507,6 +10718,8 @@ function createVehicleKit(options) {
|
|
|
10507
10718
|
V3: THREE.Vector3,
|
|
10508
10719
|
PI: Math.PI,
|
|
10509
10720
|
document: doc,
|
|
10721
|
+
particleMaterials: opts.particleMaterials,
|
|
10722
|
+
effects: opts.effects,
|
|
10510
10723
|
strings,
|
|
10511
10724
|
t,
|
|
10512
10725
|
matchers: createMatchers(strings),
|
|
@@ -10762,7 +10975,9 @@ function createVehicleKit(options) {
|
|
|
10762
10975
|
assignDeep(strings, resolveStrings(opts.locale));
|
|
10763
10976
|
return kit;
|
|
10764
10977
|
};
|
|
10765
|
-
const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
|
|
10978
|
+
const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
|
|
10979
|
+
void _particleMaterials;
|
|
10980
|
+
void _effects;
|
|
10766
10981
|
void _three;
|
|
10767
10982
|
void _lod;
|
|
10768
10983
|
void _locale;
|
|
@@ -10861,6 +11076,14 @@ var floorPan = (b) => ({
|
|
|
10861
11076
|
center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
|
|
10862
11077
|
size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
|
|
10863
11078
|
});
|
|
11079
|
+
function paneBox(c, pane, inverse) {
|
|
11080
|
+
pane.geometry.computeBoundingBox();
|
|
11081
|
+
const box = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
|
|
11082
|
+
box.applyMatrix4(inverse);
|
|
11083
|
+
const center = [(box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2];
|
|
11084
|
+
const size = [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
|
|
11085
|
+
return { center, size };
|
|
11086
|
+
}
|
|
10864
11087
|
function buildVehicleModel(kit, c, options = {}) {
|
|
10865
11088
|
const kind = kindOf(c);
|
|
10866
11089
|
const m = c.meta;
|
|
@@ -11001,6 +11224,16 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
11001
11224
|
tags: [role, ...tags]
|
|
11002
11225
|
});
|
|
11003
11226
|
}
|
|
11227
|
+
const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
|
|
11228
|
+
c.root.updateMatrixWorld(true);
|
|
11229
|
+
const inverse = c.vehicle.matrixWorld.clone().invert();
|
|
11230
|
+
for (const pane of c.glass ?? []) {
|
|
11231
|
+
if (claimed.has(pane)) continue;
|
|
11232
|
+
const G = pane.userData.glass;
|
|
11233
|
+
const bb = paneBox(c, pane, inverse);
|
|
11234
|
+
const laminated = G?.type === "laminated" || m.bullet;
|
|
11235
|
+
add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
|
|
11236
|
+
}
|
|
11004
11237
|
if (!hubZone) {
|
|
11005
11238
|
const h = Math.max(0.15, belt - clearance);
|
|
11006
11239
|
add({
|
|
@@ -11336,6 +11569,76 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
11336
11569
|
}
|
|
11337
11570
|
|
|
11338
11571
|
// src/destructible/visual.ts
|
|
11572
|
+
var HOLE_DECAL_SCALE = 2.4;
|
|
11573
|
+
var DOOR_DEPTH = 0.07;
|
|
11574
|
+
var DOOR_EDGE_PIECES = 16;
|
|
11575
|
+
var DOOR_CARD_COLOR = "#3b3e42";
|
|
11576
|
+
var DOOR_LINER_COLOR = "#5a5e64";
|
|
11577
|
+
var DOOR_LINER_DARK = 0.15;
|
|
11578
|
+
var DOOR_STRIP_THIN = 0.05;
|
|
11579
|
+
var DOOR_STRIP_NEAR = 0.04;
|
|
11580
|
+
var DOOR_STRIP_OVER = 0.3;
|
|
11581
|
+
var DOOR_TOP_OVER_PANE = 0.03;
|
|
11582
|
+
var DOOR_STRIP_SUB = 0.02;
|
|
11583
|
+
var HOLE_TEXTURE_SIZE = 64;
|
|
11584
|
+
var DOOR_HANDLE_INSET = 0.12;
|
|
11585
|
+
var DOOR_HANDLE_MIN = 0.07;
|
|
11586
|
+
var DOOR_HANDLE_MAX = 0.24;
|
|
11587
|
+
var DOOR_HANDLE_THIN = 0.07;
|
|
11588
|
+
var DOOR_HANDLE_OUTSIDE = 0.1;
|
|
11589
|
+
var PANE_SEAT_OUT = 0.05;
|
|
11590
|
+
var PANE_SEAT_IN = -0.05;
|
|
11591
|
+
var PANE_SEAT_REACH = 0.14;
|
|
11592
|
+
var PANE_SEAT_SAMPLES = 24;
|
|
11593
|
+
var PANE_SEAT_LIFT = 4e-3;
|
|
11594
|
+
var PANE_FRAME_NEAR = 0.07;
|
|
11595
|
+
var DRAWN_MIRROR_SIZE = 0.3;
|
|
11596
|
+
var DRAWN_MIRROR_REACH = 0.4;
|
|
11597
|
+
var DRAWN_MIRROR_MOUNT = 0.2;
|
|
11598
|
+
var MIRROR_GLASS_FIT = 0.76;
|
|
11599
|
+
var MIRROR_GLASS_THIN = 0.04;
|
|
11600
|
+
var MIRROR_GLASS_MIN = 0.05;
|
|
11601
|
+
var MIRROR_GLASS_COLOR = "#59636a";
|
|
11602
|
+
var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
|
|
11603
|
+
var holeMaterials = /* @__PURE__ */ new WeakMap();
|
|
11604
|
+
var holeDisc = null;
|
|
11605
|
+
function holeDiscGeometry(THREE) {
|
|
11606
|
+
holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
|
|
11607
|
+
return holeDisc;
|
|
11608
|
+
}
|
|
11609
|
+
function holeMaterial(kit, material) {
|
|
11610
|
+
const key = kit;
|
|
11611
|
+
const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
|
|
11612
|
+
holeMaterials.set(key, cache2);
|
|
11613
|
+
const known = cache2.get(material);
|
|
11614
|
+
if (known) return known;
|
|
11615
|
+
const I = kit._;
|
|
11616
|
+
const THREE = kit.THREE;
|
|
11617
|
+
const canvas = kit.document?.createElement?.("canvas");
|
|
11618
|
+
const x = canvas?.getContext?.("2d");
|
|
11619
|
+
let made = I.MATERIALS.dark;
|
|
11620
|
+
if (x) {
|
|
11621
|
+
canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
|
|
11622
|
+
const mid = HOLE_TEXTURE_SIZE / 2;
|
|
11623
|
+
x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
|
|
11624
|
+
x.beginPath();
|
|
11625
|
+
for (let i = 0; i < 18; i++) {
|
|
11626
|
+
const a = i / 18 * Math.PI * 2;
|
|
11627
|
+
const r = mid * (i % 2 ? 0.98 : 0.76);
|
|
11628
|
+
x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
|
|
11629
|
+
}
|
|
11630
|
+
x.closePath();
|
|
11631
|
+
x.fill();
|
|
11632
|
+
x.fillStyle = "#050505";
|
|
11633
|
+
x.beginPath();
|
|
11634
|
+
x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
|
|
11635
|
+
x.fill();
|
|
11636
|
+
const map = new THREE.CanvasTexture(canvas);
|
|
11637
|
+
made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
|
|
11638
|
+
}
|
|
11639
|
+
cache2.set(material, made);
|
|
11640
|
+
return made;
|
|
11641
|
+
}
|
|
11339
11642
|
var VehicleVisuals = class {
|
|
11340
11643
|
constructor(kit, c, model) {
|
|
11341
11644
|
__publicField(this, "kit");
|
|
@@ -11353,14 +11656,23 @@ var VehicleVisuals = class {
|
|
|
11353
11656
|
__publicField(this, "syncPlates", /* @__PURE__ */ new Map());
|
|
11354
11657
|
/** Doors and lids whose own skin was cut from the body and hung on the hinge (true), or that had none to take and keep the plate (false). */
|
|
11355
11658
|
__publicField(this, "skins", /* @__PURE__ */ new Map());
|
|
11659
|
+
/** How high each door's cut reaches. */
|
|
11660
|
+
__publicField(this, "doorTops", /* @__PURE__ */ new Map());
|
|
11356
11661
|
__publicField(this, "lightsOn", true);
|
|
11357
11662
|
/** Heavy body meshes whose triangle index is still to build (null once done). */
|
|
11358
11663
|
__publicField(this, "warming");
|
|
11359
11664
|
/** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
|
|
11360
11665
|
__publicField(this, "tornThisFrame", false);
|
|
11666
|
+
/** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
|
|
11667
|
+
__publicField(this, "queued", false);
|
|
11668
|
+
/** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
|
|
11669
|
+
__publicField(this, "holdArmed", false);
|
|
11670
|
+
__publicField(this, "holdFrame", -1);
|
|
11361
11671
|
/** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
|
|
11362
11672
|
__publicField(this, "smoothing", /* @__PURE__ */ new Map());
|
|
11363
11673
|
__publicField(this, "hood");
|
|
11674
|
+
;
|
|
11675
|
+
c.smoothLater = (mesh) => this.smoothLater(mesh);
|
|
11364
11676
|
this.kit = kit;
|
|
11365
11677
|
this.c = c;
|
|
11366
11678
|
this.model = model;
|
|
@@ -11369,6 +11681,8 @@ var VehicleVisuals = class {
|
|
|
11369
11681
|
for (const p of model.parts) this.byId.set(p.id, p);
|
|
11370
11682
|
c.root.updateMatrixWorld(true);
|
|
11371
11683
|
this.hood = this.nameHood();
|
|
11684
|
+
this.seatPanes();
|
|
11685
|
+
this.seatDoorHandles();
|
|
11372
11686
|
this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
|
|
11373
11687
|
if (!this.warming.length) this.warming = null;
|
|
11374
11688
|
for (const p of model.parts) {
|
|
@@ -11389,6 +11703,7 @@ var VehicleVisuals = class {
|
|
|
11389
11703
|
const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
|
|
11390
11704
|
if (!id) return null;
|
|
11391
11705
|
c.hood.piv.name = id;
|
|
11706
|
+
c.hood.piv.userData.plateOf = id;
|
|
11392
11707
|
return { id, open: c.hood.open, sign: c.hood.sign || 1 };
|
|
11393
11708
|
}
|
|
11394
11709
|
mirrorObjects(p) {
|
|
@@ -11476,6 +11791,7 @@ var VehicleVisuals = class {
|
|
|
11476
11791
|
recess.userData.noFollow = true;
|
|
11477
11792
|
const piv = new THREE.Group();
|
|
11478
11793
|
piv.name = p.id;
|
|
11794
|
+
piv.userData.plateOf = p.id;
|
|
11479
11795
|
piv.position.set(hx, cy, sk + s * 0.016);
|
|
11480
11796
|
piv.userData.rigid = true;
|
|
11481
11797
|
this.c.vehicle.add(piv);
|
|
@@ -11492,6 +11808,7 @@ var VehicleVisuals = class {
|
|
|
11492
11808
|
recess.userData.noFollow = true;
|
|
11493
11809
|
const piv = new THREE.Group();
|
|
11494
11810
|
piv.name = p.id;
|
|
11811
|
+
piv.userData.plateOf = p.id;
|
|
11495
11812
|
piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
|
|
11496
11813
|
piv.userData.rigid = true;
|
|
11497
11814
|
this.c.vehicle.add(piv);
|
|
@@ -11555,10 +11872,19 @@ var VehicleVisuals = class {
|
|
|
11555
11872
|
try {
|
|
11556
11873
|
if (p.role === "door") {
|
|
11557
11874
|
const dirty = /* @__PURE__ */ new Set();
|
|
11558
|
-
const
|
|
11559
|
-
const
|
|
11875
|
+
const glassBox = new this.THREE.Box3();
|
|
11876
|
+
for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
|
|
11877
|
+
const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
|
|
11878
|
+
this.doorTops.set(id, top);
|
|
11879
|
+
const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
|
|
11880
|
+
const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
|
|
11560
11881
|
this.mend(dirty);
|
|
11561
|
-
if (pcs.length)
|
|
11882
|
+
if (pcs.length) {
|
|
11883
|
+
const panes = I.windowsOver(this.c, z, inv);
|
|
11884
|
+
const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
|
|
11885
|
+
const mirrors = this.mirrorsOn(z);
|
|
11886
|
+
objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
|
|
11887
|
+
}
|
|
11562
11888
|
} else {
|
|
11563
11889
|
const pcs = this.cutZone(z);
|
|
11564
11890
|
if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
|
|
@@ -11576,10 +11902,124 @@ var VehicleVisuals = class {
|
|
|
11576
11902
|
const di = deform.indexOf(o);
|
|
11577
11903
|
if (di >= 0) deform.splice(di, 1);
|
|
11578
11904
|
}
|
|
11905
|
+
if (p.role === "door" && objs.length > 0) {
|
|
11906
|
+
this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
|
|
11907
|
+
this.liftDoorTrim(objs, z);
|
|
11908
|
+
this.cutStripsAcross(piv, z, inv);
|
|
11909
|
+
}
|
|
11579
11910
|
piv.quaternion.copy(q);
|
|
11580
11911
|
this.c.root.updateMatrixWorld(true);
|
|
11581
11912
|
return objs.length > 0;
|
|
11582
11913
|
}
|
|
11914
|
+
/** A door has edges: a band that follows the skin all round closes it to its card and hides where the cut followed the triangles of the shell; the same band, in the same paint as on a real car, lines the opening. */
|
|
11915
|
+
doorEdges(piv, z, y1) {
|
|
11916
|
+
const THREE = this.THREE;
|
|
11917
|
+
const B = z.box;
|
|
11918
|
+
const side2 = z.side < 0 ? -1 : 1;
|
|
11919
|
+
const flat = piv.position.z - side2 * 0.016;
|
|
11920
|
+
const x0 = B.x0 + 0.03;
|
|
11921
|
+
const x1 = B.x1 - 0.03;
|
|
11922
|
+
const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
|
|
11923
|
+
const loop = [];
|
|
11924
|
+
const run = (ax, ay, bx, by) => {
|
|
11925
|
+
for (let i = 0; i < DOOR_EDGE_PIECES; i++) loop.push([ax + (bx - ax) * i / DOOR_EDGE_PIECES, ay + (by - ay) * i / DOOR_EDGE_PIECES]);
|
|
11926
|
+
};
|
|
11927
|
+
run(x0, y0, x1, y0);
|
|
11928
|
+
run(x1, y0, x1, y1);
|
|
11929
|
+
run(x1, y1, x0, y1);
|
|
11930
|
+
run(x0, y1, x0, y0);
|
|
11931
|
+
const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
|
|
11932
|
+
const band = (lift) => {
|
|
11933
|
+
const position = [];
|
|
11934
|
+
const index = [];
|
|
11935
|
+
loop.forEach(([x, y], i) => {
|
|
11936
|
+
position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
|
|
11937
|
+
const a = i * 2;
|
|
11938
|
+
const b = (i + 1) % loop.length * 2;
|
|
11939
|
+
index.push(a, a + 1, b, b, a + 1, b + 1);
|
|
11940
|
+
});
|
|
11941
|
+
const g = new THREE.BufferGeometry();
|
|
11942
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
|
|
11943
|
+
g.setIndex(index);
|
|
11944
|
+
g.computeVertexNormals();
|
|
11945
|
+
return g;
|
|
11946
|
+
};
|
|
11947
|
+
const paint = this.paint().clone();
|
|
11948
|
+
paint.side = THREE.DoubleSide;
|
|
11949
|
+
const edges = new THREE.Mesh(band(2e-3), paint);
|
|
11950
|
+
const lining = new THREE.Mesh(band(2e-3), paint);
|
|
11951
|
+
for (const m of [edges, lining]) {
|
|
11952
|
+
m.userData.noFollow = true;
|
|
11953
|
+
m.castShadow = false;
|
|
11954
|
+
this.c.vehicle.add(m);
|
|
11955
|
+
}
|
|
11956
|
+
piv.attach(edges);
|
|
11957
|
+
}
|
|
11958
|
+
/** A front door carries the mirror drawn at its corner. */
|
|
11959
|
+
mirrorsOn(z) {
|
|
11960
|
+
if (!z.mirror) return [];
|
|
11961
|
+
const own = (z.mirrors ?? []).filter((o) => o.parent);
|
|
11962
|
+
if (own.length) return own;
|
|
11963
|
+
const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
|
|
11964
|
+
return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
|
|
11965
|
+
}
|
|
11966
|
+
/** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
|
|
11967
|
+
liftDoorTrim(objs, z) {
|
|
11968
|
+
const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
|
|
11969
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
11970
|
+
for (const o of objs) {
|
|
11971
|
+
if (!o.isMesh || o.userData.glass) continue;
|
|
11972
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
11973
|
+
const size = box.getSize(new this.THREE.Vector3());
|
|
11974
|
+
if (size.x > (z.box.x1 - z.box.x0) * 0.4 && size.y > (z.box.y1 - z.box.y0) * 0.3 && size.z < DOOR_DEPTH * 1.5 && o.material?.color?.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK) o.material = card;
|
|
11975
|
+
}
|
|
11976
|
+
}
|
|
11977
|
+
/** A long strip drawn along the side of the car (the chrome of the belt): the part over the door goes with the door, and nothing stays across the opening. */
|
|
11978
|
+
cutStripsAcross(piv, z, inv) {
|
|
11979
|
+
const THREE = this.THREE;
|
|
11980
|
+
const B = z.box;
|
|
11981
|
+
const x0 = B.x0 + 0.03;
|
|
11982
|
+
const x1 = B.x1 - 0.03;
|
|
11983
|
+
const y0 = B.y0 - DOOR_STRIP_NEAR;
|
|
11984
|
+
const y1 = B.y1 + DOOR_STRIP_NEAR;
|
|
11985
|
+
const deform = this.c.deform ?? [];
|
|
11986
|
+
for (const o of [...this.c.vehicle.children]) {
|
|
11987
|
+
if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
|
|
11988
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
11989
|
+
const size = box.getSize(new THREE.Vector3());
|
|
11990
|
+
const mid = box.getCenter(new THREE.Vector3());
|
|
11991
|
+
if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
|
|
11992
|
+
const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
|
|
11993
|
+
const v = new THREE.Vector3();
|
|
11994
|
+
const sdf = (x, y, zz) => {
|
|
11995
|
+
v.set(x, y, zz).applyMatrix4(toVehicle);
|
|
11996
|
+
return Math.max(x0 - v.x, v.x - x1);
|
|
11997
|
+
};
|
|
11998
|
+
this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
|
|
11999
|
+
const from = Math.max(box.min.x, x0);
|
|
12000
|
+
const to = Math.min(box.max.x, x1);
|
|
12001
|
+
const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
|
|
12002
|
+
piece.userData.noFollow = true;
|
|
12003
|
+
piv.attach(piece);
|
|
12004
|
+
}
|
|
12005
|
+
}
|
|
12006
|
+
/** The inside of a door: its skin brought in by the depth of the door, in the trim's dark plastic, so the open door is a door and not one sheet of paint. */
|
|
12007
|
+
doorCards(skins, z) {
|
|
12008
|
+
const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
|
|
12009
|
+
return skins.map((skin) => {
|
|
12010
|
+
const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
|
|
12011
|
+
card.position.copy(skin.position);
|
|
12012
|
+
card.quaternion.copy(skin.quaternion);
|
|
12013
|
+
card.scale.copy(skin.scale);
|
|
12014
|
+
const side2 = z.side < 0 ? -1 : 1;
|
|
12015
|
+
const inward = -side2 * DOOR_DEPTH;
|
|
12016
|
+
card.position.z += inward;
|
|
12017
|
+
card.userData.noFollow = true;
|
|
12018
|
+
card.castShadow = false;
|
|
12019
|
+
this.c.vehicle.add(card);
|
|
12020
|
+
return card;
|
|
12021
|
+
});
|
|
12022
|
+
}
|
|
11583
12023
|
mend(dirty) {
|
|
11584
12024
|
dirty.forEach((msh) => {
|
|
11585
12025
|
const g = msh.geometry;
|
|
@@ -11589,7 +12029,191 @@ var VehicleVisuals = class {
|
|
|
11589
12029
|
g.computeBoundingBox();
|
|
11590
12030
|
});
|
|
11591
12031
|
}
|
|
12032
|
+
/**
|
|
12033
|
+
* A windshield or rear window is drawn as a flat quad, and the body under its edge is curved: the pane and its frame stand off the shell or rise past the roof. Each one
|
|
12034
|
+
* comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
|
|
12035
|
+
*/
|
|
12036
|
+
seatPanes() {
|
|
12037
|
+
const THREE = this.THREE;
|
|
12038
|
+
const c = this.c;
|
|
12039
|
+
c.root.updateMatrixWorld(true);
|
|
12040
|
+
const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
|
|
12041
|
+
if (!shell.length) return;
|
|
12042
|
+
const ray = new THREE.Raycaster();
|
|
12043
|
+
ray.layers.enableAll();
|
|
12044
|
+
for (const pane of c.glass) {
|
|
12045
|
+
if (pane.userData.glass?.outline) continue;
|
|
12046
|
+
const seat = this.paneSeat(pane, shell, ray);
|
|
12047
|
+
if (!seat) continue;
|
|
12048
|
+
const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
|
|
12049
|
+
for (const m of [pane, ...near]) this.movePane(m, seat);
|
|
12050
|
+
const past = this.shellPast(pane, shell, ray, seat.normal);
|
|
12051
|
+
if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
|
|
12052
|
+
}
|
|
12053
|
+
}
|
|
12054
|
+
/** The outward normal of a pane and how far the shell is at each sampled point (world). */
|
|
12055
|
+
paneSeat(pane, shell, ray) {
|
|
12056
|
+
const THREE = this.THREE;
|
|
12057
|
+
const pos = pane.geometry.attributes.position;
|
|
12058
|
+
const box = new THREE.Box3().setFromObject(pane);
|
|
12059
|
+
const centre = box.getCenter(new THREE.Vector3());
|
|
12060
|
+
const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
|
|
12061
|
+
const tri = new THREE.Triangle();
|
|
12062
|
+
const normal = new THREE.Vector3();
|
|
12063
|
+
let largest = 0;
|
|
12064
|
+
const idx = pane.geometry.index;
|
|
12065
|
+
const count = idx ? idx.count : pos.count;
|
|
12066
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
12067
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
12068
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
12069
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
12070
|
+
if (tri.getArea() > largest) {
|
|
12071
|
+
largest = tri.getArea();
|
|
12072
|
+
tri.getNormal(normal);
|
|
12073
|
+
}
|
|
12074
|
+
}
|
|
12075
|
+
normal.transformDirection(pane.matrixWorld);
|
|
12076
|
+
if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
|
|
12077
|
+
const samples = [];
|
|
12078
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12079
|
+
const v = new THREE.Vector3();
|
|
12080
|
+
const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
|
|
12081
|
+
for (let i = 0; i < pos.count; i += step) {
|
|
12082
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
12083
|
+
const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
|
|
12084
|
+
if (seen.has(key)) continue;
|
|
12085
|
+
seen.add(key);
|
|
12086
|
+
const away = v.clone().sub(centre);
|
|
12087
|
+
away.addScaledVector(normal, -away.dot(normal)).normalize();
|
|
12088
|
+
let by = -Infinity;
|
|
12089
|
+
for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
|
|
12090
|
+
const at = v.clone().addScaledVector(away, off);
|
|
12091
|
+
ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
12092
|
+
ray.far = PANE_SEAT_REACH * 2;
|
|
12093
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
12094
|
+
if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
|
|
12095
|
+
}
|
|
12096
|
+
if (by > -Infinity) samples.push({ at: v.clone(), by });
|
|
12097
|
+
}
|
|
12098
|
+
return samples.length >= 3 ? { normal, samples } : null;
|
|
12099
|
+
}
|
|
12100
|
+
/** How far the shell still stands past a seated pane, over its triangles. */
|
|
12101
|
+
shellPast(pane, shell, ray, normal) {
|
|
12102
|
+
const THREE = this.THREE;
|
|
12103
|
+
const pos = pane.geometry.attributes.position;
|
|
12104
|
+
const idx = pane.geometry.index;
|
|
12105
|
+
const count = idx ? idx.count : pos.count;
|
|
12106
|
+
const tri = new THREE.Triangle();
|
|
12107
|
+
const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
|
|
12108
|
+
let past = 0;
|
|
12109
|
+
for (let f = 0; f + 2 < count; f += step) {
|
|
12110
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
12111
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
12112
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
12113
|
+
const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
|
|
12114
|
+
ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
12115
|
+
ray.far = PANE_SEAT_REACH;
|
|
12116
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
12117
|
+
if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
|
|
12118
|
+
}
|
|
12119
|
+
return past;
|
|
12120
|
+
}
|
|
12121
|
+
/** A frame or a seal drawn on the pane: small, and close to its plane. */
|
|
12122
|
+
onPane(o, pane, seat) {
|
|
12123
|
+
const THREE = this.THREE;
|
|
12124
|
+
const box = new THREE.Box3().setFromObject(o);
|
|
12125
|
+
if (box.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box)) return false;
|
|
12126
|
+
const at = box.getCenter(new THREE.Vector3());
|
|
12127
|
+
return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
|
|
12128
|
+
}
|
|
12129
|
+
movePane(mesh, seat) {
|
|
12130
|
+
const THREE = this.THREE;
|
|
12131
|
+
const g = mesh.geometry.clone();
|
|
12132
|
+
const pos = g.attributes.position;
|
|
12133
|
+
const inverse = mesh.matrixWorld.clone().invert();
|
|
12134
|
+
const v = new THREE.Vector3();
|
|
12135
|
+
for (let i = 0; i < pos.count; i++) {
|
|
12136
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld);
|
|
12137
|
+
let weight = 0;
|
|
12138
|
+
let by = 0;
|
|
12139
|
+
for (const s of seat.samples) {
|
|
12140
|
+
const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
|
|
12141
|
+
weight += w;
|
|
12142
|
+
by += s.by * w;
|
|
12143
|
+
}
|
|
12144
|
+
v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
|
|
12145
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
12146
|
+
}
|
|
12147
|
+
pos.needsUpdate = true;
|
|
12148
|
+
g.computeBoundingBox();
|
|
12149
|
+
g.computeBoundingSphere();
|
|
12150
|
+
mesh.geometry = g;
|
|
12151
|
+
}
|
|
12152
|
+
/** A handle is at the free edge of its door, the end away from the hinge. */
|
|
12153
|
+
seatDoorHandles() {
|
|
12154
|
+
const THREE = this.THREE;
|
|
12155
|
+
const c = this.c;
|
|
12156
|
+
const inv = this.I.worldInverseOf(c);
|
|
12157
|
+
for (const p of this.model.parts) {
|
|
12158
|
+
if (p.role !== "door") continue;
|
|
12159
|
+
const z = this.zone(p);
|
|
12160
|
+
if (!z) continue;
|
|
12161
|
+
const B = z.box;
|
|
12162
|
+
const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
|
|
12163
|
+
for (const o of c.vehicle.children) {
|
|
12164
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
|
|
12165
|
+
const box = this.I.localBox(c, o, inv);
|
|
12166
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12167
|
+
const at = box.getCenter(new THREE.Vector3());
|
|
12168
|
+
const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
|
|
12169
|
+
if (!isHandle || Math.sign(at.z) !== Math.sign(z.side) || at.x < B.x0 || at.x > B.x1 || at.y < B.y0 || at.y > B.y1) continue;
|
|
12170
|
+
const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
|
|
12171
|
+
if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
|
|
12172
|
+
const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
|
|
12173
|
+
o.position.x += free - at.x;
|
|
12174
|
+
if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
|
|
12175
|
+
}
|
|
12176
|
+
}
|
|
12177
|
+
c.root.updateMatrixWorld(true);
|
|
12178
|
+
}
|
|
12179
|
+
/** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
|
|
12180
|
+
seatMirrorGlass(glass) {
|
|
12181
|
+
const g = glass.geometry.clone();
|
|
12182
|
+
g.computeBoundingBox();
|
|
12183
|
+
const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
|
|
12184
|
+
g.translate(-mid.x, -mid.y, -mid.z);
|
|
12185
|
+
g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
|
|
12186
|
+
g.translate(mid.x, mid.y, mid.z);
|
|
12187
|
+
glass.geometry = g;
|
|
12188
|
+
glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
|
|
12189
|
+
}
|
|
12190
|
+
/** The mirror the model already draws around where the part says, if it draws one. */
|
|
12191
|
+
drawnMirror(p) {
|
|
12192
|
+
const THREE = this.THREE;
|
|
12193
|
+
const [cx, cy, cz] = p.box.center;
|
|
12194
|
+
this.c.root.updateMatrixWorld(true);
|
|
12195
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
12196
|
+
const small = this.c.vehicle.children.flatMap((o) => {
|
|
12197
|
+
if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
|
|
12198
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
12199
|
+
if (box.isEmpty()) return [];
|
|
12200
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12201
|
+
return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box.getCenter(new THREE.Vector3()) }] : [];
|
|
12202
|
+
});
|
|
12203
|
+
const housing = small.filter(({ at }) => Math.hypot(at.x - cx, at.y - cy, at.z - cz) < DRAWN_MIRROR_REACH && Math.abs(at.z) > Math.abs(cz) - DRAWN_MIRROR_REACH / 2);
|
|
12204
|
+
return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
|
|
12205
|
+
}
|
|
11592
12206
|
makeMirror(p) {
|
|
12207
|
+
const drawn = this.drawnMirror(p);
|
|
12208
|
+
if (drawn.length) {
|
|
12209
|
+
for (const o of drawn) {
|
|
12210
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
|
|
12211
|
+
const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
|
|
12212
|
+
if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
|
|
12213
|
+
}
|
|
12214
|
+
this.extra.set(p.id, drawn);
|
|
12215
|
+
return;
|
|
12216
|
+
}
|
|
11593
12217
|
const [cx, cy, cz] = p.box.center;
|
|
11594
12218
|
const s = cz < 0 ? -1 : 1;
|
|
11595
12219
|
const mats = this.I.MATERIALS;
|
|
@@ -11630,17 +12254,28 @@ var VehicleVisuals = class {
|
|
|
11630
12254
|
}
|
|
11631
12255
|
// ---- marks -------------------------------------------------------------------------------------------------------------------------------
|
|
11632
12256
|
/** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
|
|
11633
|
-
holeDecal(point, normal, diameter) {
|
|
12257
|
+
holeDecal(point, normal, diameter, part) {
|
|
11634
12258
|
const THREE = this.THREE;
|
|
11635
|
-
const
|
|
12259
|
+
const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
|
|
12260
|
+
if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
|
|
12261
|
+
const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
|
|
12262
|
+
const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
|
|
12263
|
+
m.scale.set(radius, radius, 1);
|
|
11636
12264
|
const n = new THREE.Vector3(...normal);
|
|
11637
|
-
m.position.set(...point).addScaledVector(n,
|
|
11638
|
-
|
|
12265
|
+
m.position.set(...point).addScaledVector(n, 3e-3);
|
|
12266
|
+
this.c.vehicle.add(m);
|
|
12267
|
+
m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
|
|
11639
12268
|
m.userData.decal = true;
|
|
11640
12269
|
m.userData.noFollow = true;
|
|
12270
|
+
m.renderOrder = 3;
|
|
11641
12271
|
m.raycast = () => {
|
|
11642
12272
|
};
|
|
11643
|
-
this.c.vehicle.
|
|
12273
|
+
const carrier = (part && (this.plates.get(part.id) ?? (part.id === this.hood?.id ? this.c.hood?.piv : void 0))) ?? (pane && pane.parent !== this.c.vehicle ? pane.parent : null);
|
|
12274
|
+
if (carrier) carrier.attach(m);
|
|
12275
|
+
}
|
|
12276
|
+
/** The door or lid a hinge group stands for. */
|
|
12277
|
+
plateIdOf(o) {
|
|
12278
|
+
return o.userData.plateOf;
|
|
11644
12279
|
}
|
|
11645
12280
|
spark(point, dir, count = 8) {
|
|
11646
12281
|
const V3 = this.THREE.Vector3;
|
|
@@ -11680,10 +12315,8 @@ var VehicleVisuals = class {
|
|
|
11680
12315
|
}
|
|
11681
12316
|
/** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
|
|
11682
12317
|
hitGlass(p, point, dir, force) {
|
|
11683
|
-
const z = this.zone(p);
|
|
11684
|
-
if (!z) return false;
|
|
11685
12318
|
const V3 = this.THREE.Vector3;
|
|
11686
|
-
const list =
|
|
12319
|
+
const list = this.panesOf(p).filter((g) => g.parent && g.visible);
|
|
11687
12320
|
if (!list.length) return false;
|
|
11688
12321
|
const pt = new V3(...point);
|
|
11689
12322
|
let best = list[0];
|
|
@@ -11700,9 +12333,16 @@ var VehicleVisuals = class {
|
|
|
11700
12333
|
this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
|
|
11701
12334
|
return !!best.userData.glass?.broken;
|
|
11702
12335
|
}
|
|
12336
|
+
panesOf(p) {
|
|
12337
|
+
return p.panes ?? this.zone(p)?.glass ?? [];
|
|
12338
|
+
}
|
|
11703
12339
|
glassBreak(p) {
|
|
11704
12340
|
const z = this.zone(p);
|
|
11705
|
-
if (!z)
|
|
12341
|
+
if (!z) {
|
|
12342
|
+
for (const g of p.panes ?? []) if (g.parent && g.visible) this.kit.hitGlass(this.c, g, g.position.clone(), new this.THREE.Vector3(0, -1, 0), 0.6);
|
|
12343
|
+
this.gone.add(p.id);
|
|
12344
|
+
return;
|
|
12345
|
+
}
|
|
11706
12346
|
for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
|
|
11707
12347
|
this.gone.add(p.id);
|
|
11708
12348
|
}
|
|
@@ -11730,6 +12370,7 @@ var VehicleVisuals = class {
|
|
|
11730
12370
|
smoothLater(msh) {
|
|
11731
12371
|
this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
|
|
11732
12372
|
this.tornThisFrame = true;
|
|
12373
|
+
this.holdArmed = true;
|
|
11733
12374
|
}
|
|
11734
12375
|
/** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
|
|
11735
12376
|
pump(budgetMs = 2.5) {
|
|
@@ -11737,6 +12378,7 @@ var VehicleVisuals = class {
|
|
|
11737
12378
|
this.tornThisFrame = false;
|
|
11738
12379
|
return;
|
|
11739
12380
|
}
|
|
12381
|
+
if (this.queued) return;
|
|
11740
12382
|
if (!this.smoothing.size && !this.warming) return;
|
|
11741
12383
|
const deadline = performance.now() + budgetMs;
|
|
11742
12384
|
for (const [msh, task] of this.smoothing) {
|
|
@@ -11745,6 +12387,34 @@ var VehicleVisuals = class {
|
|
|
11745
12387
|
}
|
|
11746
12388
|
this.warmIndexes(deadline);
|
|
11747
12389
|
}
|
|
12390
|
+
setQueued(isQueued) {
|
|
12391
|
+
this.queued = isQueued;
|
|
12392
|
+
}
|
|
12393
|
+
/** Units the shared queue may run now: 0 in the frame of the cut (the first ask after it holds the work for that frame, or `pump` closes it when there is no frame). */
|
|
12394
|
+
pendingUnits(frame) {
|
|
12395
|
+
if (this.holdArmed && frame !== void 0) {
|
|
12396
|
+
this.holdArmed = false;
|
|
12397
|
+
this.holdFrame = frame;
|
|
12398
|
+
}
|
|
12399
|
+
if (frame !== void 0 && frame === this.holdFrame) return 0;
|
|
12400
|
+
if (frame === void 0 && this.tornThisFrame) return 0;
|
|
12401
|
+
return this.smoothing.size + (this.warming ? 1 : 0);
|
|
12402
|
+
}
|
|
12403
|
+
/** One slice of the pending work, for the shared queue. */
|
|
12404
|
+
runStep(deadline) {
|
|
12405
|
+
const next = this.smoothing.entries().next();
|
|
12406
|
+
if (!next.done) {
|
|
12407
|
+
const [msh, task] = next.value;
|
|
12408
|
+
if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
|
|
12409
|
+
return true;
|
|
12410
|
+
}
|
|
12411
|
+
if (!this.warming) return false;
|
|
12412
|
+
this.warmIndexes(deadline);
|
|
12413
|
+
return true;
|
|
12414
|
+
}
|
|
12415
|
+
finishAll() {
|
|
12416
|
+
while (this.runStep(Infinity)) ;
|
|
12417
|
+
}
|
|
11748
12418
|
/** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
|
|
11749
12419
|
warmIndexes(deadline) {
|
|
11750
12420
|
if (!this.warming) return;
|
|
@@ -11916,6 +12586,7 @@ var VehicleVisuals = class {
|
|
|
11916
12586
|
}
|
|
11917
12587
|
pieceTasks.forEach((task, mesh) => this.smoothing.set(mesh, task));
|
|
11918
12588
|
this.tornThisFrame = true;
|
|
12589
|
+
this.holdArmed = true;
|
|
11919
12590
|
dirty.forEach((msh) => {
|
|
11920
12591
|
const g = msh.geometry;
|
|
11921
12592
|
if (g.userData.crease || !g.index) this.smoothLater(msh);
|
|
@@ -12022,6 +12693,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12022
12693
|
let clock = 0;
|
|
12023
12694
|
let model;
|
|
12024
12695
|
let vis = null;
|
|
12696
|
+
let releaseCrease;
|
|
12025
12697
|
const ps = /* @__PURE__ */ new Map();
|
|
12026
12698
|
let js = [];
|
|
12027
12699
|
let leaks = /* @__PURE__ */ new Map();
|
|
@@ -12106,7 +12778,9 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12106
12778
|
rand = rng(seedBase);
|
|
12107
12779
|
igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
|
|
12108
12780
|
steerSign = rand() < 0.5 ? -1 : 1;
|
|
12781
|
+
releaseCrease?.();
|
|
12109
12782
|
vis = headless ? null : new VehicleVisuals(kit, c, model);
|
|
12783
|
+
releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
|
|
12110
12784
|
recomputeGroups();
|
|
12111
12785
|
};
|
|
12112
12786
|
const recomputeGroups = () => {
|
|
@@ -12417,13 +13091,13 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12417
13091
|
sync();
|
|
12418
13092
|
const M = toFrameMatrix();
|
|
12419
13093
|
const v = toLocal(h.point);
|
|
12420
|
-
const p = locate(v, 0.2);
|
|
13094
|
+
const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
|
|
12421
13095
|
if (!p) return;
|
|
12422
13096
|
const d = p.def;
|
|
12423
13097
|
const E = h.energy ?? 0;
|
|
12424
13098
|
const cal = h.caliber ?? 9;
|
|
12425
13099
|
const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
|
|
12426
|
-
const n = normalOf(d, v);
|
|
13100
|
+
const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
|
|
12427
13101
|
const res = resist(d, p);
|
|
12428
13102
|
const through = E >= res;
|
|
12429
13103
|
const cosInc = Math.abs(dir.dot(n));
|
|
@@ -12442,7 +13116,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12442
13116
|
const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
|
|
12443
13117
|
p.holes.push(hp);
|
|
12444
13118
|
out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
|
|
12445
|
-
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
|
|
13119
|
+
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
|
|
12446
13120
|
startLeak(p, v, cal, out, dir);
|
|
12447
13121
|
};
|
|
12448
13122
|
switch (d.material) {
|
|
@@ -12743,6 +13417,46 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12743
13417
|
for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
|
|
12744
13418
|
return false;
|
|
12745
13419
|
};
|
|
13420
|
+
const surfaceRays = new THREE.Raycaster();
|
|
13421
|
+
surfaceRays.layers.enableAll();
|
|
13422
|
+
surfaceRays.camera = new THREE.PerspectiveCamera();
|
|
13423
|
+
let meshParts = null;
|
|
13424
|
+
const paneParts = () => {
|
|
13425
|
+
if (meshParts) return meshParts;
|
|
13426
|
+
meshParts = /* @__PURE__ */ new Map();
|
|
13427
|
+
for (const d of model.parts) {
|
|
13428
|
+
if (d.role !== "glass") continue;
|
|
13429
|
+
const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
|
|
13430
|
+
for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
|
|
13431
|
+
}
|
|
13432
|
+
return meshParts;
|
|
13433
|
+
};
|
|
13434
|
+
const isDrawnSurface = (o) => {
|
|
13435
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
|
|
13436
|
+
return true;
|
|
13437
|
+
};
|
|
13438
|
+
const partOfSurface = (o, v) => {
|
|
13439
|
+
const pane = paneParts().get(o);
|
|
13440
|
+
if (pane) return ps.get(pane) ?? null;
|
|
13441
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) {
|
|
13442
|
+
if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
|
|
13443
|
+
}
|
|
13444
|
+
return locate(v, 0.08, (p) => p.def.role === "glass");
|
|
13445
|
+
};
|
|
13446
|
+
const surfaceRay = (ray) => {
|
|
13447
|
+
sync();
|
|
13448
|
+
surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
|
|
13449
|
+
surfaceRays.far = ray.maxDistance;
|
|
13450
|
+
for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
|
|
13451
|
+
if (!h.face || !isDrawnSurface(h.object)) continue;
|
|
13452
|
+
const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
|
|
13453
|
+
if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
|
|
13454
|
+
const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
|
|
13455
|
+
if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
|
|
13456
|
+
return { part: p.def.id, point: [h.point.x, h.point.y, h.point.z], normal: [n.x, n.y, n.z], distance: h.distance, pathLen: 0.02 };
|
|
13457
|
+
}
|
|
13458
|
+
return null;
|
|
13459
|
+
};
|
|
12746
13460
|
const hit = (h) => {
|
|
12747
13461
|
stateCache = void 0;
|
|
12748
13462
|
const out = [];
|
|
@@ -12865,9 +13579,14 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12865
13579
|
},
|
|
12866
13580
|
describe,
|
|
12867
13581
|
hit,
|
|
13582
|
+
surfaceRay,
|
|
12868
13583
|
update,
|
|
12869
13584
|
state,
|
|
12870
13585
|
reset,
|
|
13586
|
+
dispose: () => {
|
|
13587
|
+
releaseCrease?.();
|
|
13588
|
+
releaseCrease = void 0;
|
|
13589
|
+
},
|
|
12871
13590
|
disassemble,
|
|
12872
13591
|
extinguish,
|
|
12873
13592
|
serialize: () => ({
|
|
@@ -12906,6 +13625,62 @@ function buildVehicleItem(kit, key, options = {}) {
|
|
|
12906
13625
|
return { root, vehicle };
|
|
12907
13626
|
}
|
|
12908
13627
|
|
|
13628
|
+
// src/destructible/crease-queue.ts
|
|
13629
|
+
import { DEFERRED_QUEUE_ID } from "@adatechnology/game-contracts";
|
|
13630
|
+
function createCreaseQueue() {
|
|
13631
|
+
const tracked = [];
|
|
13632
|
+
let cursor = 0;
|
|
13633
|
+
let lastFrame;
|
|
13634
|
+
let attached = false;
|
|
13635
|
+
const deferred = {
|
|
13636
|
+
id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
|
|
13637
|
+
pending(frame) {
|
|
13638
|
+
lastFrame = frame;
|
|
13639
|
+
let units = 0;
|
|
13640
|
+
for (const work of tracked) units += work.pendingUnits(frame);
|
|
13641
|
+
return units;
|
|
13642
|
+
},
|
|
13643
|
+
run(deadline, clock) {
|
|
13644
|
+
let ran = 0;
|
|
13645
|
+
let skipped = 0;
|
|
13646
|
+
while (tracked.length > 0 && skipped < tracked.length) {
|
|
13647
|
+
if (ran > 0 && clock() >= deadline) break;
|
|
13648
|
+
const work = tracked[cursor % tracked.length];
|
|
13649
|
+
cursor = (cursor + 1) % tracked.length;
|
|
13650
|
+
if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
|
|
13651
|
+
ran++;
|
|
13652
|
+
skipped = 0;
|
|
13653
|
+
} else skipped++;
|
|
13654
|
+
}
|
|
13655
|
+
return ran;
|
|
13656
|
+
},
|
|
13657
|
+
flush() {
|
|
13658
|
+
for (const work of tracked) work.finishAll();
|
|
13659
|
+
},
|
|
13660
|
+
attach() {
|
|
13661
|
+
attached = true;
|
|
13662
|
+
for (const work of tracked) work.setQueued(true);
|
|
13663
|
+
},
|
|
13664
|
+
detach() {
|
|
13665
|
+
attached = false;
|
|
13666
|
+
for (const work of tracked) work.setQueued(false);
|
|
13667
|
+
}
|
|
13668
|
+
};
|
|
13669
|
+
return {
|
|
13670
|
+
deferredQueue: () => deferred,
|
|
13671
|
+
track(work) {
|
|
13672
|
+
tracked.push(work);
|
|
13673
|
+
work.setQueued(attached);
|
|
13674
|
+
return () => {
|
|
13675
|
+
const at = tracked.indexOf(work);
|
|
13676
|
+
if (at < 0) return;
|
|
13677
|
+
tracked.splice(at, 1);
|
|
13678
|
+
work.setQueued(false);
|
|
13679
|
+
};
|
|
13680
|
+
}
|
|
13681
|
+
};
|
|
13682
|
+
}
|
|
13683
|
+
|
|
12909
13684
|
// src/destructible/enclosure.ts
|
|
12910
13685
|
var CABIN_LEAK = 0.02;
|
|
12911
13686
|
var HOLE_RATE_PER_AREA = 3e3;
|
|
@@ -13000,6 +13775,8 @@ export {
|
|
|
13000
13775
|
cabinBoxOf,
|
|
13001
13776
|
cabinEnclosure,
|
|
13002
13777
|
clamp,
|
|
13778
|
+
crackedGlassMaterial,
|
|
13779
|
+
createCreaseQueue,
|
|
13003
13780
|
createLod,
|
|
13004
13781
|
createTranslate,
|
|
13005
13782
|
createVehicleDestructible,
|