add 3d to srebot (#1350)
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
// Pure 3D replay view. No DOM beyond its own <canvas>; driven entirely by an
|
||||
// external clock (setTime) so it stays in sync with the 2D ReplayCanvasEngine.
|
||||
// Ported from the standalone REPLAY_VIEWER three.js viewer, stripped of all of
|
||||
// its own UI (labels, axes, guide lines, controls, hover/active-list panels).
|
||||
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
|
||||
|
||||
const MODEL_PATH = '/models/t34/'
|
||||
const MINIMAP_URL = (level) => `/api/match/minimap/${level}`
|
||||
const MINIMAP_FULL_URL = (level) => `/api/match/minimap/${level}?type=full`
|
||||
|
||||
const WIN_COLOR = '#00c800'
|
||||
const LOSE_COLOR = '#dc1e1e'
|
||||
const DRONE_COLOR = '#cbd5e1'
|
||||
|
||||
const DIRECTION_SAMPLE_MS = 700
|
||||
const DIRECTION_BLEND = 0.22
|
||||
const KILL_LINE_WINDOW_MS = 3000
|
||||
const CAP_INTERP_MAX_GAP_MS = 4000
|
||||
const FLIP_MAP_TEXTURE_Z = true
|
||||
|
||||
function coordsAreValid(c) {
|
||||
return c &&
|
||||
Number.isFinite(Number(c.x0)) && Number.isFinite(Number(c.z0)) &&
|
||||
Number.isFinite(Number(c.x1)) && Number.isFinite(Number(c.z1)) &&
|
||||
Number(c.x0) !== Number(c.x1) && Number(c.z0) !== Number(c.z1)
|
||||
}
|
||||
|
||||
function normalizeCoords(c) {
|
||||
if (!coordsAreValid(c)) return null
|
||||
return { x0: Number(c.x0), z0: Number(c.z0), x1: Number(c.x1), z1: Number(c.z1) }
|
||||
}
|
||||
|
||||
function mapSourceRect(baseCoords, renderCoords) {
|
||||
const base = normalizeCoords(baseCoords)
|
||||
const render = normalizeCoords(renderCoords)
|
||||
if (!base || !render) return { u: 0, v: 0, w: 1, h: 1 }
|
||||
const dx = base.x1 - base.x0
|
||||
const dz = base.z1 - base.z0
|
||||
const xLo = Math.min(render.x0, render.x1)
|
||||
const xHi = Math.max(render.x0, render.x1)
|
||||
const zLo = Math.min(render.z0, render.z1)
|
||||
const zHi = Math.max(render.z0, render.z1)
|
||||
const u0 = (xLo - base.x0) / dx
|
||||
const u1 = (xHi - base.x0) / dx
|
||||
const v0 = (zLo - base.z0) / dz
|
||||
const v1 = (zHi - base.z0) / dz
|
||||
const uMin = Math.max(0, Math.min(1, Math.min(u0, u1)))
|
||||
const uMax = Math.max(0, Math.min(1, Math.max(u0, u1)))
|
||||
const vMin = Math.max(0, Math.min(1, Math.min(v0, v1)))
|
||||
const vMax = Math.max(0, Math.min(1, Math.max(v0, v1)))
|
||||
const w = uMax - uMin
|
||||
const h = vMax - vMin
|
||||
if (!(w > 0 && h > 0)) return { u: 0, v: 0, w: 1, h: 1 }
|
||||
return { u: uMin, v: 1 - vMax, w, h }
|
||||
}
|
||||
|
||||
function fallbackBoundsCoords(bounds) {
|
||||
const pad = Math.max(bounds.planarSpan * 0.15, 250)
|
||||
return { x0: bounds.minX - pad, z0: bounds.minZ - pad, x1: bounds.maxX + pad, z1: bounds.maxZ + pad }
|
||||
}
|
||||
|
||||
function catmullRom(a, b, c, d, t) {
|
||||
const t2 = t * t
|
||||
const t3 = t2 * t
|
||||
return 0.5 * ((2 * b) + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3)
|
||||
}
|
||||
|
||||
class ReplayCanvas3D {
|
||||
constructor(container, data) {
|
||||
this.container = container
|
||||
this.data = data
|
||||
this.disposed = false
|
||||
this.currentT = 0
|
||||
this.selectedPlayerId = null
|
||||
this.smoothByEntity = new Map()
|
||||
|
||||
this.players = {}
|
||||
for (const p of data.players || []) this.players[p.id] = p
|
||||
this.teamWon = data.teamWon
|
||||
|
||||
this._buildEntities()
|
||||
this._buildCaptureModel()
|
||||
this.kills = (data.kills || []).filter((k) => k.killerPos && k.victimPos)
|
||||
|
||||
this.bounds = this._computeBounds()
|
||||
this.scale = 1200 / this.bounds.planarSpan
|
||||
|
||||
this._initThree()
|
||||
|
||||
this._mode = 'ground'
|
||||
this.mapInfo = this._resolveMapInfo('ground')
|
||||
|
||||
this._buildScene()
|
||||
this._loadTankTemplate()
|
||||
this._loadMapTexture()
|
||||
|
||||
this._animate = this._animate.bind(this)
|
||||
this._raf = requestAnimationFrame(this._animate)
|
||||
this.setTime(0)
|
||||
}
|
||||
|
||||
// ---- data adaptation -----------------------------------------------------
|
||||
|
||||
_isWinner(entity) {
|
||||
if (entity.playerId > 0) return this.players[entity.playerId]?.team === this.teamWon
|
||||
return entity.droneTeam === this.teamWon
|
||||
}
|
||||
|
||||
_colorFor(entity) {
|
||||
if (entity.playerId === 0 && entity.type === 'drone') return DRONE_COLOR
|
||||
return this._isWinner(entity) ? WIN_COLOR : LOSE_COLOR
|
||||
}
|
||||
|
||||
_buildEntities() {
|
||||
this.entities = []
|
||||
for (const e of this.data.entities || []) {
|
||||
if (!Array.isArray(e.path) || !e.path.length) continue
|
||||
const path = e.path.map((p) => ({ t: Number(p.t), x: Number(p.x), y: Number(p.y || 0), z: Number(p.z) }))
|
||||
this.entities.push({
|
||||
playerId: e.playerId,
|
||||
entityIndex: e.entityIndex,
|
||||
type: e.type,
|
||||
droneTeam: e.droneTeam,
|
||||
path,
|
||||
startT: path[0].t,
|
||||
endT: path[path.length - 1].t,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_buildCaptureModel() {
|
||||
const areas = Array.isArray(this.data.captureAreas) ? this.data.captureAreas : []
|
||||
const state = this.data.captureState || {}
|
||||
this.captureZones = areas.map((area, index) => {
|
||||
const letter = String.fromCharCode(65 + index)
|
||||
const cap = Array.isArray(state[letter])
|
||||
? state[letter].map(([t, v]) => [Number(t), Number(v)]).filter(([t, v]) => Number.isFinite(t) && Number.isFinite(v))
|
||||
: []
|
||||
const center = area.tm?.center || [area.x, 0, area.z]
|
||||
return {
|
||||
key: letter,
|
||||
center: { x: Number(area.x ?? center[0]), y: Number(center[1] || 0), z: Number(area.z ?? center[2]) },
|
||||
radius: Math.max(1, Number(area.radius) || 1),
|
||||
cap,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_computeBounds() {
|
||||
const b = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity, minZ: Infinity, maxZ: -Infinity }
|
||||
for (const e of this.entities) {
|
||||
for (const p of e.path) {
|
||||
b.minX = Math.min(b.minX, p.x); b.maxX = Math.max(b.maxX, p.x)
|
||||
b.minY = Math.min(b.minY, p.y); b.maxY = Math.max(b.maxY, p.y)
|
||||
b.minZ = Math.min(b.minZ, p.z); b.maxZ = Math.max(b.maxZ, p.z)
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(b.minX)) { b.minX = 0; b.maxX = 1; b.minY = 0; b.maxY = 1; b.minZ = 0; b.maxZ = 1 }
|
||||
b.centerX = (b.minX + b.maxX) / 2
|
||||
b.centerZ = (b.minZ + b.maxZ) / 2
|
||||
b.spanX = Math.max(1, b.maxX - b.minX)
|
||||
b.spanZ = Math.max(1, b.maxZ - b.minZ)
|
||||
b.planarSpan = Math.max(b.spanX, b.spanZ)
|
||||
return b
|
||||
}
|
||||
|
||||
_resolveMapInfo(mode) {
|
||||
const level = this.data.mission?.level
|
||||
const full = this.data.fullMapLevel
|
||||
if (mode === 'air' && full && normalizeCoords(this.data.mapCoords)) {
|
||||
const coords = normalizeCoords(this.data.mapCoords)
|
||||
return { image: MINIMAP_FULL_URL(full), coords, baseCoords: coords, sourceRect: { u: 0, v: 0, w: 1, h: 1 } }
|
||||
}
|
||||
const coords = normalizeCoords(this.data.levelCoords) || fallbackBoundsCoords(this.bounds)
|
||||
const baseCoords = normalizeCoords(this.data.tankMapCoords) || coords
|
||||
return {
|
||||
image: level ? MINIMAP_URL(level) : null,
|
||||
coords,
|
||||
baseCoords,
|
||||
sourceRect: mapSourceRect(baseCoords, coords),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- coordinate transforms ----------------------------------------------
|
||||
|
||||
_toWorld(p) {
|
||||
const b = this.bounds
|
||||
return new THREE.Vector3(
|
||||
(p.x - b.centerX) * this.scale,
|
||||
(p.y - b.minY) * this.scale,
|
||||
(p.z - b.centerZ) * this.scale,
|
||||
)
|
||||
}
|
||||
|
||||
_renderPoint(p) {
|
||||
if (!FLIP_MAP_TEXTURE_Z) return p
|
||||
const coords = normalizeCoords(this.mapInfo?.coords)
|
||||
if (!coords) return p
|
||||
return { ...p, z: coords.z0 + coords.z1 - p.z }
|
||||
}
|
||||
|
||||
// ---- three.js setup ------------------------------------------------------
|
||||
|
||||
_initThree() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.className = 'rc3d-canvas'
|
||||
this.canvasEl = canvas
|
||||
this.container.appendChild(canvas)
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
||||
this.renderer.setClearColor(0x0b0d10, 1)
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(55, 1, 0.1, 20000)
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.controls.enableDamping = true
|
||||
this.controls.dampingFactor = 0.08
|
||||
this.controls.screenSpacePanning = false
|
||||
this.controls.maxPolarAngle = Math.PI * 0.49
|
||||
this.controls.mouseButtons.LEFT = THREE.MOUSE.ROTATE
|
||||
this.controls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY
|
||||
this.controls.mouseButtons.RIGHT = THREE.MOUSE.PAN
|
||||
|
||||
this.root = new THREE.Group()
|
||||
this.mapGroup = new THREE.Group()
|
||||
this.pathGroup = new THREE.Group()
|
||||
this.zoneGroup = new THREE.Group()
|
||||
this.killLineGroup = new THREE.Group()
|
||||
this.markerGroup = new THREE.Group()
|
||||
this.root.add(this.mapGroup, this.pathGroup, this.zoneGroup, this.killLineGroup, this.markerGroup)
|
||||
this.scene.add(this.root)
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.72))
|
||||
const sun = new THREE.DirectionalLight(0xffffff, 1.1)
|
||||
sun.position.set(240, 500, 180)
|
||||
this.scene.add(sun)
|
||||
|
||||
this.resize()
|
||||
}
|
||||
|
||||
_worldMapExtents(coords = this.mapInfo?.coords) {
|
||||
const c = normalizeCoords(coords)
|
||||
if (!c) return null
|
||||
const a = this._toWorld({ x: c.x0, y: this.bounds.minY, z: c.z0 })
|
||||
const b = this._toWorld({ x: c.x1, y: this.bounds.minY, z: c.z1 })
|
||||
return {
|
||||
minX: Math.min(a.x, b.x), maxX: Math.max(a.x, b.x),
|
||||
minZ: Math.min(a.z, b.z), maxZ: Math.max(a.z, b.z),
|
||||
width: Math.abs(b.x - a.x), depth: Math.abs(b.z - a.z),
|
||||
}
|
||||
}
|
||||
|
||||
_resetCamera() {
|
||||
const ext = this._worldMapExtents()
|
||||
const span = Math.max(ext?.width || 1400, ext?.depth || 1400, 100)
|
||||
this.camera.position.set(span * 0.45, span * 0.55, span * 0.72)
|
||||
this.controls.target.set(0, 0, 0)
|
||||
this.controls.minDistance = Math.max(18, span * 0.025)
|
||||
this.controls.maxDistance = Math.max(320, span * 1.8)
|
||||
this.camera.far = Math.max(9000, span * 8)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
// ---- scene construction --------------------------------------------------
|
||||
|
||||
_buildScene() {
|
||||
this._clearGroup(this.mapGroup)
|
||||
this._clearGroup(this.pathGroup)
|
||||
this._clearGroup(this.markerGroup)
|
||||
this._clearGroup(this.zoneGroup)
|
||||
this._clearGroup(this.killLineGroup)
|
||||
this._buildMapPlane()
|
||||
this._buildCaptureZones()
|
||||
this._buildKillLines()
|
||||
this.visualEntities = this.entities.map((e) => this._makeVisualEntity(e))
|
||||
this._applyHighlight()
|
||||
this._resetCamera()
|
||||
}
|
||||
|
||||
_clearGroup(group) {
|
||||
for (const child of [...group.children]) {
|
||||
group.remove(child)
|
||||
child.traverse?.((c) => {
|
||||
if (c.geometry) c.geometry.dispose()
|
||||
if (c.material) (Array.isArray(c.material) ? c.material : [c.material]).forEach((m) => m.dispose())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_buildMapPlane() {
|
||||
const info = this.mapInfo
|
||||
if (!info || !coordsAreValid(info.coords)) return
|
||||
const x0 = Math.min(info.coords.x0, info.coords.x1)
|
||||
const x1 = Math.max(info.coords.x0, info.coords.x1)
|
||||
const z0 = Math.min(info.coords.z0, info.coords.z1)
|
||||
const z1 = Math.max(info.coords.z0, info.coords.z1)
|
||||
const width = Math.max(1, (x1 - x0) * this.scale)
|
||||
const depth = Math.max(1, (z1 - z0) * this.scale)
|
||||
const center = this._toWorld({ x: (x0 + x1) / 2, y: this.bounds.minY, z: (z0 + z1) / 2 })
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(width, depth, 1, 1)
|
||||
this._applyPlaneUvs(geometry, info.sourceRect || { u: 0, v: 0, w: 1, h: 1 })
|
||||
geometry.rotateX(-Math.PI / 2)
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x18202a, transparent: true, opacity: 0.92, depthWrite: false, side: THREE.DoubleSide,
|
||||
})
|
||||
const plane = new THREE.Mesh(geometry, material)
|
||||
plane.position.set(center.x, -0.12, center.z)
|
||||
plane.renderOrder = -10
|
||||
this.mapGroup.add(plane)
|
||||
this._mapMaterial = material
|
||||
}
|
||||
|
||||
_applyPlaneUvs(geometry, rect) {
|
||||
const u0 = rect.u
|
||||
const u1 = rect.u + rect.w
|
||||
const vTop = rect.v
|
||||
const vBottom = rect.v + rect.h
|
||||
const uv = geometry.attributes.uv
|
||||
if (FLIP_MAP_TEXTURE_Z) {
|
||||
uv.setXY(0, u0, 1 - vTop); uv.setXY(1, u1, 1 - vTop)
|
||||
uv.setXY(2, u0, 1 - vBottom); uv.setXY(3, u1, 1 - vBottom)
|
||||
} else {
|
||||
uv.setXY(0, u0, 1 - vBottom); uv.setXY(1, u1, 1 - vBottom)
|
||||
uv.setXY(2, u0, 1 - vTop); uv.setXY(3, u1, 1 - vTop)
|
||||
}
|
||||
uv.needsUpdate = true
|
||||
}
|
||||
|
||||
_loadMapTexture() {
|
||||
if (!this.mapInfo.image || !this._mapMaterial) return
|
||||
const material = this._mapMaterial
|
||||
new THREE.TextureLoader().load(
|
||||
this.mapInfo.image,
|
||||
(texture) => {
|
||||
if (this.disposed) { texture.dispose(); return }
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy()
|
||||
material.map = texture
|
||||
material.color.setHex(0xffffff)
|
||||
material.needsUpdate = true
|
||||
},
|
||||
undefined,
|
||||
() => { material.color.setHex(0x1f2933); material.opacity = 0.6 },
|
||||
)
|
||||
}
|
||||
|
||||
_makeVisualEntity(entity) {
|
||||
const color = this._colorFor(entity)
|
||||
const points = entity.path.map((p) => this._toWorld(this._renderPoint(p)))
|
||||
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points)
|
||||
const lineMaterial = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.42 })
|
||||
const line = new THREE.Line(lineGeometry, lineMaterial)
|
||||
this.pathGroup.add(line)
|
||||
|
||||
const radius = 4.4
|
||||
let model
|
||||
if (this.unitTemplate && entity.type === 'ground') {
|
||||
model = this.unitTemplate.clone(true)
|
||||
model.traverse((child) => {
|
||||
if (!child.isMesh) return
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color, roughness: 0.72, metalness: 0.06, flatShading: true,
|
||||
emissive: new THREE.Color(color).multiplyScalar(0.22),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const geo = new THREE.SphereGeometry(radius, 18, 12)
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color, emissive: new THREE.Color(color).multiplyScalar(0.24), roughness: 0.45, metalness: 0.08,
|
||||
})
|
||||
model = new THREE.Mesh(geo, mat)
|
||||
}
|
||||
|
||||
const arrow = new THREE.ArrowHelper(
|
||||
new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 0),
|
||||
radius * 4.6, new THREE.Color(color), radius * 1.55, radius * 0.9,
|
||||
)
|
||||
arrow.line.material.transparent = true
|
||||
arrow.line.material.opacity = 0.78
|
||||
arrow.cone.material.transparent = true
|
||||
arrow.cone.material.opacity = 0.95
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model, arrow)
|
||||
this.markerGroup.add(group)
|
||||
|
||||
return { entity, line, group, model, arrow, radius, smoothedDirection: null, lastDirectionT: null }
|
||||
}
|
||||
|
||||
async _loadTankTemplate() {
|
||||
try {
|
||||
const object = await new Promise((resolve, reject) => {
|
||||
const loader = new OBJLoader()
|
||||
loader.setPath(MODEL_PATH)
|
||||
loader.load('t_34_obj.obj', resolve, undefined, reject)
|
||||
})
|
||||
if (this.disposed) return
|
||||
|
||||
const planes = []
|
||||
object.traverse((child) => {
|
||||
if (!child.isMesh) return
|
||||
child.castShadow = false
|
||||
child.receiveShadow = false
|
||||
if (/^Plane/i.test(child.name || '')) { planes.push(child); return }
|
||||
})
|
||||
for (const plane of planes) plane.removeFromParent()
|
||||
|
||||
const box = new THREE.Box3()
|
||||
object.traverse((child) => { if (child.isMesh) box.expandByObject(child) })
|
||||
if (box.isEmpty()) box.setFromObject(object)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const maxDim = Math.max(size.x, size.y, size.z, 1)
|
||||
const scale = 70 / maxDim
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
const pivot = new THREE.Group()
|
||||
object.position.x -= center.x
|
||||
object.position.z -= center.z
|
||||
object.position.y -= box.min.y
|
||||
pivot.add(object)
|
||||
pivot.scale.setScalar(scale)
|
||||
this.unitTemplate = pivot
|
||||
|
||||
// Rebuild ground entities now that the model is available.
|
||||
this._buildScene()
|
||||
this.setTime(this.currentT)
|
||||
} catch {
|
||||
// No model -> spheres are used; nothing else to do.
|
||||
}
|
||||
}
|
||||
|
||||
// ---- capture zones -------------------------------------------------------
|
||||
|
||||
_buildCaptureZones() {
|
||||
for (const zone of this.captureZones) {
|
||||
const center = this._toWorld(this._renderPoint(zone.center))
|
||||
const radius = Math.max(8, zone.radius * this.scale)
|
||||
const group = new THREE.Group()
|
||||
group.position.set(center.x, 1.05, center.z)
|
||||
|
||||
const ringGeo = new THREE.RingGeometry(radius * 0.92, radius, 96)
|
||||
ringGeo.rotateX(-Math.PI / 2)
|
||||
const ring = new THREE.Mesh(ringGeo, new THREE.MeshBasicMaterial({
|
||||
color: 0xdde6ee, transparent: true, opacity: 0.7, depthWrite: false, side: THREE.DoubleSide,
|
||||
}))
|
||||
ring.renderOrder = 8
|
||||
|
||||
const fillGeo = new THREE.CircleGeometry(radius * 0.82, 96)
|
||||
fillGeo.rotateX(-Math.PI / 2)
|
||||
const fill = new THREE.Mesh(fillGeo, new THREE.MeshBasicMaterial({
|
||||
color: 0xd8dee9, transparent: true, opacity: 0, depthWrite: false, side: THREE.DoubleSide,
|
||||
}))
|
||||
fill.position.y = 0.02
|
||||
fill.renderOrder = 7
|
||||
|
||||
group.add(fill, ring)
|
||||
group.userData = { zone, fill }
|
||||
this.zoneGroup.add(group)
|
||||
}
|
||||
}
|
||||
|
||||
_captureValueAt(zone, t) {
|
||||
if (!zone.cap.length) return 0
|
||||
if (t <= zone.cap[0][0]) return zone.cap[0][1]
|
||||
for (let i = 1; i < zone.cap.length; i++) {
|
||||
const prev = zone.cap[i - 1]
|
||||
const cur = zone.cap[i]
|
||||
if (t <= cur[0]) {
|
||||
const gap = cur[0] - prev[0]
|
||||
if (gap > CAP_INTERP_MAX_GAP_MS) return prev[1]
|
||||
const alpha = (t - prev[0]) / Math.max(1, gap)
|
||||
return THREE.MathUtils.lerp(prev[1], cur[1], THREE.MathUtils.clamp(alpha, 0, 1))
|
||||
}
|
||||
}
|
||||
return zone.cap[zone.cap.length - 1][1]
|
||||
}
|
||||
|
||||
_updateCaptureZones() {
|
||||
for (const group of this.zoneGroup.children) {
|
||||
const { zone, fill } = group.userData
|
||||
if (!zone || !fill) continue
|
||||
const value = THREE.MathUtils.clamp(this._captureValueAt(zone, this.currentT), -100, 100)
|
||||
const amount = Math.abs(value) / 100
|
||||
fill.visible = value !== 0
|
||||
fill.material.color.set(value > 0 ? LOSE_COLOR : value < 0 ? WIN_COLOR : 0xd8dee9)
|
||||
fill.material.opacity = value === 0 ? 0 : 0.18 + amount * 0.34
|
||||
const s = value === 0 ? 0 : Math.sqrt(amount)
|
||||
fill.scale.set(s, s, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- kill lines ----------------------------------------------------------
|
||||
|
||||
_buildKillLines() {
|
||||
this.killLines = []
|
||||
for (const k of this.kills) {
|
||||
const start = this._toWorld(this._renderPoint({ x: k.killerPos.x, y: this.bounds.minY, z: k.killerPos.z }))
|
||||
const end = this._toWorld(this._renderPoint({ x: k.victimPos.x, y: this.bounds.minY, z: k.victimPos.z }))
|
||||
start.y += 12; end.y += 12
|
||||
const vec = end.clone().sub(start)
|
||||
const length = vec.length()
|
||||
if (length < 0.5) continue
|
||||
const color = new THREE.Color(this.players[k.killerId]?.team === this.teamWon ? WIN_COLOR : LOSE_COLOR)
|
||||
const arrow = new THREE.ArrowHelper(vec.clone().normalize(), start, length, color,
|
||||
Math.min(28, Math.max(10, length * 0.12)), Math.min(16, Math.max(7, length * 0.055)))
|
||||
for (const m of [arrow.line.material, arrow.cone.material]) {
|
||||
m.transparent = true; m.opacity = 0; m.depthTest = false; m.depthWrite = false
|
||||
}
|
||||
arrow.renderOrder = 18
|
||||
arrow.visible = false
|
||||
this.killLineGroup.add(arrow)
|
||||
this.killLines.push({ time: k.time, arrow })
|
||||
}
|
||||
}
|
||||
|
||||
_updateKillLines() {
|
||||
for (const { time, arrow } of this.killLines) {
|
||||
const age = this.currentT - time
|
||||
const visible = age >= 0 && age <= KILL_LINE_WINDOW_MS
|
||||
arrow.visible = visible
|
||||
if (!visible) continue
|
||||
const opacity = 1 - age / KILL_LINE_WINDOW_MS
|
||||
arrow.line.material.opacity = 0.18 + opacity * 0.62
|
||||
arrow.cone.material.opacity = 0.24 + opacity * 0.72
|
||||
}
|
||||
}
|
||||
|
||||
// ---- interpolation -------------------------------------------------------
|
||||
|
||||
_findSegment(entity, t) {
|
||||
const path = entity.path
|
||||
if (t < entity.startT || t > entity.endT) return null
|
||||
let lo = 0, hi = path.length - 1
|
||||
while (lo < hi - 1) {
|
||||
const mid = (lo + hi) >> 1
|
||||
if (path[mid].t <= t) lo = mid; else hi = mid
|
||||
}
|
||||
return { index: lo, a: path[lo], b: path[Math.min(lo + 1, path.length - 1)] }
|
||||
}
|
||||
|
||||
_interp(entity, t) {
|
||||
const seg = this._findSegment(entity, t)
|
||||
if (!seg) return null
|
||||
const { index, a, b } = seg
|
||||
const path = entity.path
|
||||
const span = Math.max(1, b.t - a.t)
|
||||
const alpha = Math.min(1, Math.max(0, (t - a.t) / span))
|
||||
const before = path[Math.max(0, index - 1)]
|
||||
const after = path[Math.min(path.length - 1, index + 2)]
|
||||
return {
|
||||
t,
|
||||
x: catmullRom(before.x, a.x, b.x, after.x, alpha),
|
||||
y: catmullRom(before.y, a.y, b.y, after.y, alpha),
|
||||
z: catmullRom(before.z, a.z, b.z, after.z, alpha),
|
||||
}
|
||||
}
|
||||
|
||||
_directionAt(entity, t) {
|
||||
const beforeT = Math.max(entity.startT, t - DIRECTION_SAMPLE_MS)
|
||||
const afterT = Math.min(entity.endT, t + DIRECTION_SAMPLE_MS)
|
||||
if (afterT <= beforeT) return null
|
||||
const before = this._interp(entity, beforeT)
|
||||
const after = this._interp(entity, afterT)
|
||||
if (!before || !after) return null
|
||||
const dir = this._toWorld(this._renderPoint(after)).sub(this._toWorld(this._renderPoint(before)))
|
||||
if (dir.lengthSq() < 0.0001) return null
|
||||
return dir.normalize()
|
||||
}
|
||||
|
||||
_blendedDirection(visual, t) {
|
||||
const raw = this._directionAt(visual.entity, t)
|
||||
if (!raw) return visual.smoothedDirection
|
||||
const reset = visual.lastDirectionT == null || Math.abs(t - visual.lastDirectionT) > 1800
|
||||
if (reset || !visual.smoothedDirection) visual.smoothedDirection = raw.clone()
|
||||
else visual.smoothedDirection.lerp(raw, DIRECTION_BLEND).normalize()
|
||||
visual.lastDirectionT = t
|
||||
return visual.smoothedDirection
|
||||
}
|
||||
|
||||
// ---- highlight / focus ---------------------------------------------------
|
||||
|
||||
_applyHighlight() {
|
||||
const sel = this.selectedPlayerId
|
||||
if (!this.visualEntities) return
|
||||
for (const visual of this.visualEntities) {
|
||||
const isSel = sel != null && visual.entity.playerId === sel
|
||||
const teamColor = this._colorFor(visual.entity)
|
||||
const mat = visual.line.material
|
||||
if (sel == null) {
|
||||
mat.opacity = 0.42; mat.color.set(teamColor); visual.line.renderOrder = 0
|
||||
} else if (isSel) {
|
||||
mat.opacity = 1; mat.color.set(teamColor).lerp(new THREE.Color(0xffffff), 0.35); visual.line.renderOrder = 30
|
||||
} else {
|
||||
mat.opacity = 0.08; mat.color.set(teamColor); visual.line.renderOrder = 0
|
||||
}
|
||||
if (visual.model?.isGroup) {
|
||||
visual.model.traverse((child) => {
|
||||
if (child.isMesh && child.material?.emissive) child.material.emissiveIntensity = isSel ? 2.6 : 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus(playerId) {
|
||||
this.selectedPlayerId = this.selectedPlayerId === playerId ? null : playerId
|
||||
this._applyHighlight()
|
||||
if (this.selectedPlayerId == null) return
|
||||
const visual = (this.visualEntities || []).find((v) => v.entity.playerId === playerId && v.group.visible)
|
||||
|| (this.visualEntities || []).find((v) => v.entity.playerId === playerId)
|
||||
if (!visual) return
|
||||
const p = this._interp(visual.entity, this.currentT)
|
||||
if (!p) return
|
||||
const world = this._toWorld(this._renderPoint(p))
|
||||
const offset = this.camera.position.clone().sub(this.controls.target)
|
||||
this.controls.target.copy(world)
|
||||
this.camera.position.copy(world.clone().add(offset))
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
// ---- public API ----------------------------------------------------------
|
||||
|
||||
setTime(t) {
|
||||
this.currentT = t
|
||||
if (!this.visualEntities) return
|
||||
for (const visual of this.visualEntities) {
|
||||
const p = this._interp(visual.entity, t)
|
||||
visual.group.visible = Boolean(p)
|
||||
if (!p) continue
|
||||
visual.group.position.copy(this._toWorld(this._renderPoint(p)))
|
||||
const dir = this._blendedDirection(visual, t)
|
||||
visual.arrow.visible = Boolean(dir)
|
||||
if (dir) {
|
||||
visual.arrow.setDirection(dir)
|
||||
if (visual.model?.isGroup) {
|
||||
const flat = dir.clone(); flat.y = 0
|
||||
if (flat.lengthSq() > 0) {
|
||||
flat.normalize()
|
||||
visual.model.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), flat)
|
||||
visual.model.rotateY(Math.PI)
|
||||
visual.model.rotateY(-Math.PI / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._updateCaptureZones()
|
||||
this._updateKillLines()
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
const next = mode === 'air' && normalizeCoords(this.data.mapCoords) ? 'air' : 'ground'
|
||||
if (next === this._mode) return
|
||||
this._mode = next
|
||||
this.mapInfo = this._resolveMapInfo(next)
|
||||
this.smoothByEntity.clear()
|
||||
this._buildScene()
|
||||
this._loadMapTexture()
|
||||
this.setTime(this.currentT)
|
||||
}
|
||||
|
||||
resize() {
|
||||
const w = this.container.clientWidth || this.container.offsetWidth || 720
|
||||
const h = this.container.clientHeight || w
|
||||
this.renderer.setSize(w, h, false)
|
||||
this.camera.aspect = w / Math.max(1, h)
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
_animate() {
|
||||
if (this.disposed) return
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
this._raf = requestAnimationFrame(this._animate)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposed = true
|
||||
if (this._raf) cancelAnimationFrame(this._raf)
|
||||
this.controls?.dispose()
|
||||
this._clearGroup(this.mapGroup)
|
||||
this._clearGroup(this.pathGroup)
|
||||
this._clearGroup(this.markerGroup)
|
||||
this._clearGroup(this.zoneGroup)
|
||||
this._clearGroup(this.killLineGroup)
|
||||
this.renderer?.dispose()
|
||||
if (this.canvasEl?.parentNode) this.canvasEl.parentNode.removeChild(this.canvasEl)
|
||||
}
|
||||
}
|
||||
|
||||
window.ReplayCanvas3D = ReplayCanvas3D
|
||||
@@ -53,6 +53,9 @@ class ReplayCanvas {
|
||||
this._teamNames = (data.teamNames && typeof data.teamNames === 'object') ? data.teamNames : {};
|
||||
this._winnerSlot = Number(data.winnerSlot) || 0; // winning team slot (1/2)
|
||||
this._mode = 'ground';
|
||||
this._viewMode = '2d';
|
||||
this.view3d = null;
|
||||
this.supports3d = false;
|
||||
const hasAircraft = data.entities.some(e => e.type === 'aircraft');
|
||||
this.hasAirMode = !!(this._airCoords && this._fullMapLevel && hasAircraft);
|
||||
|
||||
@@ -166,6 +169,9 @@ class ReplayCanvas {
|
||||
async init() {
|
||||
this._buildDOM();
|
||||
await Promise.all([this._loadMap(), this._loadEntityIcons()]);
|
||||
this._initView3d();
|
||||
this._onResize = () => { if (this._viewMode === '3d') this.view3d?.resize(); };
|
||||
window.addEventListener('resize', this._onResize);
|
||||
this.playing = true;
|
||||
this.playBtn.innerHTML = '<i class="fas fa-pause"></i>';
|
||||
this.lastFrameTime = performance.now();
|
||||
@@ -173,6 +179,42 @@ class ReplayCanvas {
|
||||
this.animFrameId = requestAnimationFrame(this._tick);
|
||||
}
|
||||
|
||||
_initView3d() {
|
||||
try {
|
||||
if (typeof window.ReplayCanvas3D !== 'function') return;
|
||||
this.view3d = new window.ReplayCanvas3D(this.view3dContainer, this.data);
|
||||
this.supports3d = true;
|
||||
} catch (e) {
|
||||
this.view3d = null;
|
||||
this.supports3d = false;
|
||||
}
|
||||
}
|
||||
|
||||
_renderActive() {
|
||||
if (this._viewMode === '3d') { this.view3d?.setTime(this.currentTime); }
|
||||
else { this.render(); }
|
||||
}
|
||||
|
||||
setViewMode(mode) {
|
||||
const next = mode === '3d' && this.view3d ? '3d' : '2d';
|
||||
if (next === this._viewMode) return;
|
||||
this._viewMode = next;
|
||||
const is3d = next === '3d';
|
||||
this.canvas.classList.toggle('rc-hidden', is3d);
|
||||
this.view3dContainer.classList.toggle('rc-hidden', !is3d);
|
||||
if (is3d) {
|
||||
this.view3d.setMode(this._mode);
|
||||
this.view3d.resize();
|
||||
this.view3d.setTime(this.currentTime);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
focus(playerId) {
|
||||
if (this._viewMode === '3d') this.view3d?.focus(playerId);
|
||||
}
|
||||
|
||||
_buildDOM() {
|
||||
this.container.innerHTML = '';
|
||||
const layout = document.createElement('div');
|
||||
@@ -190,12 +232,18 @@ class ReplayCanvas {
|
||||
// Tickets meter above the battle view (its top aligns with the team panels)
|
||||
this._buildTicketsBar(center);
|
||||
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'rc-stage';
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = this.canvasSize;
|
||||
this.canvas.height = this.canvasSize;
|
||||
this.canvas.className = 'rc-canvas';
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
center.appendChild(this.canvas);
|
||||
stage.appendChild(this.canvas);
|
||||
this.view3dContainer = document.createElement('div');
|
||||
this.view3dContainer.className = 'rc-3d-container rc-hidden';
|
||||
stage.appendChild(this.view3dContainer);
|
||||
center.appendChild(stage);
|
||||
|
||||
// Controls
|
||||
const controls = document.createElement('div');
|
||||
@@ -243,7 +291,7 @@ class ReplayCanvas {
|
||||
this.currentTime = this.tStart + (this.scrubber.value / 1000) * (this.tEnd - this.tStart);
|
||||
this._updatePanelDeathStates();
|
||||
this._updateBattleLog();
|
||||
this.render();
|
||||
this._renderActive();
|
||||
this._updateTicketsBar(this.currentTime);
|
||||
});
|
||||
controls.querySelectorAll('.rc-sp').forEach(btn => {
|
||||
@@ -331,6 +379,7 @@ class ReplayCanvas {
|
||||
}
|
||||
});
|
||||
row.addEventListener('mouseleave', () => this._setHighlight(null));
|
||||
row.addEventListener('click', () => this.focus(parseInt(row.dataset.playerId)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -873,8 +922,9 @@ class ReplayCanvas {
|
||||
this._drawMapToCanvas();
|
||||
// Recompute death positions in new coordinate space
|
||||
this._computeDeaths();
|
||||
this.view3d?.setMode(mode);
|
||||
// Render immediately
|
||||
this.render();
|
||||
this._renderActive();
|
||||
}
|
||||
|
||||
_togglePlay() {
|
||||
@@ -897,7 +947,7 @@ class ReplayCanvas {
|
||||
this.playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||||
}
|
||||
}
|
||||
this.render();
|
||||
this._renderActive();
|
||||
this._updateTicketsBar(this.currentTime);
|
||||
this._updateControls();
|
||||
// Update panel death states + battle log every ~250ms
|
||||
@@ -1145,6 +1195,9 @@ class ReplayCanvas {
|
||||
|
||||
destroy() {
|
||||
if (this.animFrameId) { cancelAnimationFrame(this.animFrameId); this.animFrameId = null; }
|
||||
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||
this.view3d?.dispose();
|
||||
this.view3d = null;
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
|
||||
# File Created: 27.04.2017 18:23:39
|
||||
|
||||
newmtl wire_022022022
|
||||
Ns 32
|
||||
d 1
|
||||
Tr 0
|
||||
Tf 1 1 1
|
||||
illum 2
|
||||
Ka 0.0863 0.0863 0.0863
|
||||
Kd 0.0863 0.0863 0.0863
|
||||
Ks 0.3500 0.3500 0.3500
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+955
@@ -0,0 +1,955 @@
|
||||
import {
|
||||
BufferGeometry,
|
||||
FileLoader,
|
||||
Float32BufferAttribute,
|
||||
Group,
|
||||
LineBasicMaterial,
|
||||
LineSegments,
|
||||
Loader,
|
||||
Material,
|
||||
Mesh,
|
||||
MeshPhongMaterial,
|
||||
Points,
|
||||
PointsMaterial,
|
||||
Vector3,
|
||||
Color,
|
||||
SRGBColorSpace
|
||||
} from 'three';
|
||||
|
||||
// o object_name | g group_name
|
||||
const _object_pattern = /^[og]\s*(.+)?/;
|
||||
// mtllib file_reference
|
||||
const _material_library_pattern = /^mtllib /;
|
||||
// usemtl material_name
|
||||
const _material_use_pattern = /^usemtl /;
|
||||
// usemap map_name
|
||||
const _map_use_pattern = /^usemap /;
|
||||
const _face_vertex_data_separator_pattern = /\s+/;
|
||||
|
||||
const _vA = new Vector3();
|
||||
const _vB = new Vector3();
|
||||
const _vC = new Vector3();
|
||||
|
||||
const _ab = new Vector3();
|
||||
const _cb = new Vector3();
|
||||
|
||||
const _color = new Color();
|
||||
|
||||
function ParserState() {
|
||||
|
||||
const state = {
|
||||
objects: [],
|
||||
object: {},
|
||||
|
||||
vertices: [],
|
||||
normals: [],
|
||||
colors: [],
|
||||
uvs: [],
|
||||
|
||||
materials: {},
|
||||
materialLibraries: [],
|
||||
|
||||
startObject: function ( name, fromDeclaration ) {
|
||||
|
||||
// If the current object (initial from reset) is not from a g/o declaration in the parsed
|
||||
// file. We need to use it for the first parsed g/o to keep things in sync.
|
||||
if ( this.object && this.object.fromDeclaration === false ) {
|
||||
|
||||
this.object.name = name;
|
||||
this.object.fromDeclaration = ( fromDeclaration !== false );
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
|
||||
|
||||
if ( this.object && typeof this.object._finalize === 'function' ) {
|
||||
|
||||
this.object._finalize( true );
|
||||
|
||||
}
|
||||
|
||||
this.object = {
|
||||
name: name || '',
|
||||
fromDeclaration: ( fromDeclaration !== false ),
|
||||
|
||||
geometry: {
|
||||
vertices: [],
|
||||
normals: [],
|
||||
colors: [],
|
||||
uvs: [],
|
||||
hasUVIndices: false
|
||||
},
|
||||
materials: [],
|
||||
smooth: true,
|
||||
|
||||
startMaterial: function ( name, libraries ) {
|
||||
|
||||
const previous = this._finalize( false );
|
||||
|
||||
// New usemtl declaration overwrites an inherited material, except if faces were declared
|
||||
// after the material, then it must be preserved for proper MultiMaterial continuation.
|
||||
if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {
|
||||
|
||||
this.materials.splice( previous.index, 1 );
|
||||
|
||||
}
|
||||
|
||||
const material = {
|
||||
index: this.materials.length,
|
||||
name: name || '',
|
||||
mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
|
||||
smooth: ( previous !== undefined ? previous.smooth : this.smooth ),
|
||||
groupStart: ( previous !== undefined ? previous.groupEnd : 0 ),
|
||||
groupEnd: - 1,
|
||||
groupCount: - 1,
|
||||
inherited: false,
|
||||
|
||||
clone: function ( index ) {
|
||||
|
||||
const cloned = {
|
||||
index: ( typeof index === 'number' ? index : this.index ),
|
||||
name: this.name,
|
||||
mtllib: this.mtllib,
|
||||
smooth: this.smooth,
|
||||
groupStart: 0,
|
||||
groupEnd: - 1,
|
||||
groupCount: - 1,
|
||||
inherited: false
|
||||
};
|
||||
cloned.clone = this.clone.bind( cloned );
|
||||
return cloned;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
this.materials.push( material );
|
||||
|
||||
return material;
|
||||
|
||||
},
|
||||
|
||||
currentMaterial: function () {
|
||||
|
||||
if ( this.materials.length > 0 ) {
|
||||
|
||||
return this.materials[ this.materials.length - 1 ];
|
||||
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
},
|
||||
|
||||
_finalize: function ( end ) {
|
||||
|
||||
const lastMultiMaterial = this.currentMaterial();
|
||||
if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {
|
||||
|
||||
lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
|
||||
lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
|
||||
lastMultiMaterial.inherited = false;
|
||||
|
||||
}
|
||||
|
||||
// Ignore objects tail materials if no face declarations followed them before a new o/g started.
|
||||
if ( end && this.materials.length > 1 ) {
|
||||
|
||||
for ( let mi = this.materials.length - 1; mi >= 0; mi -- ) {
|
||||
|
||||
if ( this.materials[ mi ].groupCount <= 0 ) {
|
||||
|
||||
this.materials.splice( mi, 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Guarantee at least one empty material, this makes the creation later more straight forward.
|
||||
if ( end && this.materials.length === 0 ) {
|
||||
|
||||
this.materials.push( {
|
||||
name: '',
|
||||
smooth: this.smooth
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
return lastMultiMaterial;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Inherit previous objects material.
|
||||
// Spec tells us that a declared material must be set to all objects until a new material is declared.
|
||||
// If a usemtl declaration is encountered while this new object is being parsed, it will
|
||||
// overwrite the inherited material. Exception being that there was already face declarations
|
||||
// to the inherited material, then it will be preserved for proper MultiMaterial continuation.
|
||||
|
||||
if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {
|
||||
|
||||
const declared = previousMaterial.clone( 0 );
|
||||
declared.inherited = true;
|
||||
this.object.materials.push( declared );
|
||||
|
||||
}
|
||||
|
||||
this.objects.push( this.object );
|
||||
|
||||
},
|
||||
|
||||
finalize: function () {
|
||||
|
||||
if ( this.object && typeof this.object._finalize === 'function' ) {
|
||||
|
||||
this.object._finalize( true );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
parseVertexIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
|
||||
|
||||
},
|
||||
|
||||
parseNormalIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
|
||||
|
||||
},
|
||||
|
||||
parseUVIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
|
||||
|
||||
},
|
||||
|
||||
addVertex: function ( a, b, c ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addVertexPoint: function ( a ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addVertexLine: function ( a ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addNormal: function ( a, b, c ) {
|
||||
|
||||
const src = this.normals;
|
||||
const dst = this.object.geometry.normals;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addFaceNormal: function ( a, b, c ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.normals;
|
||||
|
||||
_vA.fromArray( src, a );
|
||||
_vB.fromArray( src, b );
|
||||
_vC.fromArray( src, c );
|
||||
|
||||
_cb.subVectors( _vC, _vB );
|
||||
_ab.subVectors( _vA, _vB );
|
||||
_cb.cross( _ab );
|
||||
|
||||
_cb.normalize();
|
||||
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
|
||||
},
|
||||
|
||||
addColor: function ( a, b, c ) {
|
||||
|
||||
const src = this.colors;
|
||||
const dst = this.object.geometry.colors;
|
||||
|
||||
if ( src[ a ] !== undefined ) dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
if ( src[ b ] !== undefined ) dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
if ( src[ c ] !== undefined ) dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addUV: function ( a, b, c ) {
|
||||
|
||||
const src = this.uvs;
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ] );
|
||||
|
||||
},
|
||||
|
||||
addDefaultUV: function () {
|
||||
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( 0, 0 );
|
||||
dst.push( 0, 0 );
|
||||
dst.push( 0, 0 );
|
||||
|
||||
},
|
||||
|
||||
addUVLine: function ( a ) {
|
||||
|
||||
const src = this.uvs;
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ] );
|
||||
|
||||
},
|
||||
|
||||
addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
|
||||
let ia = this.parseVertexIndex( a, vLen );
|
||||
let ib = this.parseVertexIndex( b, vLen );
|
||||
let ic = this.parseVertexIndex( c, vLen );
|
||||
|
||||
this.addVertex( ia, ib, ic );
|
||||
this.addColor( ia, ib, ic );
|
||||
|
||||
// normals
|
||||
|
||||
if ( na !== undefined && na !== '' ) {
|
||||
|
||||
const nLen = this.normals.length;
|
||||
|
||||
ia = this.parseNormalIndex( na, nLen );
|
||||
ib = this.parseNormalIndex( nb, nLen );
|
||||
ic = this.parseNormalIndex( nc, nLen );
|
||||
|
||||
this.addNormal( ia, ib, ic );
|
||||
|
||||
} else {
|
||||
|
||||
this.addFaceNormal( ia, ib, ic );
|
||||
|
||||
}
|
||||
|
||||
// uvs
|
||||
|
||||
if ( ua !== undefined && ua !== '' ) {
|
||||
|
||||
const uvLen = this.uvs.length;
|
||||
|
||||
ia = this.parseUVIndex( ua, uvLen );
|
||||
ib = this.parseUVIndex( ub, uvLen );
|
||||
ic = this.parseUVIndex( uc, uvLen );
|
||||
|
||||
this.addUV( ia, ib, ic );
|
||||
|
||||
this.object.geometry.hasUVIndices = true;
|
||||
|
||||
} else {
|
||||
|
||||
// add placeholder values (for inconsistent face definitions)
|
||||
|
||||
this.addDefaultUV();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
addPointGeometry: function ( vertices ) {
|
||||
|
||||
this.object.geometry.type = 'Points';
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
|
||||
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
|
||||
|
||||
const index = this.parseVertexIndex( vertices[ vi ], vLen );
|
||||
|
||||
this.addVertexPoint( index );
|
||||
this.addColor( index );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
addLineGeometry: function ( vertices, uvs ) {
|
||||
|
||||
this.object.geometry.type = 'Line';
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
const uvLen = this.uvs.length;
|
||||
|
||||
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
|
||||
|
||||
this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );
|
||||
|
||||
}
|
||||
|
||||
for ( let uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {
|
||||
|
||||
this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
state.startObject( '', false );
|
||||
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A loader for the OBJ format.
|
||||
*
|
||||
* The [OBJ format](https://en.wikipedia.org/wiki/Wavefront_.obj_file) is a simple data-format that
|
||||
* represents 3D geometry in a human readable format as the position of each vertex, the UV position of
|
||||
* each texture coordinate vertex, vertex normals, and the faces that make each polygon defined as a list
|
||||
* of vertices, and texture vertices.
|
||||
*
|
||||
* ```js
|
||||
* const loader = new OBJLoader();
|
||||
* const object = await loader.loadAsync( 'models/monster.obj' );
|
||||
* scene.add( object );
|
||||
* ```
|
||||
*
|
||||
* @augments Loader
|
||||
* @three_import import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
||||
*/
|
||||
class OBJLoader extends Loader {
|
||||
|
||||
/**
|
||||
* Constructs a new OBJ loader.
|
||||
*
|
||||
* @param {LoadingManager} [manager] - The loading manager.
|
||||
*/
|
||||
constructor( manager ) {
|
||||
|
||||
super( manager );
|
||||
|
||||
/**
|
||||
* A reference to a material creator.
|
||||
*
|
||||
* @type {?MaterialCreator}
|
||||
* @default null
|
||||
*/
|
||||
this.materials = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loading from the given URL and passes the loaded OBJ asset
|
||||
* to the `onLoad()` callback.
|
||||
*
|
||||
* @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
|
||||
* @param {function(Group)} onLoad - Executed when the loading process has been finished.
|
||||
* @param {onProgressCallback} onProgress - Executed while the loading is in progress.
|
||||
* @param {onErrorCallback} onError - Executed when errors occur.
|
||||
*/
|
||||
load( url, onLoad, onProgress, onError ) {
|
||||
|
||||
const scope = this;
|
||||
|
||||
const loader = new FileLoader( this.manager );
|
||||
loader.setPath( this.path );
|
||||
loader.setRequestHeader( this.requestHeader );
|
||||
loader.setWithCredentials( this.withCredentials );
|
||||
loader.load( url, function ( text ) {
|
||||
|
||||
try {
|
||||
|
||||
onLoad( scope.parse( text ) );
|
||||
|
||||
} catch ( e ) {
|
||||
|
||||
if ( onError ) {
|
||||
|
||||
onError( e );
|
||||
|
||||
} else {
|
||||
|
||||
console.error( e );
|
||||
|
||||
}
|
||||
|
||||
scope.manager.itemError( url );
|
||||
|
||||
}
|
||||
|
||||
}, onProgress, onError );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the material creator for this OBJ. This object is loaded via {@link MTLLoader}.
|
||||
*
|
||||
* @param {MaterialCreator} materials - An object that creates the materials for this OBJ.
|
||||
* @return {OBJLoader} A reference to this loader.
|
||||
*/
|
||||
setMaterials( materials ) {
|
||||
|
||||
this.materials = materials;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given OBJ data and returns the resulting group.
|
||||
*
|
||||
* @param {string} text - The raw OBJ data as a string.
|
||||
* @return {Group} The parsed OBJ.
|
||||
*/
|
||||
parse( text ) {
|
||||
|
||||
const state = new ParserState();
|
||||
|
||||
if ( text.indexOf( '\r\n' ) !== - 1 ) {
|
||||
|
||||
// This is faster than String.split with regex that splits on both
|
||||
text = text.replace( /\r\n/g, '\n' );
|
||||
|
||||
}
|
||||
|
||||
if ( text.indexOf( '\\\n' ) !== - 1 ) {
|
||||
|
||||
// join lines separated by a line continuation character (\)
|
||||
text = text.replace( /\\\n/g, '' );
|
||||
|
||||
}
|
||||
|
||||
const lines = text.split( '\n' );
|
||||
let result = [];
|
||||
|
||||
for ( let i = 0, l = lines.length; i < l; i ++ ) {
|
||||
|
||||
const line = lines[ i ].trimStart();
|
||||
|
||||
if ( line.length === 0 ) continue;
|
||||
|
||||
const lineFirstChar = line.charAt( 0 );
|
||||
|
||||
// @todo invoke passed in handler if any
|
||||
if ( lineFirstChar === '#' ) continue; // skip comments
|
||||
|
||||
if ( lineFirstChar === 'v' ) {
|
||||
|
||||
const data = line.split( _face_vertex_data_separator_pattern );
|
||||
|
||||
switch ( data[ 0 ] ) {
|
||||
|
||||
case 'v':
|
||||
state.vertices.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] ),
|
||||
parseFloat( data[ 3 ] )
|
||||
);
|
||||
if ( data.length >= 7 ) {
|
||||
|
||||
_color.setRGB(
|
||||
parseFloat( data[ 4 ] ),
|
||||
parseFloat( data[ 5 ] ),
|
||||
parseFloat( data[ 6 ] ),
|
||||
SRGBColorSpace
|
||||
);
|
||||
|
||||
state.colors.push( _color.r, _color.g, _color.b );
|
||||
|
||||
} else {
|
||||
|
||||
// if no colors are defined, add placeholders so color and vertex indices match
|
||||
|
||||
state.colors.push( undefined, undefined, undefined );
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
case 'vn':
|
||||
state.normals.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] ),
|
||||
parseFloat( data[ 3 ] )
|
||||
);
|
||||
break;
|
||||
case 'vt':
|
||||
state.uvs.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] )
|
||||
);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
} else if ( lineFirstChar === 'f' ) {
|
||||
|
||||
const lineData = line.slice( 1 ).trim();
|
||||
const vertexData = lineData.split( _face_vertex_data_separator_pattern );
|
||||
const faceVertices = [];
|
||||
|
||||
// Parse the face vertex data into an easy to work with format
|
||||
|
||||
for ( let j = 0, jl = vertexData.length; j < jl; j ++ ) {
|
||||
|
||||
const vertex = vertexData[ j ];
|
||||
|
||||
if ( vertex.length > 0 ) {
|
||||
|
||||
const vertexParts = vertex.split( '/' );
|
||||
faceVertices.push( vertexParts );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draw an edge between the first vertex and all subsequent vertices to form an n-gon
|
||||
|
||||
const v1 = faceVertices[ 0 ];
|
||||
|
||||
for ( let j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {
|
||||
|
||||
const v2 = faceVertices[ j ];
|
||||
const v3 = faceVertices[ j + 1 ];
|
||||
|
||||
state.addFace(
|
||||
v1[ 0 ], v2[ 0 ], v3[ 0 ],
|
||||
v1[ 1 ], v2[ 1 ], v3[ 1 ],
|
||||
v1[ 2 ], v2[ 2 ], v3[ 2 ]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
} else if ( lineFirstChar === 'l' ) {
|
||||
|
||||
const lineParts = line.substring( 1 ).trim().split( ' ' );
|
||||
let lineVertices = [];
|
||||
const lineUVs = [];
|
||||
|
||||
if ( line.indexOf( '/' ) === - 1 ) {
|
||||
|
||||
lineVertices = lineParts;
|
||||
|
||||
} else {
|
||||
|
||||
for ( let li = 0, llen = lineParts.length; li < llen; li ++ ) {
|
||||
|
||||
const parts = lineParts[ li ].split( '/' );
|
||||
|
||||
if ( parts[ 0 ] !== '' ) lineVertices.push( parts[ 0 ] );
|
||||
if ( parts[ 1 ] !== '' ) lineUVs.push( parts[ 1 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
state.addLineGeometry( lineVertices, lineUVs );
|
||||
|
||||
} else if ( lineFirstChar === 'p' ) {
|
||||
|
||||
const lineData = line.slice( 1 ).trim();
|
||||
const pointData = lineData.split( ' ' );
|
||||
|
||||
state.addPointGeometry( pointData );
|
||||
|
||||
} else if ( ( result = _object_pattern.exec( line ) ) !== null ) {
|
||||
|
||||
// o object_name
|
||||
// or
|
||||
// g group_name
|
||||
|
||||
// WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
||||
// let name = result[ 0 ].slice( 1 ).trim();
|
||||
const name = ( ' ' + result[ 0 ].slice( 1 ).trim() ).slice( 1 );
|
||||
|
||||
state.startObject( name );
|
||||
|
||||
} else if ( _material_use_pattern.test( line ) ) {
|
||||
|
||||
// material
|
||||
|
||||
state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );
|
||||
|
||||
} else if ( _material_library_pattern.test( line ) ) {
|
||||
|
||||
// mtl file
|
||||
|
||||
state.materialLibraries.push( line.substring( 7 ).trim() );
|
||||
|
||||
} else if ( _map_use_pattern.test( line ) ) {
|
||||
|
||||
// the line is parsed but ignored since the loader assumes textures are defined MTL files
|
||||
// (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
|
||||
|
||||
console.warn( 'THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.' );
|
||||
|
||||
} else if ( lineFirstChar === 's' ) {
|
||||
|
||||
result = line.split( ' ' );
|
||||
|
||||
// smooth shading
|
||||
|
||||
// @todo Handle files that have varying smooth values for a set of faces inside one geometry,
|
||||
// but does not define a usemtl for each face set.
|
||||
// This should be detected and a dummy material created (later MultiMaterial and geometry groups).
|
||||
// This requires some care to not create extra material on each smooth value for "normal" obj files.
|
||||
// where explicit usemtl defines geometry groups.
|
||||
// Example asset: examples/models/obj/cerberus/Cerberus.obj
|
||||
|
||||
/*
|
||||
* http://paulbourke.net/dataformats/obj/
|
||||
*
|
||||
* From chapter "Grouping" Syntax explanation "s group_number":
|
||||
* "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
|
||||
* Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
|
||||
* surfaces, smoothing groups are either turned on or off; there is no difference between values greater
|
||||
* than 0."
|
||||
*/
|
||||
if ( result.length > 1 ) {
|
||||
|
||||
const value = result[ 1 ].trim().toLowerCase();
|
||||
state.object.smooth = ( value !== '0' && value !== 'off' );
|
||||
|
||||
} else {
|
||||
|
||||
// ZBrush can produce "s" lines #11707
|
||||
state.object.smooth = true;
|
||||
|
||||
}
|
||||
|
||||
const material = state.object.currentMaterial();
|
||||
if ( material ) material.smooth = state.object.smooth;
|
||||
|
||||
} else {
|
||||
|
||||
// Handle null terminated files without exception
|
||||
if ( line === '\0' ) continue;
|
||||
|
||||
console.warn( 'THREE.OBJLoader: Unexpected line: "' + line + '"' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
state.finalize();
|
||||
|
||||
const container = new Group();
|
||||
container.materialLibraries = [].concat( state.materialLibraries );
|
||||
|
||||
const hasPrimitives = ! ( state.objects.length === 1 && state.objects[ 0 ].geometry.vertices.length === 0 );
|
||||
|
||||
if ( hasPrimitives === true ) {
|
||||
|
||||
for ( let i = 0, l = state.objects.length; i < l; i ++ ) {
|
||||
|
||||
const object = state.objects[ i ];
|
||||
const geometry = object.geometry;
|
||||
const materials = object.materials;
|
||||
const isLine = ( geometry.type === 'Line' );
|
||||
const isPoints = ( geometry.type === 'Points' );
|
||||
let hasVertexColors = false;
|
||||
|
||||
// Skip o/g line declarations that did not follow with any faces
|
||||
if ( geometry.vertices.length === 0 ) continue;
|
||||
|
||||
const buffergeometry = new BufferGeometry();
|
||||
|
||||
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( geometry.vertices, 3 ) );
|
||||
|
||||
if ( geometry.normals.length > 0 ) {
|
||||
|
||||
buffergeometry.setAttribute( 'normal', new Float32BufferAttribute( geometry.normals, 3 ) );
|
||||
|
||||
}
|
||||
|
||||
if ( geometry.colors.length > 0 ) {
|
||||
|
||||
hasVertexColors = true;
|
||||
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( geometry.colors, 3 ) );
|
||||
|
||||
}
|
||||
|
||||
if ( geometry.hasUVIndices === true ) {
|
||||
|
||||
buffergeometry.setAttribute( 'uv', new Float32BufferAttribute( geometry.uvs, 2 ) );
|
||||
|
||||
}
|
||||
|
||||
// Create materials
|
||||
|
||||
const createdMaterials = [];
|
||||
|
||||
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
|
||||
|
||||
const sourceMaterial = materials[ mi ];
|
||||
const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors;
|
||||
let material = state.materials[ materialHash ];
|
||||
|
||||
if ( this.materials !== null ) {
|
||||
|
||||
material = this.materials.create( sourceMaterial.name );
|
||||
|
||||
// mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
|
||||
if ( isLine && material && ! ( material instanceof LineBasicMaterial ) ) {
|
||||
|
||||
const materialLine = new LineBasicMaterial();
|
||||
Material.prototype.copy.call( materialLine, material );
|
||||
materialLine.color.copy( material.color );
|
||||
material = materialLine;
|
||||
|
||||
} else if ( isPoints && material && ! ( material instanceof PointsMaterial ) ) {
|
||||
|
||||
const materialPoints = new PointsMaterial( { size: 10, sizeAttenuation: false } );
|
||||
Material.prototype.copy.call( materialPoints, material );
|
||||
materialPoints.color.copy( material.color );
|
||||
materialPoints.map = material.map;
|
||||
material = materialPoints;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( material === undefined ) {
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
material = new LineBasicMaterial();
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
|
||||
|
||||
} else {
|
||||
|
||||
material = new MeshPhongMaterial();
|
||||
|
||||
}
|
||||
|
||||
material.name = sourceMaterial.name;
|
||||
material.flatShading = sourceMaterial.smooth ? false : true;
|
||||
material.vertexColors = hasVertexColors;
|
||||
|
||||
state.materials[ materialHash ] = material;
|
||||
|
||||
}
|
||||
|
||||
createdMaterials.push( material );
|
||||
|
||||
}
|
||||
|
||||
// Create mesh
|
||||
|
||||
let mesh;
|
||||
|
||||
if ( createdMaterials.length > 1 ) {
|
||||
|
||||
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
|
||||
|
||||
const sourceMaterial = materials[ mi ];
|
||||
buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );
|
||||
|
||||
}
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
mesh = new LineSegments( buffergeometry, createdMaterials );
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
mesh = new Points( buffergeometry, createdMaterials );
|
||||
|
||||
} else {
|
||||
|
||||
mesh = new Mesh( buffergeometry, createdMaterials );
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
mesh = new LineSegments( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
mesh = new Points( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
} else {
|
||||
|
||||
mesh = new Mesh( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mesh.name = object.name;
|
||||
|
||||
container.add( mesh );
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// if there is only the default parser state object with no geometry data, interpret data as point cloud
|
||||
|
||||
if ( state.vertices.length > 0 ) {
|
||||
|
||||
const material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
|
||||
|
||||
const buffergeometry = new BufferGeometry();
|
||||
|
||||
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( state.vertices, 3 ) );
|
||||
|
||||
if ( state.colors.length > 0 && state.colors[ 0 ] !== undefined ) {
|
||||
|
||||
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( state.colors, 3 ) );
|
||||
material.vertexColors = true;
|
||||
|
||||
}
|
||||
|
||||
const points = new Points( buffergeometry, material );
|
||||
container.add( points );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return container;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { OBJLoader };
|
||||
+19552
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user