/
RaskolnickOFF
/
3D
Обзор
Документация
Войти
/
RaskolnickOFF
/
3D
Код
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/source/systems/render/TreeTransparencySystem.js
248 строк
8 KB
RaskolnickOFF
Structure update
06 июн 2026, 03:18
06 июн 2026, 03:18
9bc1c25
Код
Авторство
О чём код?
// js/source/systems/render/TreeTransparencySystem.js import * as THREE from 'three'; import { Components } from '../../core/Components.js'; export class TreeTransparencySystem { constructor(ctx) { this.scene = ctx.scene; this.camera = ctx.camera; this.settings = ctx.settings || {}; this._playerEntity = null; this._hiddenTree = null; this._currentOpacity = 1.0; /** @type {Array<{group: THREE.Object3D, opacity: number}>} */ this._fadingBack = []; this._lastTime = performance.now(); const cfg = this.settings.treeTransparency || {}; this._targetOpacity = cfg.opacity ?? 0.3; this._fadeSpeed = cfg.fadeSpeed ?? 4.0; this._semicircleRadius = cfg.semicircleRadius ?? 6.0; this._maxDistToLine = cfg.maxDistToLine ?? 2.5; this._allTrees = []; this._needsRecollect = true; this._recollectTimer = 0; this._recollectInterval = 2.0; } setPlayerEntity(entity) { this._playerEntity = entity; this._needsRecollect = true; } update() { if (!this._playerEntity) return; const now = performance.now(); const dt = Math.min((now - this._lastTime) / 1000, 0.05); this._lastTime = now; const t = this._playerEntity.get(Components.TRANSFORM); if (!t) return; const px = t.x; const pz = t.z; this._recollectTimer += dt; if (this._needsRecollect || this._recollectTimer >= this._recollectInterval) { this._collectTrees(); this._needsRecollect = false; this._recollectTimer = 0; } const camDir = this._getCamDir(px, pz); if (!camDir) return; const candidates = this._filterCandidates(px, pz, camDir.ux, camDir.uz); const bestTree = this._pickBest(candidates); this._switchTarget(bestTree); this._updateFade(dt); } // ============================================================ // СБОР ДЕРЕВЬЕВ // ============================================================ _collectTrees() { this._allTrees.length = 0; this.scene.traverse(child => { if (!child.isMesh || child.name !== 'trunk') return; const pos = new THREE.Vector3(); child.getWorldPosition(pos); this._allTrees.push({ group: child.parent, x: pos.x, z: pos.z }); }); } // ============================================================ // НАПРАВЛЕНИЕ К КАМЕРЕ // ============================================================ _getCamDir(px, pz) { const cx = this.camera.position.x; const cz = this.camera.position.z; const dx = cx - px; const dz = cz - pz; const dist = Math.sqrt(dx * dx + dz * dz); if (dist < 0.1) return null; return { ux: dx / dist, uz: dz / dist }; } // ============================================================ // ФИЛЬТР: ПОЛУКРУГ // ============================================================ _filterCandidates(px, pz, ux, uz) { const maxDistSq = this._maxDistToLine * this._maxDistToLine; const result = []; for (const tree of this._allTrees) { const tdx = tree.x - px; const tdz = tree.z - pz; const dot = tdx * ux + tdz * uz; if (dot <= 0 || dot > this._semicircleRadius) continue; const projX = px + dot * ux; const projZ = pz + dot * uz; const perpSq = (tree.x - projX) * (tree.x - projX) + (tree.z - projZ) * (tree.z - projZ); if (perpSq <= maxDistSq) { result.push({ group: tree.group, dot }); } } return result; } // ============================================================ // ВЫБОР: БЛИЖАЙШЕЕ К КАМЕРЕ // ============================================================ _pickBest(candidates) { if (candidates.length === 0) return null; let best = candidates[0]; for (let i = 1; i < candidates.length; i++) { if (candidates[i].dot > best.dot) { best = candidates[i]; } } return best.group; } // ============================================================ // ПЕРЕКЛЮЧЕНИЕ ЦЕЛИ // ============================================================ _switchTarget(newTree) { if (this._hiddenTree === newTree) return; // Старое дерево → в список на проявление if (this._hiddenTree) { const alreadyThere = this._fadingBack.find(t => t.group === this._hiddenTree); if (!alreadyThere) { this._fadingBack.push({ group: this._hiddenTree, opacity: this._currentOpacity }); } } this._hiddenTree = newTree; if (newTree) { const idx = this._fadingBack.findIndex(t => t.group === newTree); if (idx !== -1) { // Забираем обратно с той прозрачностью, на которой остановились this._currentOpacity = this._fadingBack[idx].opacity; this._fadingBack.splice(idx, 1); } else { this._currentOpacity = 1.0; } } // ИСПРАВЛЕНИЕ #2: не сбрасываем _currentOpacity при newTree === null } // ============================================================ // АНИМАЦИЯ // ============================================================ _updateFade(dt) { // Текущее скрываемое дерево if (this._hiddenTree) { if (this._currentOpacity > this._targetOpacity) { this._currentOpacity -= this._fadeSpeed * dt; if (this._currentOpacity < this._targetOpacity) { this._currentOpacity = this._targetOpacity; } this._setOpacity(this._hiddenTree, this._currentOpacity, true); } } // Старые деревья — возвращаем к 1 for (let i = this._fadingBack.length - 1; i >= 0; i--) { const item = this._fadingBack[i]; item.opacity += this._fadeSpeed * dt; if (item.opacity >= 1.0) { item.opacity = 1.0; this._setOpacity(item.group, 1.0, false); this._fadingBack.splice(i, 1); } else { this._setOpacity(item.group, item.opacity, true); } } } // ============================================================ // УСТАНОВКА ПРОЗРАЧНОСТИ // ============================================================ _setOpacity(treeGroup, opacity, transparent) { treeGroup.traverse(child => { if (!child.isMesh || !child.material) return; // ИСПРАВЛЕНИЕ #1: ленивое клонирование материала if (!child.material.__isCloned) { child.material = child.material.clone(); child.material.__isCloned = true; } const mat = child.material; if (transparent) { mat.transparent = true; mat.opacity = opacity; mat.depthWrite = false; // ИСПРАВЛЕНИЕ #3: needsUpdate только при смене режима if (!mat.__wasTransparent) { mat.needsUpdate = true; mat.__wasTransparent = true; } } else { mat.transparent = false; mat.opacity = 1.0; mat.depthWrite = true; mat.needsUpdate = true; mat.__wasTransparent = false; } }); } }