1// 2// Copyright (c) 2026 Freddy Vandriessche. 3// notice: https://raw.githubusercontent.com/RetroAppleJS/RetroAppleJS.github.io/main/LICENSE.md 4// 5// EMU_DEVICE_video_THREE.js 6// 7// First-phase Apple II 3D monitor renderer for RetroAppleJS. 8// 9// Principle: 10// - keep an existing compatible Apple2Video renderer as the 2D producer; 11// - render that producer into a hidden 560x384 source canvas; 12// - copy/fit that source canvas into a second hidden 560x384 texture canvas; 13// - expose the texture canvas as a THREE.CanvasTexture; 14// - assign the texture to the 3D monitor screen material's map + emissiveMap; 15// - update the texture at a throttled rate, initially 10fps. 16// 17// v5 changes: 18// - keeps the v4 OrbitControls camera binding fix; 19// - keeps the separate THREE_scene include model; 20// - splits the Apple II source canvas from the Three texture canvas; 21// - adds configurable texture padding/fit compensation for screen-mesh UV overscan. 22// 23// 24// Optional configuration before loading this file: 25// 26// window.Apple2VideoTHREE_CONFIG = { ... }; 27// Runtime defaults live only in APPLE2_THREE_CFG_DEFAULT below. 28// The UI and the initialized Three.js scene must read through the cfg helpers, 29// never through separate literal fallbacks. 30 31"use strict"; 32 33// Explicit dependency: THREE uses the GPU renderer as its hidden 2D producer. 34// index.html defines Apple2VideoGPU before loading this file. 35var Apple2VideoTHREE_2D = typeof(Apple2VideoGPU) == "function" ? Apple2VideoGPU : null; 36 37var APPLE2_THREE_CFG_DEFAULT = 38{ 39 textureFPS: 10, 40 renderFPS: 50, 41 perspective: 30, 42 orbitControls: false, 43 lightMove: false, 44 lightMoveHandles: true, 45 lightIntensity: 0.45, 46 47 baseMap: true, 48 roughMetalMap: false, 49 fixedCameraLookAt: true, 50 fixedCameraTargetX: 0, 51 fixedCameraTargetY: 0, 52 fixedCameraTargetZ: 0, 53 54 emissiveColor: 0xFFFFFF, 55 emissiveIntensity: 1, 56 environmentLuminosity: 0, 57 environmentLightIntensity: 0.45, 58 59 // Final orientation from v3/v4. 60 textureFlipY: false, 61 textureMirrorX: false, 62 textureMirrorY: false, 63 64 // v5 overscan/UV compensation. These are canvas pixels, not Apple II pixels. 65 // The defaults correspond to roughly 7 Apple II pixels horizontally and 66 // 8 Apple II pixels vertically on the doubled 560x384 canvas. 67 texturePadLeft: 14, 68 texturePadTop: 64, 69 texturePadRight: 14, 70 texturePadBottom: 64, 71 texturePadColor: "#000000", 72 textureSmoothing: true, 73 74 eventShield: false, 75 eventShieldPreventDefault: false, 76 77 showGrid: false, 78 showAxes: false, 79 background: 0x000000, // frame background 80 cameraAspectMul: 1 81 }; 82 83 function Apple2VideoTHREE_copyConfig() 84 { 85 var cfg = {}, user = window.Apple2VideoTHREE_CONFIG || {}; 86 for (var k in APPLE2_THREE_CFG_DEFAULT) cfg[k] = APPLE2_THREE_CFG_DEFAULT[k]; 87 for (var u in user) cfg[u] = user[u]; 88 return cfg; 89 } 90 91 function Apple2VideoTHREE_clampInt(v, lo, hi) 92 { 93 v = Number(v); 94 if (!isFinite(v)) v = 0; 95 v = Math.round(v); 96 if (v < lo) return lo; 97 if (v > hi) return hi; 98 return v; 99 } 100 101 function Apple2VideoTHREE_getEmbeddedProjectScene() 102 { 103 // Important: a top-level `const THREE_scene = ...` in a non-module 104 // browser script is not necessarily a property of window. Therefore 105 // check the lexical global first, then fall back to window.THREE_scene. 106 try 107 { 108 if (typeof(THREE_scene) !== "undefined") 109 return THREE_scene; 110 } 111 catch(e) {} 112 113 if (window.THREE_scene !== undefined) 114 return window.THREE_scene; 115 116 return null; 117 } 118 119 function Apple2VideoTHREE_getEmbeddedDefaultImageMaps() 120 { 121 try 122 { 123 if (typeof(default_image_maps) !== "undefined") 124 return default_image_maps; 125 } 126 catch(e) {} 127 128 if (window.default_image_maps !== undefined) 129 return window.default_image_maps; 130 131 return null; 132 } 133 134 function Apple2VideoTHREE_isUsableImageDataUrl(url) 135 { 136 return typeof(url) == "string" && 137 url.indexOf("data:image/") == 0 && 138 url.indexOf("...") < 0 && 139 url.length > 32; 140 } 141 142 function Apple2VideoTHREE_getControlInstance() 143 { 144 if (typeof(oApple2Video) === "undefined" || oApple2Video === null || 145 typeof(oApple2Video.getRendererInstance) !== "function") 146 return null; 147 148 return oApple2Video.getRendererInstance("threejs", true); 149 } 150 151 function Apple2VideoTHREE_setPerspectiveControl(input) 152 { 153 var renderer3D = Apple2VideoTHREE_getControlInstance(); 154 if (!renderer3D || typeof(renderer3D.setPerspective) !== "function") return; 155 156 var value = renderer3D.setPerspective(input.value); 157 input.value = value; 158 159 var output = document.getElementById("threePerspectiveValue"); 160 if (output) output.textContent = value + "°"; 161 } 162 163 function Apple2VideoTHREE_setLuminosityControl(input) 164 { 165 var renderer3D = Apple2VideoTHREE_getControlInstance(); 166 if (!renderer3D || typeof(renderer3D.setEmissiveIntensity) !== "function") return; 167 168 var value = renderer3D.setEmissiveIntensity(Number(input.value) / 100); 169 input.value = Math.round(value * 100); 170 171 var output = document.getElementById("threeLuminosityValue"); 172 if (output) output.textContent = value.toFixed(2); 173 } 174 175 function Apple2VideoTHREE_setOrbitControl(input) 176 { 177 var renderer3D = Apple2VideoTHREE_getControlInstance(); 178 if (!renderer3D || typeof(renderer3D.setOrbitEnabled) !== "function") return; 179 180 input.checked = renderer3D.setOrbitEnabled(input.checked); 181 } 182 183 function Apple2VideoTHREE_setTextureSmoothingControl(input) 184 { 185 var renderer3D = Apple2VideoTHREE_getControlInstance(); 186 if (!renderer3D || typeof(renderer3D.setTextureSmoothing) !== "function") return; 187 188 input.checked = renderer3D.setTextureSmoothing(input.checked); 189 } 190 191 function Apple2VideoTHREE_setLightMoveControl(input) 192 { 193 var renderer3D = Apple2VideoTHREE_getControlInstance(); 194 if (!renderer3D || typeof(renderer3D.setLightMoveEnabled) !== "function") return; 195 196 input.checked = renderer3D.setLightMoveEnabled(input.checked); 197 } 198 199 function Apple2VideoTHREE_syncLightIntensityControl(value) 200 { 201 var input = document.getElementById("threeLightIntensity"); 202 var output = document.getElementById("threeLightIntensityValue"); 203 204 value = Number(value); 205 if (!isFinite(value)) value = 0; 206 207 if (input) input.value = value; 208 if (output) output.textContent = value.toFixed(2); 209 } 210 211 function Apple2VideoTHREE_setLightIntensityControl(input) 212 { 213 var renderer3D = Apple2VideoTHREE_getControlInstance(); 214 if (!renderer3D || typeof(renderer3D.setLightIntensity) !== "function") return; 215 216 var value = renderer3D.setLightIntensity(Number(input.value)); 217 Apple2VideoTHREE_syncLightIntensityControl(value); 218 } 219 220 function Apple2VideoTHREE_setEnvironmentControl(input) 221 { 222 var renderer3D = Apple2VideoTHREE_getControlInstance(); 223 if (!renderer3D || typeof(renderer3D.setEnvironmentLuminosity) !== "function") return; 224 225 var value = renderer3D.setEnvironmentLuminosity(Number(input.value) / 100); 226 input.value = Math.round(value * 100); 227 228 var output = document.getElementById("threeEnvironmentValue"); 229 if (output) output.textContent = Math.round(value * 100) + "%"; 230 } 231 232 function Apple2VideoTHREE_resetCameraControl() 233 { 234 var renderer3D = Apple2VideoTHREE_getControlInstance(); 235 if (!renderer3D || typeof(renderer3D.resetCameraPosition) !== "function") return; 236 237 renderer3D.resetCameraPosition(); 238 } 239 240 function Apple2VideoTHREE_resetLightControl() 241 { 242 var renderer3D = Apple2VideoTHREE_getControlInstance(); 243 if (!renderer3D || typeof(renderer3D.resetLightPosition) !== "function") return; 244 245 renderer3D.resetLightPosition(); 246 if (typeof(renderer3D.getLightIntensity) === "function") 247 Apple2VideoTHREE_syncLightIntensityControl(renderer3D.getLightIntensity()); 248 249 } 250 251 function Apple2VideoTHREE_setBaseMapControl(input) 252 { 253 var renderer3D = Apple2VideoTHREE_getControlInstance(); 254 if (!renderer3D || typeof(renderer3D.setBaseMapEnabled) !== "function") return; 255 256 input.checked = renderer3D.setBaseMapEnabled(input.checked); 257 } 258 259 function Apple2VideoTHREE_setRoughMetalControl(input) 260 { 261 var renderer3D = Apple2VideoTHREE_getControlInstance(); 262 if (!renderer3D || typeof(renderer3D.setRoughMetalEnabled) !== "function") return; 263 264 input.checked = renderer3D.setRoughMetalEnabled(input.checked); 265 } 266 267 function Apple2VideoTHREE_controls(renderer3D) 268 { 269 var fov = renderer3D && typeof(renderer3D.getPerspective) === "function" 270 ? renderer3D.getPerspective() 271 : APPLE2_THREE_CFG_DEFAULT.perspective; 272 var luminosity = renderer3D && typeof(renderer3D.getEmissiveIntensity) === "function" 273 ? renderer3D.getEmissiveIntensity() 274 : 1; 275 var orbit = renderer3D && typeof(renderer3D.getOrbitEnabled) === "function" 276 ? renderer3D.getOrbitEnabled() 277 : !!APPLE2_THREE_CFG_DEFAULT.orbitControls; 278 var lightMove = renderer3D && typeof(renderer3D.getLightMoveEnabled) === "function" 279 ? renderer3D.getLightMoveEnabled() 280 : !!APPLE2_THREE_CFG_DEFAULT.lightMove; 281 var lightIntensity = renderer3D && typeof(renderer3D.getLightIntensity) === "function" 282 ? renderer3D.getLightIntensity() 283 : APPLE2_THREE_CFG_DEFAULT.lightIntensity; 284 var baseMap = renderer3D && typeof(renderer3D.getBaseMapEnabled) === "function" 285 ? renderer3D.getBaseMapEnabled() 286 : !!APPLE2_THREE_CFG_DEFAULT.baseMap; 287 var roughMetal = renderer3D && typeof(renderer3D.getRoughMetalEnabled) === "function" 288 ? renderer3D.getRoughMetalEnabled() 289 : !!APPLE2_THREE_CFG_DEFAULT.roughMetalMap; 290 var smoothing = renderer3D && typeof(renderer3D.getTextureSmoothing) === "function" 291 ? renderer3D.getTextureSmoothing() 292 : !!APPLE2_THREE_CFG_DEFAULT.textureSmoothing; 293 294 return "<div class=\"appbut mini\">" 295 +"<input id=\"threePerspective\" type=\"range\" min=\"5\" max=\"90\" step=\"1\" value=\""+Math.round(fov)+"\" class=\"slider\"" 296 +" oninput=\"Apple2VideoTHREE_setPerspectiveControl(this)\" style=\"width:65px;float:left\"></input>" 297 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">perspective " 298 +"<span id=\"threePerspectiveValue\">"+Math.round(fov)+"°</span></div>" 299 +"</div><br>" 300 301 +"<div class=\"appbut mini\">" 302 +"<input id=\"threeLuminosity\" type=\"range\" min=\"0\" max=\"300\" step=\"5\" value=\""+Math.round(luminosity*100)+"\" class=\"slider\"" 303 +" oninput=\"Apple2VideoTHREE_setLuminosityControl(this)\" style=\"width:65px;float:left\"></input>" 304 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">screen luminosity " 305 +"<span id=\"threeLuminosityValue\">"+Number(luminosity).toFixed(2)+"</span></div>" 306 +"</div><br>" 307 308 +"<div class=\"appbut mini\">" 309 +"<input id=\"threeBaseMap\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setBaseMapControl(this)\"" 310 +(baseMap ? " checked" : "") 311 +" style=\"width:65px;float:left\"></input>" 312 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">base material</div>" 313 +"</div><br>" 314 315 +"<div class=\"appbut mini\">" 316 +"<input id=\"threeRoughMetal\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setRoughMetalControl(this)\"" 317 +(roughMetal ? " checked" : "") 318 +" style=\"width:65px;float:left\"></input>" 319 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">rough/metal</div>" 320 +"</div><br>" 321 322 +"<div class=\"appbut mini\">" 323 +"<input id=\"threeOrbit\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setOrbitControl(this)\"" 324 +(orbit ? " checked" : "") 325 +" style=\"width:65px;float:left\"></input>" 326 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">orbit camera" 327 +" | <button type=\"button\" class=\"appbut mini\" onclick=\"Apple2VideoTHREE_resetCameraControl()\">reset</button>" 328 +"</div>" 329 +"</div><br>" 330 331 +"<div class=\"appbut mini\">" 332 +"<input id=\"threeMoveLight\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setLightMoveControl(this)\"" 333 +(lightMove ? " checked" : "") 334 +" style=\"width:65px;float:left\"></input>" 335 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">move light" 336 +" | <button type=\"button\" class=\"appbut mini\" onclick=\"Apple2VideoTHREE_resetLightControl()\">reset</button>" 337 +"</div>" 338 +"</div><br>" 339 340 +"<div class=\"appbut mini\">" 341 +"<input id=\"threeLightIntensity\" type=\"range\" min=\"0\" max=\"5\" step=\"0.05\" value=\""+Number(lightIntensity).toFixed(2)+"\" class=\"slider\"" 342 +" oninput=\"Apple2VideoTHREE_setLightIntensityControl(this)\" style=\"width:65px;float:left\"></input>" 343 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">light intensity " 344 +"<span id=\"threeLightIntensityValue\">"+Number(lightIntensity).toFixed(2)+"</span></div>" 345 +"</div><br>" 346 347 +"<div class=\"appbut mini\">" 348 +"<input id=\"threeTextureSmoothing\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setTextureSmoothingControl(this)\"" 349 +(smoothing ? " checked" : "") 350 +" style=\"width:65px;float:left\"></input>" 351 +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">smooth texture</div>" 352 +"</div><br>" 353 } 354 355 function Apple2VideoTHREE(canvas) 356 { 357 this.id = { 358 "DCODE":"A2THR" 359 ,"coID":"Apple2Video" 360 ,"hostPCODE":"A2BO" 361 ,"deviceIdx":2 362 ,"icon":"fa fa-eye" 363 ,"description":"Apple II Three.js video" 364 ,"deviceEnable":true 365 }; 366 367 368 const DISPLAY_W = 560; 369 const DISPLAY_H = 384; 370 371 var video3D = this; 372 var cfg = Apple2VideoTHREE_copyConfig(); 373 374 var renderLoopStarted = false;
375 var resizeListenerInstalled = false; 376 var lastTextureUpdate = 0; 377 var lastRenderUpdate = 0; 378 var sceneLoadStarted = false; 379 380 this.ctx = canvas || null; // For Apple2Plus this is the visible canvas. 381 this.canvas = canvas || null; 382 this.vidram = null; // Apple2Hw assigns this after construction. 383 this.hw = null; 384 385 this.video2D = null; // Previous renderer, usually EMU_DEVICE_video_GPU.js. 386 this.serial8 = new Uint8Array(); 387 388 this.renderer = null; 389 this.scene = null; 390 this.camera = null; 391 this.controls = null; 392 this.controlsCamera = null; 393 this.screenMesh = null; 394 this.screenTexture = null; 395 396 this.defaultCameraState = null; 397 this.defaultLightState = null; 398 this.sceneBounds = null; 399 this.environmentLight = null; 400 this.defaultMaterialTextures = {}; 401 402 this.lightMoveEnabled = !!cfg.lightMove; 403 this.activeSceneLight = null; 404 this.lightTransformControls = null; 405 this.lightTransformHelper = null; 406 this.lightMovePlane = null; 407 this.lightMoveRaycaster = null; 408 this.lightMoveNdc = null; 409 this.lightMovePoint = null; 410 this._lightMovePointerHandler = function(event) 411 { 412 video3D.onLightPointerMove(event); 413 }; 414 this._lightMovePreviousCursor = ""; 415 416 this.textureDirty = true; 417 this.ready = false; 418 this._eventShieldInstalled = false; 419 420 // v5: source canvas receives the Apple II GPU.js output. 421 this.videoCanvas = document.createElement("canvas"); 422 this.videoCanvas.width = DISPLAY_W; 423 this.videoCanvas.height = DISPLAY_H; 424 this.videoCanvas.style.width = DISPLAY_W + "px"; 425 this.videoCanvas.style.height = DISPLAY_H + "px"; 426 this.videoCanvas.style.display = "none"; 427 428 // v5: texture canvas is what Three.js uploads. We copy the source canvas 429 // into this canvas with optional padding/fit compensation. 430 this.textureCanvas = document.createElement("canvas"); 431 this.textureCanvas.width = DISPLAY_W; 432 this.textureCanvas.height = DISPLAY_H; 433 this.textureCanvas.style.width = DISPLAY_W + "px"; 434 this.textureCanvas.style.height = DISPLAY_H + "px"; 435 this.textureCanvas.style.display = "none"; 436 this.textureCtx = this.textureCanvas.getContext("2d", {alpha:false}); 437 438 // Apple2Plus uses this test: 439 // new Apple2Video().initGPU === undefined ? pass 2D context : pass canvas 440 // So this driver intentionally exposes initGPU, even though Three.js is not GPU.js. 441 this.initGPU = function() 442 { 443 return true; 444 }; 445
446 this.getSceneNumber = function(key,lo,hi) 447 { 448 var value = Number(cfg[key]); 449 if (!isFinite(value)) 450 value = Number(APPLE2_THREE_CFG_DEFAULT[key]); 451 if (!isFinite(value)) 452 value = 0; 453 454 if (lo !== undefined && value < lo) value = lo; 455 if (hi !== undefined && value > hi) value = hi; 456 return value; 457 }; 458 459 this.getSceneBoolean = function(key) 460 { 461 var value = cfg[key]; 462 if (value === undefined) 463 value = APPLE2_THREE_CFG_DEFAULT[key]; 464 return !!value; 465 }; 466 467 this.ensure2DRenderer = function() 468 { 469 if (this.video2D) return true; 470 if (typeof(Apple2VideoTHREE_2D) !== "function" || Apple2VideoTHREE_2D === Apple2VideoTHREE) 471 { 472 console.warn("Apple2VideoTHREE: Apple2VideoGPU is unavailable. Load EMU_DEVICE_video_GPU.js before EMU_DEVICE_video_THREE.js."); 473 return false; 474 } 475 476 this.video2D = new Apple2VideoTHREE_2D(this.videoCanvas); 477 this.video2D.vidram = this.vidram; 478 this.video2D.hw = this.hw; 479 480 return true; 481 }; 482 483 this.sync2DLinks = function() 484 { 485 if (!this.video2D) return; 486 487 this.video2D.vidram = this.vidram; 488 this.video2D.hw = this.hw; 489 490 // Expose the 2D renderer's 491 // serial buffer and delegate kernel calls back to it. 492 if (this.video2D.serial8) 493 this.serial8 = this.video2D.serial8; 494 495 this.kernel = function(mem, rom, serial8) 496 { 497 if (!video3D.video2D || typeof(video3D.video2D.kernel) !== "function") return; 498 499 var r = video3D.video2D.kernel(mem, rom, serial8); 500 video3D.textureDirty = true; 501 return r; 502 }; 503 }; 504 505 this.kernel = function() 506 { 507 console.warn("Apple2VideoTHREE: 2D kernel is not initialised yet."); 508 }; 509 510 this.reset = function() 511 { 512 this.textureDirty = true; 513 if (this.ensure2DRenderer()) 514 { 515 this.video2D.reset(); 516 this.sync2DLinks(); 517 } 518 this.ensureTHREE(); 519 }; 520 521 this.cycle = function(ticks) 522 { 523 if (this.video2D && typeof(this.video2D.cycle) === "function") 524 { 525 this.video2D.cycle(ticks); 526 527 // Since EMU_DEVICE_video_GPU.js now calls its own kernel directly, 528 // Apple2VideoTHREE no longer sees the kernel call. 529 // Keep the Three texture upload loop alive while this renderer is active. 530 this.textureDirty = true; 531 } 532 }; 533 534 this.redraw = function() 535 { 536 if (this.video2D && typeof(this.video2D.redraw) === "function") 537 this.video2D.redraw(); 538 this.textureDirty = true; 539 }; 540 541 this.write = function(addr, d8) 542 { 543 if (this.video2D && typeof(this.video2D.write) === "function") 544 { 545 this.video2D.vidram = this.vidram; 546 this.video2D.hw = this.hw; 547 this.video2D.write(addr, d8); 548 this.textureDirty = true; 549 } 550 }; 551 552 this.setGfx = function(flag) 553 { 554 if (this.video2D && typeof(this.video2D.setGfx) === "function") 555 this.video2D.setGfx(flag); 556 this.sync2DLinks(); 557 }; 558 559 this.setMix = function(flag) 560 { 561 if (this.video2D && typeof(this.video2D.setMix) === "function") 562 this.video2D.setMix(flag); 563 this.sync2DLinks(); 564 }; 565 566 this.setPage2 = function(flag) 567 { 568 if (this.video2D && typeof(this.video2D.setPage2) === "function") 569 this.video2D.setPage2(flag); 570 this.sync2DLinks(); 571 }; 572 573 this.setHires = function(flag) 574 { 575 if (this.video2D && typeof(this.video2D.setHires) === "function") 576 this.video2D.setHires(flag); 577 this.sync2DLinks(); 578 }; 579 580 this.setMonitor = function(mode) 581 { 582 var ret = {"color":"#000000", "name":"THREE"}; 583 584 if (this.video2D && typeof(this.video2D.setMonitor) === "function") 585 ret = this.video2D.setMonitor(mode); 586 587 this.sync2DLinks(); 588 this.textureDirty = true; 589 return ret; 590 }; 591 592 this.getChromeMode = function() 593 { 594 if (this.video2D && typeof(this.video2D.getChromeMode) === "function") 595 return this.video2D.getChromeMode(); 596 return 0; 597 }; 598 599 this.getloresCols = function() 600 { 601 if (this.video2D && typeof(this.video2D.getloresCols) === "function") 602 return this.video2D.getloresCols(); 603 return []; 604 }; 605 606 this.gethiresCols = function() 607 { 608 if (this.video2D && typeof(this.video2D.gethiresCols) === "function") 609 return this.video2D.gethiresCols(); 610 return []; 611 }; 612 613 this.setCol = function(idx, column, val) 614 { 615 if (this.video2D && typeof(this.video2D.setCol) === "function") 616 { 617 var ret = this.video2D.setCol(idx, column, val); 618 this.textureDirty = true; 619 return ret; 620 } 621 return val; 622 }; 623 624 this.addrVisible = function(addr) 625 { 626 if (this.video2D && typeof(this.video2D.addrVisible) === "function") 627 return this.video2D.addrVisible(addr); 628 return true; 629 }; 630 631 this.ensureTHREE = function() 632 { 633 if (this.renderer) 634 return true; 635 636 if (typeof(THREE) === "undefined") 637 { 638 console.warn("Apple2VideoTHREE: THREE is not loaded. Add three.min.js before EMU_DEVICE_video_THREE.js."); 639 return false;
640 } 641 642 if (!this.canvas) 643 { 644 console.warn("Apple2VideoTHREE: no visible canvas was supplied."); 645 return false; 646 } 647 648 this.prepareVisibleCanvas(); 649 650 this.renderer = new THREE.WebGLRenderer({ 651 canvas: this.canvas, 652 antialias: true, 653 alpha: false 654 }); 655 656 this.renderer.setPixelRatio(window.devicePixelRatio || 1); 657 this.resizeRenderer(); 658 this.resizeTextureCanvas(); 659 660 this.screenTexture = new THREE.CanvasTexture(this.textureCanvas); 661 this.applyInitialTextureSmoothing(); 662 663 this.buildFallbackScene(); 664 this.loadProjectScene(); 665 this.installResizeListener(); 666 this.startRenderLoop(); 667 668 return true; 669 }; 670 671 this.prepareVisibleCanvas = function() 672 { 673 if (!this.canvas) return; 674 675 if (!this.canvas.width) this.canvas.width = DISPLAY_W; 676 if (!this.canvas.height) this.canvas.height = DISPLAY_H; 677 678 if (this.canvas.style) 679 { 680 if (!this.canvas.style.width) this.canvas.style.width = DISPLAY_W + "px"; 681 if (!this.canvas.style.height) this.canvas.style.height = DISPLAY_H + "px"; 682 this.canvas.style.display = "block"; 683 this.canvas.style.background = "#000"; 684 this.canvas.style.pointerEvents = "auto"; 685 this.canvas.style.touchAction = "none"; 686 this.canvas.style.userSelect = "none"; 687 } 688 }; 689 690 this.resizeRenderer = function() 691 { 692 if (!this.renderer || !this.canvas) return; 693 694 var w = this.canvas.clientWidth || this.canvas.width || DISPLAY_W; 695 var h = this.canvas.clientHeight || this.canvas.height || DISPLAY_H; 696 697 this.renderer.setSize(w, h, false); 698 699 if (this.camera && this.camera.isPerspectiveCamera) 700 { 701 this.camera.aspect = (w * cfg.cameraAspectMul) / h; 702 this.camera.updateProjectionMatrix(); 703 } 704 }; 705 706 this.configureRendererFromProject = function(project) 707 { 708 if (!this.renderer || !project) return; 709 710 this.renderer.shadowMap.enabled = Boolean(project.shadows); 711 this.renderer.shadowMap.type = (project.shadowType === 1) ? 712 THREE.PCFShadowMap : THREE.PCFSoftShadowMap; 713 714 this.renderer.toneMapping = (project.toneMapping === 1) ? THREE.ReinhardToneMapping : 715 (project.toneMapping === 2) ? THREE.CineonToneMapping : 716 (project.toneMapping === 3) ? THREE.ACESFilmicToneMapping : 717 THREE.LinearToneMapping; 718 719 this.renderer.toneMappingExposure = project.toneMappingExposure || 1; 720 }; 721 722 this.getPerspective = function() 723 { 724 return this.camera && this.camera.isPerspectiveCamera 725 ? this.camera.fov 726 : this.getSceneNumber("perspective",5,90); 727 }; 728 729 this.updateSceneBounds = function() 730 { 731 if (!this.scene) return null; 732 733 this.scene.updateMatrixWorld(true); 734 var box = new THREE.Box3().setFromObject(this.scene); 735 if (box.isEmpty()) 736 { 737 this.sceneBounds = null; 738 return null; 739 } 740 741 var size = box.getSize(new THREE.Vector3()); 742 if (!isFinite(size.length()) || size.lengthSq()===0) 743 { 744 this.sceneBounds = null; 745 return null; 746 } 747 748 this.sceneBounds = box; 749 return box; 750 }; 751
752 this.getSceneCenter = function() 753 { 754 var box = this.sceneBounds || this.updateSceneBounds(); 755 if (box) 756 return box.getCenter(new THREE.Vector3()); 757 758 return new THREE.Vector3( 759 cfg.fixedCameraTargetX || 0, 760 cfg.fixedCameraTargetY || 0, 761 cfg.fixedCameraTargetZ || 0 762 ); 763 }; 764 765 this.getCameraTarget = function() 766 { 767 if (this.controls && this.controls.target) 768 return this.controls.target.clone(); 769 770 return this.getSceneCenter(); 771 }; 772 773 this.captureDefaultCameraState = function(camera,target) 774 { 775 if (!camera) return; 776 target = target ? target.clone() : this.getSceneCenter(); 777 778 this.defaultCameraState = { 779 "position":camera.position.clone() 780 ,"quaternion":camera.quaternion.clone() 781 ,"up":camera.up.clone() 782 ,"zoom":camera.zoom 783 ,"fov":camera.isPerspectiveCamera ? camera.fov : 30 784 ,"target":target 785 }; 786 }; 787 788 this.resetCameraPosition = function() 789 { 790 if (!this.camera) 791 this.ensureTHREE(); 792 793 var state = this.defaultCameraState; 794 if (!this.camera || !state) return false; 795 796 var direction = state.position.clone().sub(state.target); 797 var distance = direction.length(); 798 799 if (this.camera.isPerspectiveCamera && distance > 0) 800 { 801 var currentFov = Math.max(1,this.camera.fov); 802 distance *= Math.tan(state.fov*Math.PI/360) / 803 Math.tan(currentFov*Math.PI/360); 804 } 805 806 if (direction.lengthSq() > 0) 807 direction.setLength(distance); 808 809 this.camera.position.copy(state.target).add(direction); 810 this.camera.quaternion.copy(state.quaternion); 811 this.camera.up.copy(state.up); 812 this.camera.zoom = state.zoom; 813 this.camera.updateProjectionMatrix(); 814 this.camera.updateMatrix(); 815 this.camera.updateMatrixWorld(true); 816 817 if (this.controls) 818 { 819 this.controls.target.copy(state.target); 820 this.controls.update(); 821 } 822 823 return true; 824 }; 825 826 this.setPerspective = function(fov) 827 { 828 fov = Number(fov); 829 if (!isFinite(fov)) fov = this.getPerspective(); 830 fov = Math.max(5,Math.min(90,fov)); 831 cfg.perspective = fov; 832 833 if (!this.camera) 834 this.ensureTHREE(); 835 836 if (this.camera && this.camera.isPerspectiveCamera) 837 { 838 var oldFov = this.camera.fov || 30; 839 var target = this.getCameraTarget(); 840 841 if (Math.abs(fov-oldFov)>1e-6) 842 { 843 var viewDir = new THREE.Vector3().subVectors(this.camera.position,target); 844 var oldDistance = viewDir.length(); 845 846 if (oldDistance>1e-6) 847 { 848 var oldFovRad = THREE.MathUtils.degToRad(oldFov); 849 var newFovRad = THREE.MathUtils.degToRad(fov); 850 var visibleHeightAtTarget = 2*oldDistance*Math.tan(oldFovRad*0.5); 851 var newDistance = visibleHeightAtTarget/(2*Math.tan(newFovRad*0.5)); 852 853 viewDir.normalize(); 854 this.camera.position.copy(target).add(viewDir.multiplyScalar(newDistance)); 855 this.camera.near = Math.max(newDistance/1000,0.001); 856 this.camera.far = Math.max(newDistance*1000,1000); 857 } 858 } 859 860 var w = this.canvas ? (this.canvas.clientWidth || this.canvas.width || DISPLAY_W) : DISPLAY_W; 861 var h = this.canvas ? (this.canvas.clientHeight || this.canvas.height || DISPLAY_H) : DISPLAY_H; 862 863 this.camera.fov = fov; 864 this.camera.aspect = (w*cfg.cameraAspectMul)/Math.max(1,h); 865 this.camera.updateProjectionMatrix(); 866 this.camera.lookAt(target); 867 this.camera.updateMatrixWorld(true); 868 869 if (this.controls) 870 { 871 this.controls.target.copy(target); 872 this.controls.update(); 873 } 874 } 875 876 return fov; 877 }; 878 879 this.getEmissiveIntensity = function() 880 { 881 return Number(cfg.emissiveIntensity) || 0; 882 }; 883 884 this.setEmissiveIntensity = function(intensity) 885 { 886 intensity = Number(intensity); 887 if (!isFinite(intensity)) intensity = this.getEmissiveIntensity(); 888 intensity = Math.max(0, Math.min(5, intensity)); 889 cfg.emissiveIntensity = intensity; 890 891 if (!this.scene) 892 this.ensureTHREE(); 893 894 var screen = this.screenMesh || this.findScreenMesh(); 895 if (screen) 896 { 897 var materials = Array.isArray(screen.material) ? screen.material : [screen.material]; 898 for (var i=0;i<materials.length;i++) 899 { 900 var material = materials[i]; 901 if (!material || material.emissive === undefined) continue; 902 material.emissiveIntensity = intensity; 903 material.needsUpdate = true; 904 } 905 } 906 907 return intensity; 908 }; 909 910 this.getEnvironmentLuminosity = function() 911 { 912 var value = Number(cfg.environmentLuminosity); 913 if (!isFinite(value)) value = 0; 914 return Math.max(0,Math.min(1,value)); 915 }; 916 917 this.applyEnvironmentLuminosity = function() 918 { 919 if (!this.scene) return this.getEnvironmentLuminosity(); 920 921 var value = this.getEnvironmentLuminosity(); 922 this.scene.background = new THREE.Color(value,value,value); 923 924 if (!this.environmentLight || this.environmentLight.parent!==this.scene) 925 { 926 this.environmentLight = this.scene.getObjectByName("__Apple2VideoTHREE_environment"); 927 if (!this.environmentLight) 928 { 929 this.environmentLight = new THREE.AmbientLight(0xffffff,0); 930 this.environmentLight.name = "__Apple2VideoTHREE_environment"; 931 this.scene.add(this.environmentLight); 932 } 933 } 934 935 var gain = Number(cfg.environmentLightIntensity); 936 if (!isFinite(gain)) gain = 0.45; 937 this.environmentLight.intensity = value*Math.max(0,gain); 938 939 return value; 940 }; 941 942 this.setEnvironmentLuminosity = function(value) 943 { 944 value = Number(value); 945 if (!isFinite(value)) value = this.getEnvironmentLuminosity(); 946 cfg.environmentLuminosity = Math.max(0,Math.min(1,value)); 947 948 if (!this.scene) 949 this.ensureTHREE(); 950 951 return this.applyEnvironmentLuminosity(); 952 }; 953 954 this.getBaseMapEnabled = function() 955 { 956 return !!cfg.baseMap; 957 }; 958 959 this.setBaseMapEnabled = function(flag) 960 { 961 cfg.baseMap = !!flag; 962 this.applyScreenTextureMaterialSlots(); 963 return cfg.baseMap; 964 }; 965 966 this.getRoughMetalEnabled = function() 967 { 968 return !!cfg.roughMetalMap; 969 }; 970 971 this.setRoughMetalEnabled = function(flag) 972 { 973 cfg.roughMetalMap = !!flag; 974 this.applyScreenTextureMaterialSlots(); 975 return cfg.roughMetalMap; 976 }; 977 978 this.applyScreenMaterialProfile = function(mat) 979 { 980 if (!mat) return false;
981 982 // Mirrors the scalar part of tools/GUI_DEV/THREEJS_loader.html 983 // after APPLY BASE. Texture slots are managed separately below. 984 if (mat.color) mat.color.set(0xffffff); 985 if (mat.roughness !== undefined) mat.roughness = 1; 986 if (mat.metalness !== undefined) mat.metalness = 0; 987 if (mat.envMapIntensity !== undefined) mat.envMapIntensity = 0.45; 988 989 mat.side = THREE.DoubleSide; 990 mat.needsUpdate = true; 991 return true; 992 }; 993 994 this.applyDefaultTextureProfile = function(texture,slot) 995 { 996 if (!texture) return texture; 997 998 var srgb = (slot == "map" || slot == "emissiveMap"); 999 texture.mapping = THREE.UVMapping; 1000 texture.wrapS = THREE.RepeatWrapping; 1001 texture.wrapT = THREE.RepeatWrapping; 1002 texture.repeat.set(1,1); 1003 texture.offset.set(0,0); 1004 texture.center.set(0,0); 1005 texture.rotation = 0; 1006 texture.format = THREE.RGBAFormat; 1007 texture.type = THREE.UnsignedByteType; 1008 texture.minFilter = THREE.LinearMipmapLinearFilter; 1009 texture.magFilter = THREE.LinearFilter; 1010 texture.anisotropy = 1; 1011 texture.flipY = false; 1012 texture.generateMipmaps = true; 1013 texture.premultiplyAlpha = false; 1014 texture.unpackAlignment = 4; 1015 1016 if (srgb) 1017 { 1018 if (THREE.SRGBColorSpace !== undefined) 1019 texture.colorSpace = THREE.SRGBColorSpace; 1020 else if (THREE.sRGBEncoding !== undefined) 1021 texture.encoding = THREE.sRGBEncoding; 1022 } 1023 else 1024 { 1025 if (THREE.NoColorSpace !== undefined) 1026 texture.colorSpace = THREE.NoColorSpace; 1027 else if (THREE.LinearEncoding !== undefined) 1028 texture.encoding = THREE.LinearEncoding; 1029 } 1030 1031 texture.needsUpdate = true; 1032 return texture; 1033 }; 1034 1035 this.getDefaultMaterialTexture = function(slot) 1036 { 1037 var maps = Apple2VideoTHREE_getEmbeddedDefaultImageMaps(); 1038 if (!maps) return null; 1039 1040 var url = maps[slot]; 1041 if (slot == "roughMetal") 1042 url = maps.roughnessMap || maps.metalnessMap; 1043 1044 if (!Apple2VideoTHREE_isUsableImageDataUrl(url)) 1045 return null; 1046 1047 if (this.defaultMaterialTextures[slot]) 1048 return this.defaultMaterialTextures[slot]; 1049 1050 var texture = new THREE.TextureLoader().load(url,function(tex) 1051 { 1052 video3D.applyDefaultTextureProfile(tex,slot); 1053 video3D.applyScreenTextureMaterialSlots(); 1054 video3D.textureDirty = true; 1055 }); 1056 1057 texture.name = "__Apple2VideoTHREE_default_" + slot; 1058 this.applyDefaultTextureProfile(texture,slot); 1059 this.defaultMaterialTextures[slot] = texture; 1060 1061 return texture; 1062 }; 1063 1064 this.applyScreenTextureMaterialSlots = function() 1065 { 1066 var screen = this.screenMesh || this.findScreenMesh(); 1067 if (!screen) 1068 return false; 1069 1070 this.screenMesh = screen; 1071 1072 var baseTexture = cfg.baseMap 1073 ? (this.getDefaultMaterialTexture("map") || this.screenTexture) 1074 : null; 1075 1076 var roughTexture = cfg.roughMetalMap 1077 ? (this.getDefaultMaterialTexture("roughnessMap") || 1078 this.getDefaultMaterialTexture("roughMetal")) 1079 : null; 1080 1081 var metalTexture = cfg.roughMetalMap 1082 ? (this.getDefaultMaterialTexture("metalnessMap") || roughTexture) 1083 : null; 1084 1085 var materials = Array.isArray(screen.material) ? screen.material : [screen.material]; 1086 for (var i=0;i<materials.length;i++) 1087 { 1088 var mat = materials[i]; 1089 if (!mat) continue; 1090 1091 this.applyScreenMaterialProfile(mat); 1092 1093 mat.map = baseTexture; 1094 1095 if (mat.emissive !== undefined) 1096 { 1097 mat.emissive = new THREE.Color(cfg.emissiveColor); 1098 mat.emissiveMap = this.screenTexture; 1099 mat.emissiveIntensity = cfg.emissiveIntensity; 1100 } 1101 1102 mat.roughnessMap = roughTexture; 1103 mat.metalnessMap = metalTexture; 1104 mat.needsUpdate = true; 1105 } 1106 1107 this.textureDirty = true; 1108 return true; 1109 }; 1110 1111 this.getTextureSmoothing = function() 1112 { 1113 return !!cfg.textureSmoothing; 1114 }; 1115 1116 this.applyInitialTextureSmoothing = function() 1117 { 1118 return this.setTextureSmoothing(this.getSceneBoolean("textureSmoothing")); 1119 }; 1120 1121 this.resizeTextureCanvas = function() 1122 { 1123 var scale = cfg.textureSmoothing ? 2 : 1; 1124 var width = DISPLAY_W*scale; 1125 var height = DISPLAY_H*scale; 1126 1127 if (this.textureCanvas.width !== width) 1128 this.textureCanvas.width = width; 1129 if (this.textureCanvas.height !== height) 1130 this.textureCanvas.height = height; 1131 1132 this.textureCtx = this.textureCanvas.getContext("2d",{alpha:false}); 1133 }; 1134 1135 this.setTextureSmoothing = function(flag) 1136 { 1137 cfg.textureSmoothing = !!flag; 1138 this.resizeTextureCanvas(); 1139 this.configureScreenTexture(); 1140 this.updateTextureCanvas(); 1141 1142 if (this.screenTexture) 1143 this.screenTexture.needsUpdate = true; 1144 1145 this.textureDirty = true; 1146 return cfg.textureSmoothing; 1147 }; 1148 1149 this.renderTHREEScene = function() 1150 { 1151 if (!this.renderer || !this.scene || !this.camera) return; 1152 1153 this.renderer.setRenderTarget(null); 1154 this.renderer.render(this.scene,this.camera); 1155 }; 1156 1157 1158 1159 this.configureScreenTexture = function() 1160 { 1161 if (!this.screenTexture) return; 1162 1163 this.screenTexture.flipY = !!cfg.textureFlipY; 1164 this.screenTexture.wrapS = THREE.ClampToEdgeWrapping; 1165 this.screenTexture.wrapT = THREE.ClampToEdgeWrapping; 1166 this.screenTexture.minFilter = THREE.LinearFilter; 1167 this.screenTexture.magFilter = cfg.textureSmoothing 1168 ? THREE.LinearFilter 1169 : THREE.NearestFilter; 1170 this.screenTexture.generateMipmaps = false;
1171 var maxAnisotropy = this.renderer && this.renderer.capabilities && 1172 typeof(this.renderer.capabilities.getMaxAnisotropy) === "function" 1173 ? this.renderer.capabilities.getMaxAnisotropy() 1174 : 1; 1175 this.screenTexture.anisotropy = cfg.textureSmoothing 1176 ? Math.max(1,maxAnisotropy) 1177 : 1; 1178 this.screenTexture.needsUpdate = true; 1179 1180 if (THREE.SRGBColorSpace !== undefined) 1181 this.screenTexture.colorSpace = THREE.SRGBColorSpace; 1182 else if (THREE.sRGBEncoding !== undefined) 1183 this.screenTexture.encoding = THREE.sRGBEncoding; 1184 1185 this.applyTextureMirror(); 1186 this.screenTexture.needsUpdate = true; 1187 }; 1188 1189 this.applyTextureMirror = function() 1190 { 1191 if (!this.screenTexture) return; 1192 1193 this.screenTexture.repeat.set( 1194 cfg.textureMirrorX ? -1 : 1, 1195 cfg.textureMirrorY ? -1 : 1 1196 ); 1197 1198 this.screenTexture.offset.set( 1199 cfg.textureMirrorX ? 1 : 0, 1200 cfg.textureMirrorY ? 1 : 0 1201 ); 1202 }; 1203 1204 this.setTextureOrientation = function(flipY, mirrorX, mirrorY) 1205 { 1206 cfg.textureFlipY = !!flipY; 1207 cfg.textureMirrorX = !!mirrorX; 1208 cfg.textureMirrorY = !!mirrorY; 1209 this.configureScreenTexture(); 1210 this.textureDirty = true; 1211 return this.getTextureOrientation(); 1212 }; 1213 1214 this.getTextureOrientation = function() 1215 { 1216 return { 1217 textureFlipY: cfg.textureFlipY, 1218 textureMirrorX: cfg.textureMirrorX, 1219 textureMirrorY: cfg.textureMirrorY 1220 }; 1221 }; 1222 1223 this.setTexturePadding = function(left, top, right, bottom) 1224 { 1225 if (typeof(left) === "object" && left !== null) 1226 { 1227 var p = left; 1228 left = p.left; 1229 top = p.top; 1230 right = p.right; 1231 bottom = p.bottom; 1232 } 1233 cfg.texturePadLeft = Apple2VideoTHREE_clampInt(left, 0, DISPLAY_W - 1); 1234 cfg.texturePadTop = Apple2VideoTHREE_clampInt(top, 0, DISPLAY_H - 1); 1235 cfg.texturePadRight = Apple2VideoTHREE_clampInt(right, 0, DISPLAY_W - 1); 1236 cfg.texturePadBottom = Apple2VideoTHREE_clampInt(bottom, 0, DISPLAY_H - 1); 1237 1238 this.textureDirty = true; 1239 return this.getTexturePadding(); 1240 }; 1241 1242 this.getTexturePadding = function() 1243 { 1244 return { 1245 left: cfg.texturePadLeft | 0, 1246 top: cfg.texturePadTop | 0, 1247 right: cfg.texturePadRight | 0, 1248 bottom: cfg.texturePadBottom | 0, 1249 color: cfg.texturePadColor, 1250 smoothing: !!cfg.textureSmoothing 1251 }; 1252 }; 1253 1254 this.updateTextureCanvas = function() 1255 { 1256 if (!this.textureCtx || !this.videoCanvas) return; 1257 1258 var scaleX = this.textureCanvas.width/DISPLAY_W; 1259 var scaleY = this.textureCanvas.height/DISPLAY_H; 1260 1261 var l = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadLeft, 0, DISPLAY_W - 1)*scaleX); 1262 var t = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadTop, 0, DISPLAY_H - 1)*scaleY); 1263 var r = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadRight, 0, DISPLAY_W - 1)*scaleX); 1264 var b = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadBottom, 0, DISPLAY_H - 1)*scaleY); 1265 1266 var dw = this.textureCanvas.width - l - r; 1267 var dh = this.textureCanvas.height - t - b; 1268 1269 if (dw < 1) dw = 1; 1270 if (dh < 1) dh = 1; 1271 1272 var ctx = this.textureCtx; 1273 ctx.save(); 1274 ctx.imageSmoothingEnabled = !!cfg.textureSmoothing; 1275 if ("imageSmoothingQuality" in ctx) 1276 ctx.imageSmoothingQuality = cfg.textureSmoothing ? "high" : "low"; 1277 ctx.fillStyle = cfg.texturePadColor || "#000000"; 1278 ctx.fillRect(0,0,this.textureCanvas.width,this.textureCanvas.height); 1279 ctx.drawImage(this.videoCanvas,0,0,DISPLAY_W,DISPLAY_H,l,t,dw,dh); 1280
1281 ctx.restore(); 1282 }; 1283 1284 this.getConfiguredInitialPerspective = function() 1285 { 1286 return this.getSceneNumber("perspective",5,90); 1287 }; 1288 1289 this.applyInitialPerspective = function() 1290 { 1291 if (!this.camera || !this.camera.isPerspectiveCamera) 1292 return false; 1293 1294 this.setPerspective(this.getConfiguredInitialPerspective()); 1295 this.captureDefaultCameraState(this.camera,this.getCameraTarget()); 1296 return true; 1297 }; 1298 1299 this.getConfiguredInitialLightIntensity = function() 1300 { 1301 return this.getSceneNumber("lightIntensity",0,5); 1302 }; 1303 1304 this.applyInitialLightIntensity = function() 1305 { 1306 var light = this.findMovableLight(); 1307 if (!light || light.intensity === undefined) 1308 return false; 1309 1310 light.intensity = this.getConfiguredInitialLightIntensity(); 1311 light.updateMatrixWorld(true); 1312 1313 if (light.userData) 1314 delete light.userData.__Apple2VideoTHREE_defaultLight; 1315 1316 this.captureDefaultLightState(light); 1317 1318 if (typeof(Apple2VideoTHREE_syncLightIntensityControl) === "function") 1319 Apple2VideoTHREE_syncLightIntensityControl(light.intensity); 1320 1321 return true; 1322 }; 1323 1324 this.loadProjectScene = function() 1325 { 1326 if (sceneLoadStarted) return; 1327 sceneLoadStarted = true; 1328 1329 var json_proj = Apple2VideoTHREE_getEmbeddedProjectScene(); 1330 if (!json_proj) 1331 { 1332 console.warn("Apple2VideoTHREE: THREE_scene is not defined; using fallback scene."); 1333 return; 1334 } 1335 1336 this.installProjectScene(json_proj); 1337 }; 1338 1339 this.installProjectScene = function(json_proj) 1340 { 1341 if (!json_proj || !json_proj.scene) 1342 return; 1343 1344 this.configureRendererFromProject(json_proj.project); 1345 1346 var loader = new THREE.ObjectLoader(); 1347 this.scene = loader.parse(json_proj.scene); 1348 this.updateSceneBounds(); 1349 1350 this.camera = this.cameraFromProject(json_proj.camera); 1351 this.applyInitialPerspective(); 1352 this.attachTextureToScreen(); 1353 this.applyEnvironmentLuminosity(); 1354 this.applyInitialLightIntensity(); 1355 1356 if (cfg.showGrid) 1357 this.scene.add(new THREE.GridHelper(200, 20)); 1358 1359 if (cfg.showAxes) 1360 this.scene.add(new THREE.AxesHelper(60)); 1361 1362 this.installOrbitControls(); 1363 if (this.lightMoveEnabled) 1364 this.setLightMoveEnabled(true); 1365 this.resizeRenderer(); 1366 this.ready = true; 1367 }; 1368 1369 this.cameraFromProject = function(cameraJson) 1370 { 1371 if (!cameraJson || !cameraJson.object) 1372 return this.makeFallbackCamera(); 1373 1374 var c = cameraJson.object; 1375 var cam = new THREE.PerspectiveCamera(c.fov, c.aspect, c.near, c.far); 1376 1377 cam.name = c.name || "Camera"; 1378 cam.zoom = c.zoom || 1; 1379 cam.focus = c.focus || 10; 1380 cam.filmGauge = c.filmGauge || 35; 1381 cam.filmOffset = c.filmOffset || 0; 1382 1383 if (c.matrix) 1384 { 1385 var m4 = new THREE.Matrix4().fromArray(c.matrix); 1386 cam.matrix.copy(m4); 1387 cam.matrix.decompose(cam.position, cam.quaternion, cam.scale); 1388 } 1389 1390 if (c.up) cam.up.fromArray(c.up); 1391 if (c.layers !== undefined) cam.layers.mask = c.layers; 1392 1393 var target = this.getSceneCenter(); 1394 if (cfg.fixedCameraLookAt) 1395 cam.lookAt(target); 1396 1397 cam.updateProjectionMatrix(); 1398 cam.updateMatrix(); 1399 cam.updateMatrixWorld(true); 1400 this.captureDefaultCameraState(cam,target); 1401 1402 return cam; 1403 }; 1404 1405 this.makeFallbackCamera = function() 1406 { 1407 var cam = new THREE.PerspectiveCamera(this.getSceneNumber("perspective",5,90), DISPLAY_W / DISPLAY_H, 0.01, 1000); 1408 cam.position.set(0, 0, 3.2); 1409 cam.lookAt(0, 0, 0); 1410 cam.updateMatrix(); 1411 cam.updateMatrixWorld(true); 1412 return cam; 1413 }; 1414 1415 this.buildFallbackScene = function() 1416 { 1417 this.scene = new THREE.Scene(); 1418 this.scene.background = new THREE.Color(cfg.background); 1419 this.camera = this.makeFallbackCamera(); 1420 1421 var group = new THREE.Group(); 1422 group.name = "Apple II fallback monitor"; 1423 1424 var frameGeo = new THREE.BoxGeometry(3.2, 2.35, 0.18); 1425 var frameMat = new THREE.MeshStandardMaterial({ 1426 color: 0x303030, 1427 roughness: 0.85, 1428 metalness: 0.0 1429 }); 1430 var frame = new THREE.Mesh(frameGeo, frameMat); 1431 frame.position.z = -0.08; 1432 group.add(frame); 1433 1434 var screenGeo = new THREE.PlaneGeometry(2.8, 1.92); 1435 var screenMat = new THREE.MeshStandardMaterial({ 1436 color: 0xffffff, 1437 map: this.screenTexture, 1438 emissive: new THREE.Color(cfg.emissiveColor), 1439 emissiveMap: this.screenTexture, 1440 emissiveIntensity: cfg.emissiveIntensity, 1441 roughness: 0.55, 1442 metalness: 0.0 1443 }); 1444 var screen = new THREE.Mesh(screenGeo, screenMat); 1445 screen.name = "screen"; 1446 screen.position.z = 0.02; 1447 group.add(screen); 1448 1449 this.screenMesh = screen; 1450 this.scene.add(group); 1451 1452 var ambient = new THREE.AmbientLight(0xffffff, 0.55); 1453 this.scene.add(ambient); 1454 1455 var light = new THREE.DirectionalLight(0xffffff, this.get
1455SceneNumber("lightIntensity",0,5)); 1456 light.position.set(2, 4, 3); 1457 this.scene.add(light); 1458 1459 this.updateSceneBounds(); 1460 this.applyInitialPerspective(); 1461 this.captureDefaultCameraState(this.camera,this.getSceneCenter()); 1462 this.applyEnvironmentLuminosity(); 1463 this.applyInitialLightIntensity(); 1464 this.installOrbitControls(); 1465 if (this.lightMoveEnabled) 1466 this.setLightMoveEnabled(true); 1467 this.ready = true; 1468 }; 1469 1470 this.findScreenMesh = function() 1471 { 1472 if (!this.scene) return null; 1473 1474 var found = this.scene.getObjectByName("screen"); 1475 if (found && found.isMesh) return found; 1476 1477 this.scene.traverse(function(obj) 1478 { 1479 if (found || !obj.isMesh) return; 1480 1481 var mat = obj.material; 1482 if (Array.isArray(mat)) mat = mat[0]; 1483 1484 if (obj.name && obj.name.toLowerCase().indexOf("screen") >= 0) 1485 found = obj; 1486 else if (mat && mat.name && mat.name.toLowerCase().indexOf("screen") >= 0) 1487 found = obj; 1488 }); 1489 1490 return found; 1491 }; 1492 1493 this.attachTextureToScreen = function() 1494 { 1495 var screen = this.findScreenMesh(); 1496 if (!screen) 1497 { 1498 console.warn("Apple2VideoTHREE: no mesh named 'screen' found in the scene."); 1499 return false; 1500 } 1501 1502 this.screenMesh = screen; 1503 return this.applyScreenTextureMaterialSlots(); 1504 }; 1505 1506 this.findMovableLight = function() 1507 { 1508 if (!this.scene) return null; 1509 1510 var pointLight = null; 1511 var positionedLight = null; 1512 1513 this.scene.traverse(function(obj) 1514 { 1515 if (!pointLight && obj.isPointLight) 1516 pointLight = obj; 1517 1518 if (!positionedLight && obj.isLight && 1519 !obj.isAmbientLight && !obj.isHemisphereLight && obj.position) 1520 positionedLight = obj; 1521 }); 1522 1523 return pointLight || positionedLight; 1524 }; 1525 1526 this.captureDefaultLightState = function(light) 1527 { 1528 if (!light || !light.position) return null; 1529 1530 if (!light.userData) 1531 light.userData = {}; 1532 1533 if (!light.userData.__Apple2VideoTHREE_defaultLight) 1534 { 1535 light.userData.__Apple2VideoTHREE_defaultLight = { 1536 "position": light.position.clone() 1537 ,"intensity": light.intensity 1538 ,"distance": light.distance 1539 ,"decay": light.decay 1540 }; 1541 } 1542 1543 this.defaultLightState = light.userData.__Apple2VideoTHREE_defaultLight; 1544 return this.defaultLightState; 1545 }; 1546 1547 this.resetLightPosition = function() 1548 { 1549 if (!this.scene) 1550 this.ensureTHREE(); 1551 1552 var light = this.activeSceneLight || this.findMovableLight(); 1553 if (!light) return false; 1554 1555 var state = this.captureDefaultLightState(light); 1556 if (!state) return false; 1557 1558 light.position.copy(state.position); 1559 1560 if (state.intensity !== undefined) light.intensity = state.intensity; 1561 if (state.distance !== undefined) light.distance = state.distance; 1562 if (state.decay !== undefined) light.decay = state.decay; 1563 1564 light.updateMatrixWorld(true); 1565 1566 if (this.lightTransformControls && 1567 typeof(this.lightTransformControls.updateMatrixWorld) == "function") 1568 this.lightTransformControls.updateMatrixWorld(true); 1569 1570 if (this.lightMoveEnabled && this.lightMovePlane && this.camera) 1571 { 1572 var normal = new THREE.Vector3(); 1573 this.camera.getWorldDirection(normal); 1574 this.lightMovePlane.setFromNormalAndCoplanarPoint(normal,light.position); 1575 } 1576 1577 return true; 1578 }; 1579 1580 this.installLightTransformControls = function(light) 1581 { 1582 if (!cfg.lightMoveHandles || !light || !this.scene || !this.camera || 1583 !this.renderer || typeof(THREE.TransformControls) != "function") 1584 return false;
1585 1586 if (!this.lightTransformControls) 1587 { 1588 var controls3D = new THREE.TransformControls(this.camera,this.renderer.domElement); 1589 controls3D.setMode("translate"); 1590 if (typeof(controls3D.setSize) == "function") 1591 controls3D.setSize(0.8); 1592 controls3D.name = "__Apple2VideoTHREE_light_transform"; 1593 1594 controls3D.addEventListener("dragging-changed",function(event) 1595 { 1596 if (video3D.controls) 1597 video3D.controls.enabled = !video3D.lightMoveEnabled && 1598 !event.value && 1599 !!cfg.orbitControls; 1600 }); 1601 1602 controls3D.addEventListener("objectChange",function() 1603 { 1604 if (video3D.activeSceneLight) 1605 { 1606 video3D.activeSceneLight.updateMatrixWorld(true); 1607 1608 if (video3D.lightMovePlane && video3D.camera) 1609 { 1610 var normal = new THREE.Vector3(); 1611 video3D.camera.getWorldDirection(normal); 1612 video3D.lightMovePlane.setFromNormalAndCoplanarPoint( 1613 normal, 1614 video3D.activeSceneLight.position 1615 ); 1616 } 1617 } 1618 }); 1619 1620 controls3D.addEventListener("change",function() 1621 { 1622 video3D.renderTHREEScene(); 1623 }); 1624 1625 this.lightTransformControls = controls3D; 1626 } 1627 else 1628 { 1629 this.lightTransformControls.camera = this.camera; 1630 } 1631 1632 this.lightTransformHelper = 1633 typeof(this.lightTransformControls.getHelper) == "function" 1634 ? this.lightTransformControls.getHelper() 1635 : this.lightTransformControls; 1636 1637 if (this.lightTransformHelper && this.lightTransformHelper.parent !== this.scene) 1638 this.scene.add(this.lightTransformHelper); 1639 1640 this.lightTransformControls.attach(light); 1641 return true; 1642 }; 1643 1644 this.detachLightTransformControls = function() 1645 { 1646 if (this.lightTransformControls && 1647 typeof(this.lightTransformControls.detach) == "function") 1648 this.lightTransformControls.detach(); 1649 1650 if (this.lightTransformHelper && this.lightTransformHelper.parent) 1651 this.lightTransformHelper.parent.remove(this.lightTransformHelper); 1652 }; 1653 1654 this.getLightIntensity = function() 1655 { 1656 var light = this.activeSceneLight || this.findMovableLight(); 1657 if (!light || light.intensity === undefined) 1658 return this.getSceneNumber("lightIntensity",0,5); 1659 1660 var value = Number(light.intensity); 1661 return isFinite(value) ? value : this.getSceneNumber("lightIntensity",0,5); 1662 }; 1663 1664 this.setLightIntensity = function(intensity) 1665 { 1666 intensity = Number(intensity); 1667 if (!isFinite(intensity)) intensity = this.getLightIntensity(); 1668 intensity = Math.max(0,Math.min(5,intensity)); 1669 cfg.lightIntensity = intensity; 1670 1671 if (!this.scene) 1672 this.ensureTHREE(); 1673 1674 var light = this.activeSceneLight || this.findMovableLight(); 1675 if (!light || light.intensity === undefined) 1676 return intensity; 1677 1678 this.captureDefaultLightState(light); 1679 light.intensity = intensity; 1680 light.updateMatrixWorld(true); 1681 1682 return intensity; 1683 }; 1684 1685 this.getLightMoveEnabled = function() 1686 { 1687 return !!this.lightMoveEnabled; 1688 }; 1689 1690 this.setLightMoveEnabled = function(flag) 1691 { 1692 flag = !!flag; 1693 cfg.lightMove = flag; 1694 1695 if (flag && !this.renderer) 1696 this.ensureTHREE(); 1697 1698 var el = this.renderer && this.renderer.domElement; 1699 if (!flag) 1700 { 1701 if (el) 1702 { 1703 el.removeEventListener("pointermove",this._lightMovePointerHandler,true); 1704 el.style.cursor = this._lightMovePreviousCursor || ""; 1705 } 1706 1707 this.detachLightTransformControls(); 1708 1709 cfg.lightMove = false;
1710 this.lightMoveEnabled = false; 1711 this.activeSceneLight = null; 1712 1713 if (this.controls) 1714 this.controls.enabled = !!cfg.orbitControls; 1715 1716 return false; 1717 } 1718 1719 var light = this.findMovableLight(); 1720 if (!el || !this.camera || !light) 1721 { 1722 cfg.lightMove = false; 1723 this.lightMoveEnabled = false; 1724 return false; 1725 } 1726 1727 if (!this.lightMovePlane) 1728 { 1729 this.lightMovePlane = new THREE.Plane(); 1730 this.lightMoveRaycaster = new THREE.Raycaster(); 1731 this.lightMoveNdc = new THREE.Vector2(); 1732 this.lightMovePoint = new THREE.Vector3(); 1733 } 1734 1735 this.captureDefaultLightState(light); 1736 this.activeSceneLight = light; 1737 this.lightMoveEnabled = true; 1738 1739 var normal = new THREE.Vector3(); 1740 this.camera.getWorldDirection(normal); 1741 this.lightMovePlane.setFromNormalAndCoplanarPoint(normal,light.position); 1742 1743 if (this.controls) 1744 this.controls.enabled = false; 1745 1746 var hasTransformHandles = this.installLightTransformControls(light); 1747 el.removeEventListener("pointermove",this._lightMovePointerHandler,true); 1748 1749 if (hasTransformHandles) 1750 { 1751 el.style.cursor = this._lightMovePreviousCursor || ""; 1752 } 1753 else 1754 { 1755 this._lightMovePreviousCursor = el.style.cursor || ""; 1756 el.addEventListener("pointermove",this._lightMovePointerHandler,true); 1757 el.style.cursor = "crosshair"; 1758 } 1759 1760 1761 return true; 1762 }; 1763 1764 this.onLightPointerMove = function(event) 1765 { 1766 if (!this.lightMoveEnabled || !this.activeSceneLight || 1767 !this.lightMoveRaycaster || !this.lightMovePlane || 1768 !this.renderer || !this.camera) 1769 return; 1770 1771 var rect = this.renderer.domElement.getBoundingClientRect(); 1772 this.lightMoveNdc.x = 1773 ((event.clientX-rect.left)/Math.max(1,rect.width))*2-1; 1774 this.lightMoveNdc.y = 1775 -(((event.clientY-rect.top)/Math.max(1,rect.height))*2-1); 1776 1777 this.lightMoveRaycaster.setFromCamera(this.lightMoveNdc,this.camera); 1778 var hit = this.lightMoveRaycaster.ray.intersectPlane( 1779 this.lightMovePlane, 1780 this.lightMovePoint 1781 ); 1782 1783 if (!hit) return; 1784 1785 this.activeSceneLight.position.copy(this.lightMovePoint); 1786 this.activeSceneLight.updateMatrixWorld(true); 1787 1788 if (event.cancelable) 1789 event.preventDefault(); 1790 event.stopPropagation(); 1791 }; 1792 1793 this.getOrbitEnabled = function() 1794 { 1795 return !!cfg.orbitControls; 1796 }; 1797 1798 this.setOrbitEnabled = function(flag) 1799 { 1800 cfg.orbitControls = !!flag; 1801 1802 if (!cfg.orbitControls) 1803 { 1804 if (this.controls && typeof(this.controls.dispose) === "function") 1805 this.controls.dispose(); 1806 1807 this.controls = null; 1808 this.controlsCamera = null; 1809 return false; 1810 } 1811 1812 if (!this.renderer) 1813 this.ensureTHREE(); 1814 1815 this.installOrbitControls(); 1816 return !!this.controls; 1817 }; 1818 1819 this.installOrbitControls = function() 1820 { 1821 if (!cfg.orbitControls || !this.camera || !this.renderer) 1822 return; 1823 1824 if (typeof(THREE.OrbitControls) !== "function") 1825 return; 1826 1827 // v4 fix retained: 1828 // ensureTHREE() first builds a fallback scene/camera, then replaces it 1829 // with the editor/project scene/camera. Recreate controls if the 1830 // active rendered camera changed. 1831 if (this.controls) 1832 { 1833 var sameCamera = (this.controlsCamera === this.camera) || 1834 (this.controls.object === this.camera); 1835 if (sameCamera) 1836 return; 1837 1838 if (typeof(this.controls.dispose) === "function") 1839 this.controls.dispose(); 1840 1841 this.controls = null; 1842 this.controlsCamera = null; 1843 } 1844 1845 this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); 1846 this.controlsCamera = this.camera; 1847 this.controls.enableDamping = true; 1848 this.controls.dampingFactor = 0.08; 1849 1850 this.controls.target.copy(this.getSceneCenter()); 1851 this.controls.enabled = !this.lightMoveEnabled; 1852 this.controls.update(); 1853 if (typeof(this.controls.saveState)==="function") 1854 this.controls.saveState(); 1855 1856 this.installCanvasEventShield(); 1857 }; 1858 1859 this.getOrbitControlsInfo = function() 1860 { 1861 return { 1862 hasControls: !!this.controls, 1863 controlsCameraIsRenderedCamera: !!this.controls && 1864 ((this.controlsCamera === this.camera) || (this.controls.object === this.camera)), 1865 eventShield: !!cfg.eventShield, 1866 eventShieldInstalled: !!this._eventShieldInstalled 1867 }; 1868 }; 1869 1870 this.installCanvasEventShield = function() 1871 { 1872 if (!cfg.eventShield || this._eventShieldInstalled || !this.renderer || !this.renderer.domElement) 1873 return; 1874 1875 this._eventShieldInstalled = true; 1876 1877 var el = this.renderer.domElement; 1878 var opt = {capture:true, passive:false}; 1879 var events = [ 1880 "pointerdown", "pointermove", "pointerup", "pointercancel", 1881 "mousedown", "mousemove", "mouseup", "click", "dblclick", 1882 "wheel", "touchstart", "touchmove", "touchend", "contextmenu" 1883 ]; 1884 1885 var stop = function(ev) 1886 {
1887 // Do not use stopImmediatePropagation(), because OrbitControls also 1888 // listens on this same element and must still receive the event. 1889 ev.stopPropagation(); 1890 1891 if (ev.type === "contextmenu") 1892 ev.preventDefault(); 1893 else if (cfg.eventShieldPreventDefault && ev.cancelable) 1894 ev.preventDefault(); 1895 }; 1896 1897 for (var i = 0; i < events.length; i++) 1898 { 1899 try { el.addEventListener(events[i], stop, opt); } 1900 catch(e) { el.addEventListener(events[i], stop, true); } 1901 } 1902 }; 1903 1904 this.installResizeListener = function() 1905 { 1906 if (resizeListenerInstalled) return; 1907 resizeListenerInstalled = true; 1908 1909 window.addEventListener("resize", function() 1910 { 1911 video3D.resizeRenderer(); 1912 }); 1913 }; 1914 1915 this.startRenderLoop = function() 1916 { 1917 if (renderLoopStarted) return; 1918 renderLoopStarted = true; 1919 1920 function animate(now) 1921 { 1922 window.requestAnimationFrame(animate); 1923 1924 var renderInterval = 1000 / Math.max(1, cfg.renderFPS | 0); 1925 if (now - lastRenderUpdate < renderInterval) 1926 return; 1927 lastRenderUpdate = now; 1928 1929 video3D.pumpTexture(now); 1930 1931 if (video3D.controls) 1932 video3D.controls.update(); 1933 1934 video3D.renderTHREEScene(); 1935 } 1936 1937 window.requestAnimationFrame(animate); 1938 }; 1939 1940 this.pumpTexture = function(now) 1941 { 1942 if (!this.screenTexture || !this.textureDirty) 1943 return; 1944 1945 var textureInterval = 1000 / Math.max(1, cfg.textureFPS | 0); 1946 if (now - lastTextureUpdate < textureInterval) 1947 return; 1948 1949 this.updateTextureCanvas(); 1950 this.screenTexture.needsUpdate = true; 1951 this.textureDirty = false; 1952 lastTextureUpdate = now; 1953 }; 1954 1955 this.getTextureCanvas = function() 1956 { 1957 return this.textureCanvas; 1958 }; 1959
1960 this.getSourceCanvas = function() 1961 { 1962 return this.videoCanvas; 1963 }; 1964 1965 this.get2DRenderer = function() 1966 { 1967 return this.video2D; 1968 }; 1969 1970 this.getScene = function() 1971 { 1972 return this.scene; 1973 }; 1974 1975 this.getRenderer = function() 1976 { 1977 return this.renderer; 1978 }; 1979 1980 this.ctrl_dlg = function() 1981 { 1982 return Apple2VideoTHREE_controls(this); 1983 }; 1984 1985 } 1986 1987Apple2VideoTHREE.ctrl_dlg = function() 1988{ 1989 return Apple2VideoTHREE_controls(null); 1990}; 1991 1992if(typeof(oEMU)!="undefined" && oEMU.component && oEMU.component.Video) 1993 oEMU.component.Video.Apple2Video = new Apple2VideoTHREE();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.