mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
211 lines
8.6 KiB
JavaScript
211 lines
8.6 KiB
JavaScript
// ThreeRenderer — lightweight 3D projection for the shared Scene. It uses generated GLB avatars
|
|
// when available and falls back to low-poly pieces so the mode remains usable during asset work.
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
function colorNum(hex) { return parseInt(String(hex || '#9aa4b2').replace('#', ''), 16) || 0x9aa4b2; }
|
|
|
|
function ThreeRenderer(host, opts) {
|
|
opts = opts || {};
|
|
if (!root.RK || !RK.THREE || !RK.THREE.THREE) throw new Error('THREE not loaded');
|
|
this.name = '3d';
|
|
this.host = host;
|
|
this.THREE = RK.THREE.THREE;
|
|
this.GLTFLoader = RK.THREE.GLTFLoader;
|
|
this.models = {};
|
|
this.mixers = [];
|
|
this.entities = {};
|
|
this.last = {};
|
|
this._sizeKey = null;
|
|
this._init();
|
|
}
|
|
|
|
ThreeRenderer.prototype._init = function () {
|
|
var T = this.THREE;
|
|
this.scene = new T.Scene();
|
|
this.scene.background = new T.Color(0x0a0c0f);
|
|
this.camera = new T.OrthographicCamera(-10, 10, 10, -10, 0.1, 100);
|
|
this.camera.position.set(8, 9, 8);
|
|
this.camera.lookAt(0, 0, 0);
|
|
this.renderer = new T.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: true });
|
|
this.renderer.domElement.className = 'rk-canvas';
|
|
this.renderer.setPixelRatio(Math.min(2, root.devicePixelRatio || 1));
|
|
this.host.appendChild(this.renderer.domElement);
|
|
|
|
var hemi = new T.HemisphereLight(0xfff3d2, 0x283044, 2.1);
|
|
this.scene.add(hemi);
|
|
var key = new T.DirectionalLight(0xffffff, 1.6);
|
|
key.position.set(6, 10, 5);
|
|
this.scene.add(key);
|
|
this.world = new T.Group();
|
|
this.scene.add(this.world);
|
|
this.clock = new T.Clock();
|
|
var self = this;
|
|
this._raf = requestAnimationFrame(function tick() { self._animate(); self._raf = requestAnimationFrame(tick); });
|
|
};
|
|
|
|
ThreeRenderer.prototype._resize = function (scene) {
|
|
var w = Math.max(420, scene.cols * 32);
|
|
var h = Math.max(360, scene.rows * 28 + 120);
|
|
this.renderer.setSize(w, h, false);
|
|
var aspect = w / h, span = Math.max(scene.cols, scene.rows) * 0.78 + 2.5;
|
|
this.camera.left = -span * aspect;
|
|
this.camera.right = span * aspect;
|
|
this.camera.top = span;
|
|
this.camera.bottom = -span;
|
|
this.camera.updateProjectionMatrix();
|
|
};
|
|
|
|
ThreeRenderer.prototype._mat = function (hex) {
|
|
var T = this.THREE;
|
|
return new T.MeshStandardMaterial({ color: colorNum(hex), roughness: 0.82, metalness: 0.02 });
|
|
};
|
|
|
|
ThreeRenderer.prototype._buildTiles = function (scene) {
|
|
var T = this.THREE;
|
|
this.world.clear();
|
|
this.tileLayer = new T.Group();
|
|
this.entityLayer = new T.Group();
|
|
this.world.add(this.tileLayer, this.entityLayer);
|
|
var cx = (scene.cols - 1) / 2, cz = (scene.rows - 1) / 2;
|
|
for (var y = 0; y < scene.rows; y++) {
|
|
for (var x = 0; x < scene.cols; x++) {
|
|
var kind = scene.grid[y][x];
|
|
var def = scene.legend[kind] || {};
|
|
var h = kind === 'wall' ? 0.9 : (kind === 'bar' ? 0.42 : (kind === 'table' ? 0.28 : 0.08));
|
|
var geom = new T.BoxGeometry(kind === 'wall' ? 1 : 0.96, h, kind === 'wall' ? 1 : 0.96);
|
|
var mesh = new T.Mesh(geom, this._mat(kind === 'floor' ? '#4f4a3e' : (def.color || '#333333')));
|
|
mesh.position.set(x - cx, h / 2 - 0.05, y - cz);
|
|
this.tileLayer.add(mesh);
|
|
}
|
|
}
|
|
this.world.position.set(0, 0, 0);
|
|
};
|
|
|
|
ThreeRenderer.prototype._fallbackAvatar = function (e) {
|
|
var T = this.THREE;
|
|
var g = new T.Group();
|
|
var body = new T.Mesh(new T.CapsuleGeometry(0.24, 0.56, 4, 8), this._mat(e.color || '#9aa4b2'));
|
|
body.position.y = 0.52;
|
|
g.add(body);
|
|
var face = new T.Mesh(new T.BoxGeometry(0.11, 0.07, 0.035), this._mat('#0a0c0f'));
|
|
face.position.set(0, 0.7, 0.23);
|
|
g.add(face);
|
|
return g;
|
|
};
|
|
|
|
ThreeRenderer.prototype._loadModel = function (resolved, cb) {
|
|
if (!resolved || !resolved.model || !resolved.model.url || !this.GLTFLoader) { cb(null); return; }
|
|
var url = resolved.model.url;
|
|
if (this.models[url]) {
|
|
if (this.models[url].ready) cb(this.models[url]);
|
|
else this.models[url].cbs.push(cb);
|
|
return;
|
|
}
|
|
var rec = this.models[url] = { ready: false, cbs: [cb] };
|
|
var loader = new this.GLTFLoader();
|
|
loader.load(url, function (gltf) {
|
|
rec.ready = true;
|
|
rec.gltf = gltf;
|
|
var cbs = rec.cbs; rec.cbs = [];
|
|
cbs.forEach(function (fn) { fn(rec); });
|
|
}, null, function () {
|
|
rec.ready = true;
|
|
rec.error = true;
|
|
var cbs = rec.cbs; rec.cbs = [];
|
|
cbs.forEach(function (fn) { fn(null); });
|
|
});
|
|
};
|
|
|
|
ThreeRenderer.prototype._makeEntity = function (e) {
|
|
var T = this.THREE, self = this;
|
|
var shell = new T.Group();
|
|
shell._body = this._fallbackAvatar(e);
|
|
shell.add(shell._body);
|
|
var resolved = (root.RK && RK.resolveAppearance) ? RK.resolveAppearance(e, '3d') : null;
|
|
if (root.RK && RK.canUsePack && resolved && RK.canUsePack(resolved.pack)) {
|
|
this._loadModel(resolved, function (rec) {
|
|
if (!rec || !rec.gltf || !shell.parent) return;
|
|
shell.remove(shell._body);
|
|
var model = rec.gltf.scene.clone(true);
|
|
model.scale.setScalar(0.55);
|
|
model.rotation.y = Math.PI;
|
|
shell.add(model);
|
|
shell._body = model;
|
|
if (rec.gltf.animations && rec.gltf.animations.length) {
|
|
var mixer = new T.AnimationMixer(model);
|
|
shell._mixer = mixer;
|
|
shell._actions = {};
|
|
rec.gltf.animations.forEach(function (clip) {
|
|
shell._actions[String(clip.name).toLowerCase()] = mixer.clipAction(clip);
|
|
});
|
|
var idle = shell._actions.idle || shell._actions[Object.keys(shell._actions)[0]];
|
|
if (idle) idle.play();
|
|
self.mixers.push(mixer);
|
|
}
|
|
});
|
|
}
|
|
return shell;
|
|
};
|
|
|
|
ThreeRenderer.prototype.render = function (scene) {
|
|
if (!scene || !this.renderer) return;
|
|
this._resize(scene);
|
|
var key = scene.cols + 'x' + scene.rows + ':' + scene.grid.map(function (r) { return r.join(''); }).join('/');
|
|
if (key !== this._sizeKey) {
|
|
this._buildTiles(scene);
|
|
this._sizeKey = key;
|
|
}
|
|
var seen = {}, cx = (scene.cols - 1) / 2, cz = (scene.rows - 1) / 2;
|
|
for (var i = 0; i < scene.entities.length; i++) {
|
|
var e = scene.entities[i], id = e.id;
|
|
seen[id] = true;
|
|
var ent = this.entities[id];
|
|
if (!ent) {
|
|
ent = this.entities[id] = { obj: this._makeEntity(e), x: e.x, y: e.y };
|
|
this.entityLayer.add(ent.obj);
|
|
}
|
|
ent.e = e;
|
|
ent.tx = e.x - cx;
|
|
ent.tz = e.y - cz;
|
|
}
|
|
for (id in this.entities) {
|
|
if (!seen[id]) {
|
|
this.entityLayer.remove(this.entities[id].obj);
|
|
delete this.entities[id];
|
|
}
|
|
}
|
|
};
|
|
|
|
ThreeRenderer.prototype._animate = function () {
|
|
if (!this.renderer) return;
|
|
var dt = this.clock.getDelta();
|
|
for (var i = 0; i < this.mixers.length; i++) this.mixers[i].update(dt);
|
|
for (var id in this.entities) {
|
|
var ent = this.entities[id], o = ent.obj;
|
|
o.position.x += ((ent.tx || 0) - o.position.x) * 0.18;
|
|
o.position.z += ((ent.tz || 0) - o.position.z) * 0.18;
|
|
if (ent.e && ent.e.facing) {
|
|
var r = ent.e.facing === 'up' ? Math.PI : ent.e.facing === 'left' ? -Math.PI / 2 : ent.e.facing === 'right' ? Math.PI / 2 : 0;
|
|
o.rotation.y += (r - o.rotation.y) * 0.18;
|
|
}
|
|
}
|
|
this.renderer.render(this.scene, this.camera);
|
|
};
|
|
|
|
ThreeRenderer.prototype.destroy = function () {
|
|
if (this._raf) cancelAnimationFrame(this._raf);
|
|
if (this.renderer) {
|
|
if (this.renderer.domElement && this.renderer.domElement.parentNode) this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
|
|
this.renderer.dispose();
|
|
}
|
|
this.renderer = null;
|
|
this.entities = {};
|
|
this.models = {};
|
|
this.mixers = [];
|
|
};
|
|
|
|
root.RK = root.RK || {};
|
|
root.RK.ThreeRenderer = ThreeRenderer;
|
|
})(window);
|