首页 / 模块目录 / 世界 / 环境 / 视觉 / ProjectileVisualFactory

ProjectileVisualFactory

three

为子弹/导弹构建弹丸视觉模型与更新助手。

类别世界 / 环境 / 视觉
依赖档位three
内部依赖ScalarUtils
相关模块
演示场factory-showroom · 视觉工厂展厅 ↓
只取这个模块
modules/world/object/factory/ProjectileVisualFactory.js

连同其内部依赖一并复制,保留相对目录结构。

视觉工厂展厅

three

自动旋转展示,无需操作。

源码

import * as THREE from 'three';
import { clamp } from '../../../math/ScalarUtils.js';

const LOCAL_CONE_FORWARD = new THREE.Vector3(0, 1, 0);
const LOCAL_CYLINDER_FORWARD = new THREE.Vector3(0, 1, 0);

function setCylinderBetween(mesh, start, end) {
  const delta = end.clone().sub(start);
  const length = Math.max(0.001, delta.length());
  const midpoint = start.clone().addScaledVector(delta, 0.5);
  mesh.position.copy(midpoint);
  mesh.scale.set(1, length, 1);
  mesh.quaternion.setFromUnitVectors(LOCAL_CYLINDER_FORWARD, delta.normalize());
}

export function createBulletProjectileVisual() {
  const group = new THREE.Group();
  group.name = 'ProjectileObject.bullet';

  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(2.6, 12, 8),
    new THREE.MeshBasicMaterial({
      color: 0xfff3a1,
      transparent: true,
      opacity: 0.95,
      depthTest: false,
      depthWrite: false,
    })
  );
  mesh.renderOrder = 7;
  group.add(mesh);

  return {
    group,
    mesh,
    step({ position, ageSeconds, lifetimeSeconds }) {
      mesh.position.copy(position);
      const fade = clamp(1 - ageSeconds / lifetimeSeconds, 0, 1);
      mesh.material.opacity = 0.95 * fade;
    },
  };
}

export function createMissileProjectileVisual() {
  const group = new THREE.Group();
  group.name = 'ProjectileObject.missile';

  const mesh = new THREE.Mesh(
    new THREE.ConeGeometry(2.8, 13, 16),
    new THREE.MeshStandardMaterial({
      color: 0xf4f7ff,
      emissive: 0xff7b35,
      emissiveIntensity: 1.2,
      metalness: 0.35,
      roughness: 0.28,
    })
  );
  const flame = new THREE.Mesh(
    new THREE.SphereGeometry(3.6, 14, 10),
    new THREE.MeshBasicMaterial({
      color: 0xff9f2f,
      transparent: true,
      opacity: 0.9,
      depthWrite: false,
    })
  );
  const trail = new THREE.Mesh(
    new THREE.CylinderGeometry(1.35, 0.55, 1, 10),
    new THREE.MeshBasicMaterial({
      color: 0xffd28a,
      transparent: true,
      opacity: 0.5,
      depthWrite: false,
    })
  );

  mesh.renderOrder = 7;
  flame.renderOrder = 7;
  trail.renderOrder = 5;
  group.add(trail, mesh, flame);

  return {
    group,
    mesh,
    flame,
    trail,
    step({ position, direction, ageSeconds }) {
      mesh.position.copy(position);
      mesh.quaternion.setFromUnitVectors(LOCAL_CONE_FORWARD, direction);
      flame.position.copy(position).addScaledVector(direction, -7.5);
      setCylinderBetween(
        trail,
        position.clone().addScaledVector(direction, -84),
        position.clone().addScaledVector(direction, -9)
      );

      const flicker = 0.78 + Math.sin(ageSeconds * 60) * 0.18;
      flame.material.opacity = flicker;
      flame.scale.setScalar(0.9 + flicker * 0.22);
    },
  };
}