Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json. Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser Stand als normaler Ordner im Repo, ohne Verweis auf GitHub. dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher lokal gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.1 KiB
JavaScript
114 lines
4.1 KiB
JavaScript
import {
|
|
Matrix3,
|
|
Mesh,
|
|
MeshBasicMaterial,
|
|
Object3D,
|
|
PlaneGeometry,
|
|
TextureLoader,
|
|
} from "three";
|
|
|
|
const _normalMatrix = new Matrix3(); // 法线矩阵
|
|
const _decalHelper = new Object3D(); // 弹孔朝向辅助体
|
|
|
|
// 弹孔贴花系统(对象池)
|
|
export class DecalSystem {
|
|
constructor(scene, maxDecals = 60, decalSize = 0.02) {
|
|
// ==================== 场景引用 ====================
|
|
this._scene = scene;
|
|
|
|
// ==================== 对象池配置 ====================
|
|
this._maxDecals = maxDecals; // 最大弹孔数量,超出后移除最旧的
|
|
this._pool = []; // 场景中现存的弹孔 Mesh
|
|
this._mats = []; // 可用的弹痕材质(多张随机选取)
|
|
this._geo = new PlaneGeometry(decalSize, decalSize); // 弹孔平面几何体
|
|
this.onSpawn = null; // (hitPoint, hitNormal) => void,多人模式注入广播回调
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
// 批量加载弹痕贴图,生成对应材质
|
|
async loadMaterials(files, baseUrl) {
|
|
const loader = new TextureLoader();
|
|
await Promise.all(
|
|
files.map(async (f) => {
|
|
const tex = await loader.loadAsync(baseUrl + f);
|
|
this._mats.push(
|
|
new MeshBasicMaterial({
|
|
map: tex,
|
|
transparent: true,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
polygonOffset: true,
|
|
polygonOffsetFactor: -4, // 防止与墙面 Z-fighting
|
|
})
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
// ==================== 生成 / 清除 ====================
|
|
|
|
// 在射线命中点生成弹孔,并触发命中硝烟特效
|
|
spawn(hit, effects) {
|
|
if (!hit?.face || !hit?.object) return;
|
|
if (!this._mats.length) return;
|
|
|
|
// 计算世界法线
|
|
_normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
const hitNormal = hit.face.normal.clone().applyMatrix3(_normalMatrix).normalize();
|
|
const hitPoint = hit.point.clone();
|
|
|
|
// 计算弹孔朝向(面向法线 + 随机旋转)
|
|
_decalHelper.position.copy(hitPoint);
|
|
_decalHelper.lookAt(hitPoint.clone().add(hitNormal));
|
|
_decalHelper.rotation.z = Math.random() * Math.PI * 2;
|
|
|
|
// 创建弹孔 Mesh,沿法线微偏移以避免 Z-fighting
|
|
const mat = this._mats[Math.floor(Math.random() * this._mats.length)];
|
|
const decal = new Mesh(this._geo, mat);
|
|
decal.position.copy(hitPoint).addScaledVector(hitNormal, 0.001);
|
|
decal.rotation.copy(_decalHelper.rotation);
|
|
decal.renderOrder = 1;
|
|
this._scene.add(decal);
|
|
|
|
// 超出上限时移除最旧弹孔
|
|
this._pool.push(decal);
|
|
if (this._pool.length > this._maxDecals) {
|
|
this._scene.remove(this._pool.shift());
|
|
}
|
|
|
|
// 命中硝烟特效
|
|
effects?.triggerHitSmoke(hitPoint, hitNormal);
|
|
|
|
// 广播给其他客户端(多人模式注入)
|
|
this.onSpawn?.(hitPoint, hitNormal);
|
|
}
|
|
|
|
// 根据世界坐标和法线直接生成弹孔(接收远程弹痕时使用,不触发硝烟)
|
|
spawnAtPoint(hitPoint, hitNormal) {
|
|
if (!this._mats.length) return;
|
|
|
|
_decalHelper.position.copy(hitPoint);
|
|
_decalHelper.lookAt(hitPoint.clone().add(hitNormal));
|
|
_decalHelper.rotation.z = Math.random() * Math.PI * 2;
|
|
|
|
const mat = this._mats[Math.floor(Math.random() * this._mats.length)];
|
|
const decal = new Mesh(this._geo, mat);
|
|
decal.position.copy(hitPoint).addScaledVector(hitNormal, 0.001);
|
|
decal.rotation.copy(_decalHelper.rotation);
|
|
decal.renderOrder = 1;
|
|
this._scene.add(decal);
|
|
|
|
this._pool.push(decal);
|
|
if (this._pool.length > this._maxDecals) {
|
|
this._scene.remove(this._pool.shift());
|
|
}
|
|
}
|
|
|
|
// 清空所有弹孔
|
|
clear() {
|
|
for (const d of this._pool) this._scene.remove(d);
|
|
this._pool.length = 0;
|
|
}
|
|
}
|