<!DOCTYPE html>
<!--
Author: Twily 2025-2026
Website: twily.info
Description: threejs playground animation rigging skinned mesh
// keybinds
// WASD Movement keys (+arrows)
// QE Extra movement
// C Crouch/Descend
// Shift Sprint
// Space Jump/Ascend
// J Toggle Joysticks (Game Mode)
// G Toggle Game Mode (Mouse)
// M Toggle Music
// O Ortho Camera Modes
// I Toggle Wireframe
// U Toggle Underwear
music: Look How High I Took You - Dark Hypnotic Techno [&&]
Dark & Progressive House Set at the Grand Canyon - DΛRK SØUND
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<!--<meta name="viewport" content="width=device-width, initial-scale=1.0">-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Three.js FPS Demo Engine</title>
<style>
html,body {
font-size: 10pt;
font-family: "Droid Sans", "Liberation Sans", "DejaVu Sans", "Segoe UI", Sans;
width: 100%; height: 100%;
margin: 0; padding: 0;
overflow: hidden;
user-select: none;
background: #000000; color: #b5857f;
overscroll-behavior: none; /* Stops the pull-down glow/refresh */
touch-action: none; /* Prevents browser handling of gestures */
-webkit-overflow-scrolling: auto;
}
canvas {
display: block;
position: relative; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
/** { box-sizing: border-box; }*/
*:focus { outline: none !important; }
/** { outline: 1px solid #f0f; }*/
/* Firefox fix (The "Shift" & "Skyward View")
"manual F11" context is the smoking gun. Firefox handles the UI transition for F11 differently than a programmatic Fullscreen API request. ..the browser keeps a "hit zone" at the top that causes a coordinate shift during the first frame of movement after a lock. */
body.is-locked {
position: fixed;
overflow: hidden;
width: 100vw;
height: 100vh;
}
:fullscreen, :-moz-full-screen {
padding: 0 !important;
margin: 0 !important;
}
#maintable {
position: relative; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
#mainframe {
position: relative;
}
#overlay {
display: block;
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 100;
background: transparent;
user-select: none;
}
#loading {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background: transparent;
text-shadow: 0 0 6px #000;
color: #fff;
font-size: 22pt;
text-align: center;
opacity: 1;
transition: opacity 1s ease;
z-index: 1001;
}
#loadtxt {
width: 500px; text-align: center;
display: inline-block;
}
.tbl { display: table; table-layout: fixed; }
.tr { display: table-row; }
.td { display: table-cell; vertical-align: middle;}
.tbl.full { width: 100%; height: 100%; }
#keysWrap {
font-size: 12pt;
font-family: "Droid Sans", "Liberation Sans", "DejaVu Sans", "Segoe UI", Sans;
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
display: /*flex*/ none;
justify-content: center; align-items: center;
/*visibility: hidden;*/
z-index: 1002;
}
#keys {
display: inline-block;
/*width: 400px; height: 400px;*/
width: auto; min-width: 200px; height: auto;
white-space: nowrap;
}
.pad18 {
padding: 18px;
background: rgba(11,11,13,.5);
box-shadow: 0 0 10px 0 #111113;
border-radius: 16px;
display: inline-block;
}
.grey { color: /*#686868*/ #aaa; }
.yellow { color: #fad900; }
.pink { color: #fa00d9; }
#keys > .tbl > .tr > .td:nth-child(1) {
font-weight: bold;
}
.keypad {
padding: 4px 8px;
border: 2px solid #b5857f;
border-radius: 10px;
display: inline-block;
}
#leftJoyContainer {
background: rgba(255,255,255,.1);
width: 100px; height: 100px;
border-radius: 200px;
position: absolute; bottom: 50px; left: 50px;
z-index: 999;
display: none;
touch-action: none; /* Prevents the browser from stealing the touch for scrolling */
}
#rightJoyContainer {
background: rgba(255,255,255,.1);
width: 100px; height: 100px;
border-radius: 200px;
position: absolute; bottom: 50px; right: 50px;
z-index: 999;
display: none;
touch-action: none; /* Prevents the browser from stealing the touch for scrolling */
}
#leftJoyStick {
background: rgba(255,255,255,.3);
width: 50px; height: 50px;
border-radius: 100px;
position: absolute; top: 50%; left: 50%;
margin-left: -25px; margin-top: -25px;
}
#rightJoyStick {
background: rgba(255,255,255,.3);
width: 50px; height: 50px;
border-radius: 100px;
position: absolute; top: 50%; left: 50%;
margin-left: -25px; margin-top: -25px;
}
</style>
</head>
<body>
<div id="loading"><div class="tbl full"><div class="tr"><div class="td"><span id="loadtxt">Loading...</span></div></div></div></div>
<!--<div id="notices"></div>
<div id="info"></div>
<div id="keysWrap">
<div class="pad18">
<div id="keys"></div>
</div>
</div>-->
<audio id="musicplayer"> <!-- no loop to enable track swap -->
<source src="" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<div class="tbl" id="maintable">
<div class="tr">
<div class="td" style="height: 100%;">
<div id="mainframe">
<div id="overlay"></div>
</div>
</div>
</div>
</div>
<div id="leftJoyContainer">
<div id="leftJoyStick"></div>
</div>
<div id="rightJoyContainer">
<div id="rightJoyStick"></div>
</div>
<script type="importmap">
{
"imports": {
"three": "./build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
const $=function(id) { return document.getElementById(id); }
String.prototype.replaceAt=function(index,replacement) { return this.substr(0,index)+replacement+this.substr(index+replacement.length); }
const rndMinMax=function(min,max) { return Math.floor(Math.random()*(max-min+1)+min); }
var IsJsonString=function(str) { try { JSON.parse(str); } catch(e) { return false; } return true; }
function b64Enc(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,function(match,p1) {
return String.fromCharCode(parseInt(p1,16));
}));
}
function b64Dec(str) {
try {
return decodeURIComponent(Array.prototype.map.call(atob(str),function(c) {
return '%'+('00'+c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} catch(e) {
return str;
}
}
var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
var days=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
// Twily is Standard but with animated geometry (skinned mesh)
// share fragment standard
const vertexShaderTwily = `
#include <common>
#include <uv_pars_vertex> // Added: declares attribute vec2 uv;
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <lights_pars_begin>
#include <normal_pars_vertex> // Declares varying vec3 vNormal (view-space)
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 TexCoord; // vUv
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
// varying vec3 vNormal; // Removed: redefined by <normal_pars_vertex>; it's now view-space if you need it
varying vec3 vObjectNormal;
// Manual shadow coord
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform float shadowNormalBias;
uniform float clockShadowNormalBias;
uniform mat4 sunShadowMatrix;
uniform mat4 clockShadowMatrix;
uniform int shadowOn;
void main() {
#include <color_vertex>
#include <morphcolor_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex> // Computes skinMatrix from bone transforms
#include <skinnormal_vertex> // Applies skinMatrix to objectNormal
#include <defaultnormal_vertex> // Added: transforms objectNormal to view-space (transformedNormal = normalMatrix * objectNormal)
#include <normal_vertex> // Added: sets vNormal = normalize(transformedNormal) with flip if needed
#include <begin_vertex> // Sets transformed = position
#include <morphtarget_vertex>
#include <skinning_vertex> // Applies skinMatrix to transformed → now deformed!
#include <displacementmap_vertex> // Added: if you're using displacement (since you have the pars include)
#include <project_vertex> // Sets mvPosition and gl_Position
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
TexCoord = uv;
vObjectNormal = normal;
// vNormal is now set to view-space normal via includes (if you need object-space, add a custom varying vec3 vObjectNormal = objectNormal;)
// World-space normal (using skinned objectNormal; correct for random rotation)
mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
vWorldNormal = normalize(normalMat3 * objectNormal);
vec4 worldPos = modelMatrix * vec4(transformed, 1.0); // Use transformed (skinned position)
vWorldPos = worldPos.xyz;
// Offset for sun
vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
vec3 clockOffset = normal * clockShadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
// For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on
vFogDepth = -mvPosition.z;
}
`;
const vertexShaderStandard = `
varying vec2 TexCoord; // vUv
varying vec3 vWorldNormal; // world-space normal (transformed)
varying vec3 vWorldPos; // world-space position
varying vec3 vObjectNormal; // world-space position
//varying float vFogDepth;
// Manual shadow coord
varying vec4 vSunShadowCoord;
varying vec4 vClockShadowCoord;
uniform float shadowNormalBias;
uniform float clockShadowNormalBias;
uniform mat4 sunShadowMatrix; // from uniform
uniform mat4 clockShadowMatrix;
uniform int shadowOn;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['fog_pars_vertex']}
void main() {
TexCoord = uv;
vObjectNormal = normal;
// World-space normal (correct for random rotation)
mat3 normalMat3 = mat3(transpose(inverse(modelMatrix))); // proper normal transform
vWorldNormal = normalize(normalMat3 * normal);
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vFogDepth = -mvPosition.z;
// Offset for sun
vec3 offset = normal * shadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
vec3 clockOffset = normal * clockShadowNormalBias; // object-space offset (assumes uniform scale; if not, use worldNormal below)
// For non-uniform scale: vec3 offset = vWorldNormal * shadowNormalBias;
vec4 offsetWorldPos = modelMatrix * vec4(position + offset, 1.0);
vec4 clockOffsetWorldPos = modelMatrix * vec4(position + clockOffset, 1.0);
if(shadowOn >= 1) {
vSunShadowCoord = sunShadowMatrix * offsetWorldPos;
// Repeat for moon with another offsetWorldPos if separate bias, but same for now
}
vClockShadowCoord = clockShadowMatrix * clockOffsetWorldPos; // sundial always on
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
${THREE.ShaderChunk['fog_vertex']}
}
`;
const fragmentShaderStandard = `
precision highp float;
${THREE.ShaderChunk['common']}
${THREE.ShaderChunk['packing']}
${THREE.ShaderChunk['fog_pars_fragment']}
uniform sampler2D map;
uniform sampler2D normalMap;
uniform sampler2D roughnessMap;
uniform sampler2D emissiveMap;
uniform vec3 emissive; // color
uniform float emissiveMulti;
uniform int emissiveShadow;
uniform vec3 lightDir;
uniform vec3 lightDir2;
uniform float daytime; // 1 0 1
//uniform float xtime;
uniform float repeatScaleX;
uniform float repeatScaleY;
uniform int shadowOn;
uniform float alphaThreshold;
uniform float roughness;
uniform float metallic;
uniform int transparent; // for clip or alpha
uniform int metarough; // 0(none/values) 1(rough r) or 2(metarough bg)
uniform int flipNormal; // 0 1(x) 2(y) or 3(xy)
uniform int flatFace;
uniform float rimShineStrength;
uniform vec3 rimShineColor;
varying vec2 TexCoord; // vUv
varying vec3 vWorldNormal;
varying vec3 vObjectNormal; // old vNormal but vNormal from twily include skinned mesh is view-space
varying vec3 vWorldPos;
//varying float vFogDepth;
uniform float ambientMulti;
uniform float normalStrength;
void main() {
vec2 vUv = vec2(TexCoord.x * repeatScaleX,TexCoord.y * repeatScaleY); // Scale UVs in vertex for repeating
vec4 albedo = texture(map, vUv);
float rough=roughness; // 0.5
float metal=metallic; // 0.0
if(metarough==1) {
rough=texture2D(roughnessMap, vUv).r; // or b/w?
} else if(metarough==2) {
rough=texture2D(roughnessMap, vUv).g;
metal = texture2D(roughnessMap, vUv).b;
}
vec4 emissiveColor = texture2D(emissiveMap, vUv);
vec3 totalEmissiveRadiance = (emissiveColor.rgb * emissive * emissiveMulti);
//vec3 totalEmissiveRadiance = (emissiveColor.rgb * emissive * emissiveMulti) * (((emissiveColor.r+emissiveColor.g+emissiveColor.b)*.333));
// Object-space normal map perturbation
vec3 normalTex = texture(normalMap, vUv).rgb * 2.0 - 1.0;
// 2. Flip the channels you need
if(flipNormal==1 || flipNormal==3) {
normalTex.x *= -1.0; // Flip horizontal (Red)
}
if(flipNormal==2 || flipNormal==3) {
normalTex.y *= -1.0; // Flip vertical (Green) - MOST COMMON FIX
}
// google ai solved my final dual shadow issues https://share.google/aimode/OfmzmnqFXiKydjrBE
vec3 normal = vWorldNormal;
if(flatFace==1) {
normal = vWorldNormal * (gl_FrontFacing ? 1.0 : -1.0);
}
// Add bump to world-space normal
//vec3 finalNormal = normalize(vWorldNormal + normalTex * normalStrength); // strength 0.8, tune as needed
vec3 finalNormal = normalize(normal + normalTex * normalStrength); // strength 0.8, tune as needed
vec3 viewDir = normalize(cameraPosition - vWorldPos);
float ao=1.0;
float zeron=smoothstep(0.0,0.2,abs((daytime*2.0)-1.0)); // 1 0 1
float mixn=(1.0-((daytime*0.5)+0.5))*zeron; // 0 - 0.5 // night half
float mixt=((daytime*0.5)+0.5)*zeron; // 0.5 - 1 // day half
vec3 lightDir2Mod=vec3(-lightDir2.x,-lightDir2.y,-lightDir2.z);
// Diffuse (sun + moon)
float diff = max(dot(lightDir, finalNormal), 0.0) * ao * mixt;
float diffMoon = max(dot(lightDir2Mod, finalNormal), 0.0) * ao * mixn;
// ──────────────────────────────────────────────────────
// STANDARD SPECULAR (BLINN-PHONG)
// ──────────────────────────────────────────────────────
vec3 halfway = normalize(lightDir + viewDir);
vec3 halfwayMoon = normalize(lightDir2Mod + viewDir);
// Base specular highlights
float specDayBase = pow(max(dot(finalNormal, halfway), 0.0), 64.0); // Higher power for tighter glare glint
float specMoonBase = pow(max(dot(finalNormal, halfwayMoon), 0.0), 32.0);
float spec = specDayBase * rough * (0.04 + metal) * mixt;
float specMoon = specMoonBase * rough * (0.04 + metal) * mixn;
// ──────────────────────────────────────────────────────
// DIRECTIONAL FRESNEL RIM (BACKLIGHT/RIM GLOW)
// ──────────────────────────────────────────────────────
float NdotV_geo = max(0.0, dot(normalize(vObjectNormal), viewDir));
float NdotV_bump = max(0.0, dot(finalNormal, viewDir));
float NdotV = mix(NdotV_geo, NdotV_bump, 0.6);
// Base edge ring intensity
float fresnel = pow(max(1.0 - NdotV, 0.0), 3.0);
// Alignment vectors for directional backlighting
// We look for where the view directory aligns with the outgoing light path
float rimAlignmentDay = max(dot(-lightDir, viewDir), 0.0);
float rimAlignmentNight = max(dot(-lightDir2Mod, viewDir), 0.0);
// Sharpen the alignment window so it concentrates specifically on back edges
float directionalMaskDay = pow(rimAlignmentDay, 2.0);
float directionalMaskNight = pow(rimAlignmentNight, 2.0);
// Assemble directional weights linked directly to active environmental time blocks
float dynamicDirectionalRim = (directionalMaskDay * mixt) + (directionalMaskNight * mixn);
// Environmental multipliers
float rimRoughnessMod = mix(0.6, 1.4, rough);
float rimStrengthNight = mix(0.5, 1.8, mixn);
// Final combined rim value masked by the directional vectors
float rimShine = fresnel * rimShineStrength * rimRoughnessMod * rimStrengthNight * dynamicDirectionalRim;
// ──────────────────────────────────────────────────────
// NEW: FAKE METALLIC SUN/MOON GLARE (SPECULAR BOOST)
// ──────────────────────────────────────────────────────
// Compute the reflection vector of the sun/moon light off the face normal
vec3 reflectDirDay = reflect(-lightDir, finalNormal);
vec3 reflectDirMoon = reflect(-lightDir2Mod, finalNormal);
//reflectDirDay.z=reflectDirDay.z;
reflectDirMoon.z=-reflectDirMoon.z;
// Check how closely the camera view vector lines up with the reflection angle
float glareFactorDay = max(dot(reflectDirDay, viewDir), 0.0) * 1.02;
float glareFactorMoon = max(dot(reflectDirMoon, viewDir), 0.0) * 1.0;
// Sharpen the reflection heavily so it only catches at the perfect viewing angle
// True metals have intensely concentrated reflection highlights
float metallicGlareDay = pow(glareFactorDay, 128.0) * metal * (1.0 - rough) * mixt * 5.5;
float metallicGlareMoon = pow(glareFactorMoon, 64.0) * metal * (1.0 - rough) * mixn * 1.5;
// Metallic reflection rule: Gold/Bronze metals tint their reflections with their own color
vec3 sunGlareColor = mix(vec3(1.0, 0.95, 0.85), albedo.rgb, metal) * metallicGlareDay;
vec3 moonGlareColor = mix(vec3(0.7, 0.85, 1.0), albedo.rgb, metal) * metallicGlareMoon;
vec3 totalMetallicGlare = sunGlareColor + moonGlareColor;
// ──────────────────────────────────────────────────────
// COMPOSITING THE FINAL PASS (WITH BALANCED SUNLIGHT INTENSITY)
// ──────────────────────────────────────────────────────
// Boosted base diffuse multiplier slightly to counter the early morning sun angle dimness
vec3 diffuseComposition = albedo.rgb * max(diff + diffMoon, 0.25) * 4.2;
vec3 standardSpecular = vec3(spec + specMoon);
vec3 directionalRim = rimShineColor * rimShine;
// Add the new glare step straight on top of the composition matrix
vec3 color = diffuseComposition + standardSpecular + directionalRim + totalMetallicGlare;
// shadow section stripped
#ifdef USE_ALBEDO_MAP
float alpha = albedo.a;
#else
float alpha = 1.0;
#endif
if (alpha < alphaThreshold && transparent == 0) discard; // Clip transparent pixels (no blending, but depth sorting works)
// Fog application (using your vFogDepth)
float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
color *= max(fogColor,.2) * 2.0;
color = mix(color, fogColor, fogFactor);
//if(emissiveShadow==1) {
// totalEmissiveRadiance*=(shadow + 1.0);
//}
//#include <fog_fragment>
color+=totalEmissiveRadiance;
//if(shadowOn>=2) {
// gl_FragColor = vec4(vec3(shadow , 0.0, 1.0-shadow), alpha);
//} else {
gl_FragColor = vec4(color, alpha);
//}
//gl_FragColor = vec4(rough, rough, rough, 1.0);
//gl_FragColor = vec4(finalNormal, 1.0);
//gl_FragColor = vec4(halfway, 1.0);
//gl_FragColor = vec4(shadow,shadow,shadow, 1.0);
//gl_FragColor = vec4(normalTex, 1.0);
//gl_FragColor = vec4(vWorldNormal, 1.0);
//gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // red debug
}
`;
// additional to be added
// tree shader for vegetation and simple 2d plane alpha clipping geometry
// fireworks shader for animated sprite images and opacity fade
// end of custom shaders
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// linear(k): Returns k unchanged (constant speed)
function linear(k) {
return k;
}
// easeIn(k): Starts slow and speeds up (e.g., using a power function).
function easeIn(k) {
return Math.pow(k, 2); // Quadratic ease-in
}
// easeOut(k): Starts fast and slows down (a common natural effect, similar to the continuous lerp approach).
function easeOut(k) {
return 1 - Math.pow(1 - k, 2); // Quadratic ease-out
}
// easeInOut(k): Starts slow, speeds up in the middle, and slows down at the end.
function easeInOut(k) {
return k < 0.5 ? 2 * Math.pow(k, 2) : 1 - Math.pow(1 - k, 2) * 2; // Quadratic ease-in-out
// A common alternative using Math.sin for a smoother curve:
// return 0.5 * (Math.sin((k - 0.5) * Math.PI) + 1);
}
// standard lerp t=0-1
function lerp(start,end,t) {
return start * (1 - t) + end * t;
}
// To implement a smoothstep version of your custom lerp in C# for 2026, you can use the standard cubic Hermite interpolation formula. This function creates an "S-curve" that starts and ends slowly, making transitions feel more natural
function SmoothStep(start,end,t) {
// Clamp t between 0 and 1 to prevent overshoot
if(t>1.0) t=1.0;
else if(t<0.0) t=0.0;
// The smoothstep cubic formula: 3t^2 - 2t^3
t = t * t * (3.0 - 2.0 * t);
// Standard interpolation using the smoothed 't'
//return start * (1f - t) + end * t;
return lerp(start,end,t);
}
const radian=Math.PI / 180.0;
let helperGrids = false; // debug
let helperRef=[null,null];
function grid_toggle() {
helperGrids=!helperGrids;
if(helperGrids) {
const helper = new THREE.GridHelper(500, 500);
helper.material.opacity = 0.25;
helper.material.transparent = true;
scene.add(helper);
const axis = new THREE.AxesHelper(1000);
scene.add(axis);
helperRef[0]=helper;
helperRef[1]=axis;
} else {
helperRef[0].material.dispose();
scene.remove(helperRef[0]);
scene.remove(helperRef[1]);
}
}
function updateLookAt(animData, playerGroup, delta, maxAngleDeg = 55) {
// If weight hits absolute 0, immediately bypass everything and let the mixer win
if (animData.lookWeight <= 0) return;
const bone = animData.neckbone;
if (!bone) return;
// 1. Get current world positions
const bonePos = new THREE.Vector3();
bone.getWorldPosition(bonePos);
const targetPos = new THREE.Vector3();
playerGroup.getWorldPosition(targetPos);
//targetPos.y=1; // look height
targetPos.y-=0.4;
// 2. Determine target vector and check field of view limits
const toTarget = new THREE.Vector3().subVectors(targetPos, bonePos).normalize();
const characterForward = new THREE.Vector3(0, 0, 1);
if (bone.parent) {
bone.parent.getWorldDirection(characterForward);
}
const angleToTarget = characterForward.angleTo(toTarget) * THREE.MathUtils.RAD2DEG;
const inView = angleToTarget <= maxAngleDeg;
// 3. Define the desired orientation target
const desiredQuat = new THREE.Quaternion();
if (inView) {
// Build the look-at world matrix target
const lookMatrix = new THREE.Matrix4().lookAt(bonePos, targetPos, new THREE.Vector3(0, 1, 0));
const worldTargetQuat = new THREE.Quaternion().setFromRotationMatrix(lookMatrix);
// Your custom rig offset that perfectly centers the face forward
const rigOffset = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), ((Math.PI * 0.5) * 2.5)); // 90 deg * 2.5
worldTargetQuat.multiply(rigOffset);
// Transform the corrected world rotation into the bone's local coordinate space
if (bone.parent) {
const parentWorldQuat = new THREE.Quaternion();
bone.parent.getWorldQuaternion(parentWorldQuat);
desiredQuat.copy(parentWorldQuat).invert().multiply(worldTargetQuat);
} else {
desiredQuat.copy(worldTargetQuat);
}
//if(angleToTarget<=75) { // max head turn but hold until maxangledeg
// Smoothly blend the tracking target state towards the player's active position
animData.targetLookQuat.slerpQuaternions(
animData.currentLookQuat, // from: previous look frame
desiredQuat, // to: new look coordinates
Math.min(12 * delta, 1) // t: blend speed
);
animData.currentLookQuat.copy(animData.targetLookQuat);
//}
} else {
// BEYOND THE LOOK ARC:
// Instantly make the goalpost target the clean, raw animation track
desiredQuat.copy(animData.baseQuat);
// Smoothly slide the tracking target back toward center forward
animData.targetLookQuat.slerpQuaternions(
animData.currentLookQuat,
desiredQuat,
Math.min(12 * delta, 1)
);
animData.currentLookQuat.copy(animData.targetLookQuat);
}
// 4. CRITICAL BLENDING REWRITE:
// Forcefully wipe the bone clean with the live animation frame first.
bone.quaternion.copy(animData.baseQuat);
// Standard slerp cleanly stacks the weight calculation over the top of the
// live base frame, scaling the overall strength exactly as lookWeight changes.
bone.quaternion.slerp(animData.targetLookQuat, animData.lookWeight);
}
let instanceIdx=0;
function instance_twily() {
const twily=refList['twily'];
twily.updateMatrixWorld(true);
// Import SkeletonUtils if using modules, or use THREE.SkeletonUtils
const rows=20;
const cells=5;
//const rows=100;
//const cells=1;
//const rows=100;
//const cells=10;
const gap=4.0;
const offsetX=rows*gap;
const offsetZ=cells*gap;
for(let x=0;x<rows;x++) {
for(let z=0;z<cells;z++) {
let oX=(x*gap)-(offsetX*.5);
let oZ=(z*gap)-offsetZ;
const twilyInstance = SkeletonUtils.clone(twily);
//const twilyInstance = twily.clone();
const instanceAnim = {
twily: null,
mixer: null,
actions: {},
current: null,
nextAnim: 0.0,
returnAnim: null,
nextBlink: 0.0,
blinkDelta: 0.0,
shapemesh: null,
neckbone: null,
baseQuat: new THREE.Quaternion(),
nextLook: 0.0,
lookTarget: new THREE.Vector3(),
lookWeight: 0.0,
isLooking: false,
currentLookQuat: new THREE.Quaternion(),
targetLookQuat: new THREE.Quaternion(),
};
instanceAnim.twily=twilyInstance;
if (twilyAnimations && twilyAnimations.length > 0) {
instanceAnim.mixer = new THREE.AnimationMixer(twilyInstance);
function createAction(clipName, loopMode = THREE.LoopRepeat, clamp = false, weight = 1.0) {
const clip = THREE.AnimationClip.findByName(twilyAnimations, clipName);
if (!clip) {
console.warn(`Animation clip "${clipName}" not found!`);
return null;
}
const action = instanceAnim.mixer.clipAction(clip);
action.setLoop(loopMode);
action.clampWhenFinished = clamp;
action.weight = weight;
return action;
}
// Assign them – adjust exact names based on your console output!
// Common Blender-exported names: "Idle", "Start", "Cycle", "End", or sometimes "Armature|Idle", "rig.Cycle"
const idleAction = createAction("AnimIdle", THREE.LoopRepeat, false);
const idleLookAction = createAction("AnimIdleLook", THREE.LoopRepeat, false);
const idlePonyAction = createAction("AnimPonyIdleLoop", THREE.LoopRepeat, false);
const idleToPonyAction = createAction("AnimIdleToPony", THREE.LoopOnce, true);
const ponyToIdleAction = createAction("AnimPonyToIdle", THREE.LoopOnce, true);
const squatsAction = createAction("AnimSquats", THREE.LoopRepeat, false);
//const idleLookAction = createAction("AnimIdleLook", THREE.LoopOnce, true); // one-shot, freeze at end
// Store for easy reference
instanceAnim.actions.idle = idleAction;
instanceAnim.actions.idlelook = idleLookAction;
instanceAnim.actions.idlepony = idlePonyAction;
instanceAnim.actions.idletopony = idleToPonyAction;
instanceAnim.actions.ponytoidle = ponyToIdleAction;
instanceAnim.actions.squats = squatsAction;
// Start with Idle
if (idleAction) {
idleAction.play();
instanceAnim.current = idleAction;
//console.log("Started Idle animation instance");
}
refList['twilyInstance'].push(instanceAnim);
// instanceIdx matches push count here
} else {
console.warn("twily.gltf has no animations");
}
twilyInstance.position.set(oX,0.0,oZ);
//twilyInstance.scale.setScalar(100);
//console.dir(twilyInstance);
twilyInstance.traverse(child => {
child.frustumCulled = false; // optional safety — prevents culling bugs on animated bounds
// Optional: make bounds more forgiving
if (child.geometry?.boundingSphere) {
child.geometry.boundingSphere.radius *= 2.0;
}
if (child.isMesh && child.material) {
child.material = refList['twilyMaterial'];
//console.log("Applied custom twily material to instance:", child.name);
}
if (child.isSkinnedMesh) {
if(child.morphTargetInfluences) {
//shapemesh=child;
instanceAnim.shapemesh=child;
}
}
if (child.isBone && child.name === 'neck') {
instanceAnim.neckbone = child;
//instanceAnim.baseQuat.copy(child.quaternion)
}
});
if(!initialized) {
twilyInstance.visible=false;
}
scene.add(twilyInstance);
//console.log("twilyInstance"+instanceIdx+", oX: "+oX+", oZ: "+oZ);
instanceIdx++;
}
}
}
const blinkDuration = 1.0;
const blinkSpeed = 5.0; // adjust this instead, 5.0 speed of 1.0 is .2 duration
const twilyAnim = {
mixer: null,
current: null,
actions: {},
nextAnim: 0.0,
returnAnim: null,
nextBlink: 0.0,
blinkDelta: 0.0,
shapemesh: null,
neckbone: null,
baseQuat: new THREE.Quaternion(),
nextLook: 0.0,
lookTarget: new THREE.Vector3(),
lookWeight: 0.0,
isLooking: false,
currentLookQuat: new THREE.Quaternion(),
targetLookQuat: new THREE.Quaternion(),
};
let twilyAnimations = null;
function preload_twily() {
const textureLoader = new THREE.TextureLoader(manager);
const twilyNude = textureLoader.load('./models/twilysquat/finaltwily.webp?v=3.7');
twilyNude.colorSpace = THREE.SRGBColorSpace;
twilyNude.flipY = false;
[twilyNude].forEach(tex => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1, 1); // Adjust based on your scene scale
});
refList['twilyTextureNude']=twilyNude;
load_twily();
}
let twily = null;
function load_twily() {
const loader = new GLTFLoader();
loader.load('./models/twilysquat/twilysquat.gltf?v=3.7', (gltf) => {
console.log("GLTF loaded successfully!");
console.log("Scene children:", gltf.scene.children.length);
console.log("Animations:", gltf.animations?.length || 0);
gltf.animations?.forEach(clip => {
console.log("Clip:", clip.name, "duration:", clip.duration);
});
twily = gltf.scene;
twilyAnimations = gltf.animations || [];
twily.castShadow = true; // prep for shadows later
twily.receiveShadow = true;
//twily.scale.set(1.0, 1.0, 1.0);
//twily.scale.setScalar(1.6*10.0);
twily.scale.setScalar(1.6);
//const box = new THREE.Box3().setFromObject(twily);
//console.log("twily bounding box:", box.min, box.max);
//console.log("Size:", box.getSize(new THREE.Vector3()));
//twily.position.clone(camera.position);
//twily.position.set(-22, 53, -170);
//twily.position.set(0, 53.43, 2); // front head on
twily.position.set(2.0, 0.0, 0.0); // side
twily.updateMatrixWorld(true);
console.log("twily loaded");
twily.renderOrder = 100; // High renderOrder to draw above everything in its pass
twily.traverse(child => {
if (child.isMesh && child.material) {
if(refList['twilyMaterial']==null) {
child.castShadow = true; // twily doesn't cast on main map (only self-shadows on clock map)
child.receiveShadow = true; // Still receives
child.scale.setScalar(1); // force initial scale to 1 so baked tracks start from visible size
console.log(child.name+" - "+child.material.name);
let textureMap=refList['twilyTextureNude'];
if(!nudeOn) {
textureMap=child.material.emissiveMap;
}
//child.material = createCustomtwilyMaterial(child.material); // or use single instead
//console.dir(child.material);
// Create custom mat here for FRESH material per mesh (don't reuse shared twilyMaterial)
const customMat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone( // clone to avoid sharing
THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
THREE.UniformsLib.fog,
{
//map: { value: child.material.map }, // per-child texture
//normalMap: { value: child.material.normalMap },
//roughnessMap: { value: child.material.roughnessMap },
//map: { value: child.material.emissiveMap },
map: { value: null },
normalMap: { value: null },
roughnessMap: { value: null },
emissiveMap: { value: textureMap },
emissive: { value: new THREE.Color(0xFFFFFF) },
emissiveMulti: { value: 1.5 },
emissiveShadow: { value: 1 },
repeatScaleX: { value: 1.0 },
repeatScaleY: { value: 1.0 },
flipNormal: { value: 0 }, // 0 1(x) 2(y) or 3(xy)
flatFace: { value: 1 },
lightDir: { value: refList['sunLight'].position.normalize() },
lightDir2: { value: refList['sunLight'].position.normalize().negate() },
daytime: { value: 0.0 },
cameraPosition: { value: refList['camera'].position.clone() },
fogColor: { value: new THREE.Color(0xffeeff) },
fogDensity: { value: fogDensity },
alphaThreshold: { value: 0.5 }, // clip
transparent: { value: 1 }, // clip or alpha
metarough: { value: 0 }, // 0, 1 or 2
roughness: { value: 0.5 }, // default val 0.5
metallic: { value: 0.0 }, // default val 0.0
rimShineStrength: { value: 0.28 },
rimShineColor: { value: new THREE.Color(1.0, 0.98, 0.92) },
ambientMulti: { value: 1.0 },
normalStrength: { value: 0.0 },
}
])
),
//vertexShader: vertexShaderStandard,
vertexShader: vertexShaderTwily,
fragmentShader: fragmentShaderStandard,
side: THREE.DoubleSide, // safer for joined models and shadow precision~ (keep)
fog: true,
lights: true,
transparent: false, // or true if you need alpha
//skinning: true,
//morphTargets: false,
depthWrite: true,
depthTest: true,
});
//refList['twilyMaterial'].push(customMat);
//console.dir(customMat);
refList['twilyTextureUnderwear']=child.material.emissiveMap;
child.material = customMat;
refList['twilyMaterial']=customMat;
} else {
child.material = refList['twilyMaterial'];
}
console.log("Applied custom twily material to:", child.name);
}
//child.layers.set(5);
//if (child.isMesh) child.geometry.computeBoundingBox();
});
//twily.layers.set(5);
scene.add(twily);
if(!initialized) {
twily.visible=false;
}
refList['twily']=twily;
let shapemesh=null;
if (gltf.animations && gltf.animations.length > 0) {
twilyAnim.mixer = new THREE.AnimationMixer(twily);
function createAction(clipName, loopMode = THREE.LoopRepeat, clamp = false, weight = 1.0) {
const clip = THREE.AnimationClip.findByName(gltf.animations, clipName);
if (!clip) {
console.warn(`Animation clip "${clipName}" not found!`);
return null;
}
const action = twilyAnim.mixer.clipAction(clip);
action.setLoop(loopMode);
action.clampWhenFinished = clamp;
action.weight = weight;
return action;
}
// Assign them – adjust exact names based on your console output!
// Common Blender-exported names: "Idle", "Start", "Cycle", "End", or sometimes "Armature|Idle", "rig.Cycle"
const idleAction = createAction("AnimIdle", THREE.LoopRepeat, false);
const idleLookAction = createAction("AnimIdleLook", THREE.LoopRepeat, false);
const idlePonyAction = createAction("AnimPonyIdleLoop", THREE.LoopRepeat, false);
const idleToPonyAction = createAction("AnimIdleToPony", THREE.LoopOnce, true);
const ponyToIdleAction = createAction("AnimPonyToIdle", THREE.LoopOnce, true);
const squatsAction = createAction("AnimSquats", THREE.LoopRepeat, false);
//const idleLookAction = createAction("AnimIdleLook", THREE.LoopOnce, true); // one-shot, freeze at end
// Store for easy reference
twilyAnim.actions.idle = idleAction;
twilyAnim.actions.idlelook = idleLookAction;
twilyAnim.actions.idlepony = idlePonyAction;
twilyAnim.actions.idletopony = idleToPonyAction;
twilyAnim.actions.ponytoidle = ponyToIdleAction;
twilyAnim.actions.squats = squatsAction;
// Start with Idle
if (idleAction) {
idleAction.play();
twilyAnim.current = idleAction;
console.log("Started Idle animation");
}
} else {
console.warn("twily.gltf has no animations");
}
twily.traverse(child => {
if (child.isSkinnedMesh) {
child.frustumCulled = false; // optional safety — prevents culling bugs on animated bounds
// Optional: make bounds more forgiving
child.geometry.boundingSphere.radius *= 2.0;
if(child.morphTargetInfluences) {
//shapemesh=child;
twilyAnim.shapemesh=child;
}
}
//if (child.isBone && (child.name === 'neck' || child.name === 'Neck' || child.name === 'Head')) {
if (child.isBone && child.name === 'neck') {
twilyAnim.neckbone = child;
//twilyAnim.baseQuat.copy(child.quaternion)
}
});
console.log("twily added to scene");
//if(shapemesh!=null) {
// console.log("twily has shapekeys");
// Method 1: Modify by array index
//shapemesh.morphTargetInfluences[1] = 0.5; // Halfway between
//shapemesh.morphTargetInfluences[2] = 0.5; // Halfway between
// Method 2: Modify by name using the dictionary
//const idx = mesh.morphTargetDictionary['MyShapeKeyName'];
//if (idx !== undefined) {
// twily.morphTargetInfluences[idx] = 1.0; // Full influence
//}
//}
setTimeout(function() { instance_twily(); },50);
initialized = true;
hideLoadScreen();
setTimeout(function() {
if(refList['twily']!=null) {
refList['twily'].visible = true;
for(let i=0;i<instanceIdx;i++) {
refList['twilyInstance'][i].twily.visible = true;
}
}
});
});
}
// transition helper functions
// Helper: crossfade from current → target action (with reset if one-shot)
function crossFadeTo(targetAction, fadeDuration = 0.6, warp = true, iidx=-1) {
const targetAnim=(iidx>-1)?refList['twilyInstance'][iidx]:twilyAnim;
if (!targetAnim.current || !targetAction || targetAnim.current === targetAction) return;
if (targetAction.loop === THREE.LoopOnce || targetAction.loop === THREE.LoopPingPong) {
targetAction.reset(); // rewind to frame 0 + reset weight
}
targetAction
.reset()
.setEffectiveTimeScale(1.0)
.setEffectiveWeight(1.0)
.fadeIn(fadeDuration)
.play();
targetAnim.current.crossFadeTo(targetAction, fadeDuration, warp, iidx);
targetAnim.current = targetAction;
}
// Assumes all related actions are in twilyActions with consistent naming
function playSequence(startAction, loopAction, endAction, returnAction = null, fadeIn = 0.6, fadeOut = 0.8, iidx=-1) {
const targetAnim=(iidx>-1)?refList['twilyInstance'][iidx]:twilyAnim;
const start = startAction;
const cycle = loopAction; // or cycle, loop, etc.
const end = endAction;
if(returnAction==null) returnAction = targetAnim.actions.idle;
if (!start || !cycle || !end) {
console.warn(`Incomplete sequence for animations~to early?`);
return;
}
crossFadeTo(start, fadeIn, true, iidx);
const onStartDone = (e) => {
if (e.action === start) {
targetAnim.mixer.removeEventListener('finished', onStartDone);
crossFadeTo(cycle, fadeIn, true, iidx);
}
};
targetAnim.mixer.addEventListener('finished', onStartDone);
// Return a stop function so caller can decide when to end
return () => {
crossFadeTo(end, fadeOut, true, iidx);
const onEndDone = (e) => {
if (e.action === end) {
targetAnim.mixer.removeEventListener('finished', onEndDone);
crossFadeTo(returnAction, fadeOut, true, iidx);
}
};
targetAnim.mixer.addEventListener('finished', onEndDone);
targetAnim.returnAnim = null;
};
}
function pointAtPlayer(planeMesh, lookPos) {
// Create a temporary vector for the target (player head at 1.7m)
const look = new THREE.Vector3(
lookPos.x,
1.7, // Your specified player height
lookPos.z
);
planeMesh.lookAt(look);
}
function pointAtPlayerYOnly(planeMesh, lookPos) {
// Set the target Y to be exactly the same as the plane's Y
// This forces the rotation to stay vertical
const look = new THREE.Vector3(
lookPos.x,
planeMesh.position.y,
lookPos.z
);
planeMesh.lookAt(look);
}
function getDirection(theta, phi) { // dir = return
let theta_left = theta + 90.0;
let phi_up = phi - 90.0;
if(theta_left>180.0) theta_left -=360.0;
if(phi_up<-180.0) phi_up += 360.0;
//const x=Math.sin(90.0 * radian) * Math.cos(theta_left * radian);
//const y=Math.cos(90.0 * radian);
//const z=Math.sin(90.0 * radian) * Math.sin(theta_left * radian);
const x=Math.sin(phi_up * radian) * Math.cos(theta * radian);
const y=Math.cos(phi_up * radian);
const z=Math.sin(phi_up * radian) * Math.sin(theta * radian);
//const x=Math.sin(phi * radian) * Math.cos(theta * radian);
//const y=Math.cos(phi);
//const z=Math.sin(phi * radian) * Math.sin(theta * radian);
//Vec3 XZonly={ // for player full directional speed not Y
// cos(theta * radian),
// 0,
// sin(theta * radian)
//};
return new THREE.Vector3(x,y,z);
}
// --- Core Variables ---
let scene, clock, raycaster, mouse, mousep, mousem;
const dayTint = new THREE.Vector3(1.0, 0.9, 0.8); // Warm sun
const nightTint = new THREE.Vector3(0.5, 0.7, 0.8); // Cool blue moon
let currentColor = new THREE.Vector3(1.0,1.0,1.0);
// --- Player State ---
const player = {
height: 1.7,
radius: .5,
velocity: new THREE.Vector3(),
speed: 5.0,
rotation: 0, // Yaw (Left/Right)
pitch: 0, // Pitch (Up/Down)
isGrounded: true,
isCrouching: false,
sensitivity: 0.002
};
const keys = {};
var joy={
left: $('leftJoyStick'), leftC: $('leftJoyContainer'),
right: $('rightJoyStick'), rightC: $('rightJoyContainer')
};
var touchIndex={touch1: null, touch2: null};
var leftJoyHold=false;
var rightJoyHold=false;
let ovrJoysticks = false; // for isLocked auto hide joy
function refreshJoysticks() {
if(useJoysticks && !ovrJoysticks) {
$('leftJoyContainer').style.display="block";
$('rightJoyContainer').style.display="block";
} else {
$('leftJoyContainer').style.display="none";
$('rightJoyContainer').style.display="none";
}
}
function removePinchPointer(e) {
pinchPointers = pinchPointers.filter(p => p.pointerId !== e.pointerId);
if (pinchPointers.length < 2) {
prevPinchDist = -1; // Reset tracking distance
}
}
let pinchPointers = [];
let prevPinchDist = -1;
//var touchIds = { left: null, right: null };
// Map to track which pointer (mouse or finger) controls which side
var activePointers = { left: null, right: null };
// Keep track of which joystick the mouse is physically dragging
var mouseDraggingSide = null;
function holdJoy(e,hold=false,side="left") {
console.log("hold joy trig side="+side);
var register=false;
if(hold) {
// Store the touch ID that started this hold
// e is the touchstart event
//touchIds[side] = e.changedTouches[0].identifier;
// pointerId works for both mouse (usually id 1) and multiple fingers
// If it's a mouse (pointerType === 'mouse'), don't lock the real pointerId to both sides.
// Instead, use a pseudo-id for the right side so it doesn't collide with the left.
if(touchIndex['touch1']==null) {
touchIndex['touch1']=side;
register=true;
} else if(touchIndex['touch2']==null) {
touchIndex['touch2']=side;
register=true;
}
if(side=="left") {
leftJoyHold=true;
} else {
rightJoyHold=true;
}
if (e.pointerType === 'mouse') {
mouseDraggingSide = side; // Lock the mouse to this specific container
} else {
activePointers[side] = e.pointerId; // Lock touch pointer ID
}
if (e.target.setPointerCapture) {
e.target.setPointerCapture(e.pointerId);
}
} else {
activePointers[side] = null;
joy[side].style.left="50%";
joy[side].style.top="50%";
joyRead[side].isHeld=false;
joyRead[side].ratio=0;
joyRead[side].tx=0;
joyRead[side].ty=0;
if(touchIndex['touch1']==side) {
touchIndex['touch1']=null;
}
if(touchIndex['touch2']==side) {
touchIndex['touch2']=null;
}
if(side=="left") {
leftJoyHold=false;
} else {
rightJoyHold=false;
};
if (e.pointerType === 'mouse') {
if (mouseDraggingSide === side) mouseDraggingSide = null;
} else {
activePointers[side] = null;
}
}
}
var joyRead={
left: { isHeld: false, ratio: 0, angle: 0, tx: 0, ty: 0 },
right: { isHeld: false, ratio: 0, angle: 0, tx: 0, ty: 0 },
};
var orbit=25;
function moveJoy(e) {
let side = null;
// 1. Check if this is a mouse event we are dragging
if (e.pointerType === 'mouse') {
side = mouseDraggingSide;
} else { // 2. Otherwise, treat it as a multi-touch event
if (e.pointerId === activePointers.left) side = "left";
else if (e.pointerId === activePointers.right) side = "right";
}
console.log("Pointer ID:", e.pointerId, "Evaluated Side:", side);
// Only process if this specific pointer is mapped to a side
if (side) {
e.preventDefault();
//const X = e.touchX;
//const Y = e.touchY;
// Uniform coordinates for mouse AND touch
const X = e.clientX;
const Y = e.clientY;
//console.log("X: "+X+", Y: "+Y);;
const container = joy[side + "C"];
const eL = container.offsetLeft;
const eT = container.offsetTop;
const eW = container.clientWidth;
const eH = container.clientHeight;
var eX=X-eL; // bounding box = box to circle ?
if(eX<0) eX=0;
var eY=Y-eT;
if(eY<0) eY=0;
if(eX>eW) eX=eW;
if(eY>eH) eY=eH;
var pX=eX*100/eW; // percent 0 - 100
var pY=eY*100/eH;
var cangle=(Math.atan2(pY-50,pX-50) * 180) / Math.PI;
var rX=Math.round(Math.cos(cangle*radian)*orbit+25)+25;
var rY=Math.round(Math.sin(cangle*radian)*orbit+25)+25;
//console.log("pY: "+pY+" pX: "+pX);
//console.log("rY: "+pY+" rX: "+pX);
//console.log("cangle: "+cangle);
if(pX<50) {
if(pX<rX) pX=rX;
} else {
if(pX>rX) pX=rX;
}
if(pY<50) {
if(pY<rY) pY=rY;
} else {
if(pY>rY) pY=rY;
}
joy[side].style.left=(pX)+"px";
joy[side].style.top=(pY)+"px";
var a=50-pX;
var b=50-pY;
var c=Math.sqrt( a*a + b*b );
var v=cangle+180;
joyRead[side].ratio=c*4;
joyRead[side].angle=v;
joyRead[side].isHeld=true;
//console.log('c * 4 ='+(c*4)+" angle="+cangle);
// angle = 0 = right but v+180 = left = 0
// -180/+180 = left right = +180
// -90 = forward backward + 270
// 90 = backward forward = +90
//console.dir(joyRead[side1]);
joyRead[side].tx=Math.cos(joyRead[side].angle*radian)*(joyRead[side].ratio*.01);
joyRead[side].ty=Math.sin(joyRead[side].angle*radian)*(joyRead[side].ratio*.01);
//let tx=Math.cos(v*radian)*((c*4)*.01);
//let ty=Math.sin(v*radian)*((c*4)*.01);
//ty = 1 to -1 forward to backward
//tx = 1 to -1 left to right
//console.log("tx: "+tx+" ty: "+ty);
}
}
function mouse_position(e) {
if(mousem.x === null || mousem.y === null ) {
return; // iniitialize only
}
const X=mousep.x;
const Y=mousep.y;
var sY=window.scrollY;
var sX=window.scrollX;
// isLocked
// Screen warp handling: if delta is huge, it's the browser centering the hidden cursor
//if (Math.abs(mousem.x) > 100 || Math.abs(mousem.y) > 100) return;
// 1. Update Yaw (Left/Right)
//player.rotation -= e.movementX * player.sensitivity;
player.rotation -= mousem.x * player.sensitivity;
// 2. Update Pitch (Up/Down)
//player.pitch -= e.movementY * player.sensitivity;
player.pitch -= mousem.y * player.sensitivity;
// Clamp pitch to 90 degrees up/down
player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));
//if(e.movementX != 0 || e.movementY != 0) {
if(mousem.x != 0 || mousem.y != 0) {
idleTime = 0;
}
}
var musicOn=false;
let useJoysticks = true; // set default start
// check with clock update in animate
function check_music() { // only check if musicOn
if($('musicplayer').paused) {
music_play(true);
}
}
let tracklist=["./high.mp3","./canyon.mp3"];
let firstplay=true; // from beginning first time always
function music_play(reload=false) {
mF.player = $('musicplayer');
// If we need a new track and are currently playing, start Fade Out
if (reload && !mF.player.paused && mF.player.volume > 0) {
mF.fadeDir = -1;
return; // Loop will call execute_track_load once volume hits 0
}
execute_track_load();
function execute_track_load() {
// dice roll to even or odd
//let pick = (rndMinMax(1, 6) % 2 === 0) ? 0 : 1;
// more than 2 tracks
let pick = rndMinMax(0, tracklist.length-1);
//if(firstplay) {
// pick=0; // start high.mp3 first always
//}
mF.player.src = tracklist[pick];
mF.player.addEventListener('loadedmetadata', function() {
let beginning = (rndMinMax(1, 6) === 6) ? true : false;
if(firstplay) {
firstplay=false;
beginning=true;
}
if(!beginning) {
const maxTime = mF.player.duration*.8; // avoid last 20%
mF.player.currentTime = Math.random() * maxTime;
mF.fadeSpeed = .05;
} else {
console.log("music from beginning track");
mF.fadeSpeed = .5;
}
mF.player.volume = 0;
mF.player.play();
mF.fadeDir = 1; // Start Fade In
}, { once: true });
musicLoaded=true;
if(!audioContextInitiated) {
createAudioContext();
}
}
}
function music_pause() {
mF.fadeSpeed = .05;
mF.fadeDir = -1;
//$('player').pause(); // replaced with fade
}
function music_toggle() {
musicOn=!musicOn;
if(musicOn) {
music_play(true); // reload every time
} else {
music_pause();
}
//if(!musicOn){
// musicOn=true; // keep on for fadeout to finish
//}
}
function joy_toggle() {
useJoysticks=!useJoysticks;
refreshJoysticks();
}
const manager = new THREE.LoadingManager();
manager.onLoad = function ( ) {
console.log( "All textures loaded!" );
// Set a flag that all assets are ready for the simulation
refList['assetsReady'] = true;
};
let isLocked = false;
// isLocked input mode disable joystick
// joystick enable separate with J ?
let initialized = false;
let documentReady = false;
var tabTime = 0;
var fakeGround = true;
var tabHidden = false;
document.body.onload=function() {
setTimeout(function() {
documentReady=true;
},3000);
}
let autoStartMusic = false;
const normalMatrix = new THREE.Matrix3(); // create once and reuse
const mvpMatrix = new THREE.Matrix4();
const up = new THREE.Vector3(0, 0, -1); // Depends on your texture orientation
const tilt = new THREE.Matrix4().makeRotationX(Math.PI / 2); // tilt used for projector hitproject
var fogDensity=0.0005; // 0.0008
const skyColors={
topColor: [
[100,94,160], // morning 1
[100,106,190], // day begin 3
[101,115,185], // midday 5
[100,105,170], // day end 3
[55,35,140], // evening 1
[14,7,60], // night begin 3
[1,0,5], // midnight 5 (5 or 35 blue)
[8,10,56], // night end 3
],
bottomColor: [
[165,145,175],// morning
[164,178,160],// day begin
[165,174,204],// midday
[178,170,180],// day end
[215,87,137], // evening
[130,30,45], // night begin
[20,10,30], // midnight
[25,40,75], // night end
],
};
// One-time setup
var loading=0;
var cDensity=0.5;
var skyDepthOn=true;
var musicLoaded=false;
var audioContextInitiated=false;
var audioContext, source, analyser, bufferLength, dataArray;
var sunTime = {}; // theta orient around horizontally, phi vertical/ altitude
var moonTime = {};
var sunOrbit = 1000;
let refList = {
"twily": null,
"camera": null,
"cameraP": null,
"cameraO": null,
"sunLight": null,
"renderer": null,
"playerGroup": null,
"twilyInstance": [],
"twilyMaterial": null,
"twilyTextureNude": null,
"twilyTextureUnderwear": null,
}
function init() {
scene = new THREE.Scene();
sunTime=getDirection(170,90); // theta orient around horizontally, phi vertical/altitude
moonTime=getDirection(-170,-90); // theta orient around horizontally, phi vertical/altitude
// Lights (PBR-friendly)
// light intensities may be separate handled in custom shaderrs*
const ambientLight = new THREE.AmbientLight(0xffffff, 0.05); // Increased ambient for better brightness
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); // Increased intensity
scene.fog = new THREE.FogExp2(0xaaccff, fogDensity);
const aspect = window.innerWidth / window.innerHeight;
//const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 10000);
const cameraP = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
// 2. Set up the camera using the aspect ratio
const cameraO = new THREE.OrthographicCamera(
-viewSize * aspect, // left
viewSize * aspect, // right
viewSize, // top
-viewSize, // bottom
0.1, // near
10000 // far
);
//camera.position.set(-46.15, 1908.31, -2053.56); // far view debug
//camera.lookAt(0, 0, -100); // Look forward along -z
//camera.rotation.set(-6.0*radian, 180.0*radian, 0.0); // (if not using OrbitControls)
cameraP.position.set(0, 0, 0);
cameraO.position.set(0, 0, 0);
cameraP.updateProjectionMatrix();
cameraO.updateProjectionMatrix();
const cameraGroup = new THREE.Group();
scene.add(cameraGroup);
cameraGroup.add(cameraP);
cameraGroup.add(cameraO);
const playerGroup = new THREE.Group();
scene.add(playerGroup);
playerGroup.position.set(2.0, player.height, 3.0);
playerGroup.add(cameraGroup);
// 2. Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, stencil: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = false;
//renderer.shadowMap.type = THREE.PCFSoftShadowMap;
//document.body.appendChild(renderer.domElement);
$('mainframe').appendChild(renderer.domElement);
// One-time setup (after main sunLight)
// To use LinearFilter for VSMs, you might need more specific texture handling:
//renderer.shadowMap.type = THREE.VSMShadowMap; // This uses LinearFilter by default
// If using VSM and needing manual control:
//renderer.shadowMap.type = THREE.VSMShadowMap; // looks great but heavy - enable msgFilter where Wrap and shadowinit x)
renderer.sortObjects = true;
renderer.autoClear = false; // Prevents implicit clearing between renders
renderer.autoClearColor = false;
renderer.autoClearDepth = false;
renderer.autoClearStencil = false;
//renderer.shadowMap.autoUpdate = false; // Stop automatic shadow map update
refList['sunLight']=sunLight;
refList['cameraP']=cameraP;
refList['cameraO']=cameraO;
refList['camera']=cameraGroup;
refList['playerGroup']=playerGroup;
refList['renderer']=renderer;
clock = new THREE.Clock();
preload_twily();
grid_toggle();
// 5. Input Listeners
window.addEventListener('keydown', (e) => keys[e.code] = true);
window.addEventListener('keyup', (e) => keys[e.code] = false);
// 6. Interaction Setup
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2(); // -1 0 1
mousep = new THREE.Vector2(); // 0 to w/h
mousem = new THREE.Vector2(); // mx/my delta
//window.addEventListener('click', onMouseClick);
//console.dir(document.getElementsByTagName("canvas")[0]);
//document.getElementsByTagName("canvas")[0].addEventListener('click', onMouseClick);
$('overlay').addEventListener('click', onMouseClick);
window.addEventListener('resize', onWindowResize);
document.addEventListener("visibilitychange", function() {
if (document.hidden){
//console.log("Browser tab is hidden")
tabTime=new Date().getTime();
//tabTime=lastTime;
if(isLocked || useJoysticks) {
fakeGround=true;
tabHidden=true;
//gravityInitialized=false;
console.log('tab hidden in game mode');
}
} else {
tabHidden=false;
console.log('tab unhidden');
//fakeGround=false;
//console.log("Browser tab is visible")
var nTime=new Date().getTime();
//if(nTime-tabTime>(tabRefS*1000) && tabTime!=0) {
// location.reload();
//} else {
//tabTime=nTime-tabTime;
//}
}
});
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
// Toggle Play/Edit Mode
if (e.code === 'KeyG') {
if (document.pointerLockElement) {
document.exitPointerLock();
} else {
// A 50ms delay bypasses the Chrome "already locked/requesting" glitch
setTimeout(() => {
refList["renderer"].domElement.requestPointerLock();
}, 50);
}
}
if (e.code === 'KeyC') {
player.isCrouching = !player.isCrouching;
}
//if (e.code === 'Period') { // .
if (e.code === 'KeyM') {
music_toggle();
}
if (e.code === 'KeyO') {
cameraMode++;
if(cameraMode>2) cameraMode=0;
switch(cameraMode) {
case 0:
refList['cameraP'].fov = 75;
refList["cameraP"].updateProjectionMatrix();
break;
case 1:
orthoMode=!orthoMode;
break;
case 2:
orthoMode=!orthoMode;
refList['cameraP'].fov = vFOV;
refList["cameraP"].updateProjectionMatrix();
break;
default:
}
}
if (e.code === 'KeyI') {
wireframeOn=!wireframeOn;
refList['twilyMaterial'].wireframe=wireframeOn;
//for(let i=0;i<instanceIdx;i++) {
// refList['twilyInstance'][i]['twily'].wireframe=wireframeOn;
//}
}
if (e.code === 'KeyU') {
// underwear / clothes toggle
nudeOn=!nudeOn;
let newtex=refList['twilyTextureNude'];
if(!nudeOn) {
newtex=refList['twilyTextureUnderwear'];
}
refList['twilyMaterial'].uniforms.emissiveMap.value=newtex;
//for(let i=0;i<instanceIdx;i++) {
//}
refList['twilyMaterial'].needsUpdate = true;
}
if (e.code === 'KeyJ') {
if (!isLocked) {
useJoysticks=!useJoysticks;
refreshJoysticks();
}
}
//if (e.code === 'KeyP') {
// refList["camera"].position.copy(refList["sunLight"].position);
//}
//console.log(e.code);
if(e.code=="NumpadSubtract" || e.code=="Minus") {
musicVolume-=5;
if(musicVolume<0) musicVolume=0;
console.log("Volume down: "+musicVolume);
$('musicplayer').volume=musicVolume*.01;
}
if(e.code=="NumpadAdd" || e.code=="Plus") {
musicVolume+=5;
if(musicVolume>100) musicVolume=100;
console.log("Volume up: "+musicVolume);
$('musicplayer').volume=musicVolume*.01;
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === refList["renderer"].domElement;
ovrJoysticks=isLocked;
refreshJoysticks();
if (isLocked) {
document.body.classList.add('is-locked');
} else {
document.body.classList.remove('is-locked');
}
// Visual feedback (optional)
//const ui = document.getElementById('ui');
//ui.style.border = isLocked ? "2px solid lime" : "2px solid red";
//ui.innerHTML = isLocked ? "<b>MODE: PLAY</b> (G to Edit)" : "<b>MODE: EDIT</b> (G to Play)";
});
document.addEventListener('wheel', (e) => {
//const step=e.deltaY;
const step=.5;
//console.log("step="+step);
//if(e.deltaY>0) {
// markScale+=step
//} else {
// markScale-=step
//}
//if(markScale<1) markScale=1;
//else if(markScale>5) markScale=5;
});
window.addEventListener('pointerdown', (e) => {
// Standard joystick/window checks...
// Only cache touch pointers for pinching if we aren't dragging UI elements
if (e.pointerType === 'touch' && !leftJoyHold && !rightJoyHold) {
pinchPointers.push(e);
}
});
window.addEventListener('mousemove', (e) => {
if(isLocked) {
let mX=e.movementX || 0;
let mY=e.movementY || 0;
// --- FIREFOX SKYWARD DRIFT SANITIZATION ---
// if (mY !== 0 && mY === lastMoveY) {
// consecutiveIdenticalDeltas++;
// } else {
// consecutiveIdenticalDeltas = 0;
// }
// lastMoveY = mY;
// if (consecutiveIdenticalDeltas >= 1) mY = 0; // Kill the infinite loop loop
// ------------------------------------------
mousem.x = mX;
mousem.y = mY;
mouse_position(e);
}
//if(markOn) {
//if (markOn && !leftJoyHold && !rightJoyHold) {
// mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
// mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
//}
});
// window.addEventListener('pointermove', moveJoy);
window.addEventListener('pointermove',function(e) {
if(leftJoyHold || rightJoyHold) {
moveJoy(e);
}
// PINCH TO ZOOM LOGIC: Trigger only if 2 fingers are touching the background
else if (pinchPointers.length === 2 && e.pointerType === 'touch') {
e.preventDefault();
// Update the coordinates for the current moving finger in our cache
for (let i = 0; i < pinchPointers.length; i++) {
if (e.pointerId === pinchPointers[i].pointerId) {
pinchPointers[i] = e;
break;
}
}
// Calculate current distance between the two cached fingers
const dx = pinchPointers[0].clientX - pinchPointers[1].clientX;
const dy = pinchPointers[0].clientY - pinchPointers[1].clientY;
const currentDist = Math.sqrt(dx * dx + dy * dy);
//if (prevPinchDist > 0) {
// const step = 0.1; // Smooth stepping value for mobile scale adjustments
// if (currentDist < prevPinchDist) {
// // Fingers moving closer together -> Zoom Out / Grow Scale
// markScale += step;
// } else if (currentDist > prevPinchDist) {
// // Fingers moving apart -> Zoom In / Shrink Scale
// markScale -= step;
// }
// // Bound the scale exactly like your wheel event bounds it
// if (markScale < 1) markScale = 1;
// else if (markScale > 5) markScale = 5;
//}
prevPinchDist = currentDist;
}
},{ passive: false });
window.addEventListener('touchmove',function(e) {
e.preventDefault();
//if(markOn) {
//if (markOn && !leftJoyHold && !rightJoyHold) {
// mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
// mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
//}
}, { passive: false }); // <--- THIS is critical
// Remove touchstart, touchend, mousedown, mouseup
// Use pointerdown and pointerup for everything
$('leftJoyContainer').addEventListener('pointerdown', (e) => { holdJoy(e, true, 'left') });
$('rightJoyContainer').addEventListener('pointerdown', (e) => { holdJoy(e, true, 'right') });
// Use window for releases so you catch them even if the finger slides off the joystick
window.addEventListener('pointerup', (e) => {
if (e.pointerType === 'mouse') {
if (mouseDraggingSide) {
holdJoy(e, false, mouseDraggingSide);
}
} else {
// Multi-touch fallback
if (e.pointerId === activePointers.left) holdJoy(e, false, 'left');
if (e.pointerId === activePointers.right) holdJoy(e, false, 'right');
}
if (e.pointerType === 'touch') {
removePinchPointer(e);
}
});
window.addEventListener('pointercancel', (e) => {
if (e.pointerType === 'touch') {
removePinchPointer(e);
}
});
}
function onMouseClick(event) {
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, refList["camera"]);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const hit = intersects[0];
console.log("Hit:", hit.object.name, "at", hit.point);
// logic for placing or selecting object would go here
}
}
const downRay = new THREE.Raycaster();
const downVec = new THREE.Vector3(0, -1, 0);
let idleTime=0; // keeps count
let activeTime=10; // cap at 10s
let sprintSpeed = 1.8;
function handleMovement(delta,current,lapse) {
if(!initialized) return;
if(delta>.1) delta=.1; // delta cap for vel y fall after hidden tab
let currentSpeed = player.speed;
//if (keys['ShiftLeft']) currentSpeed *= sprintSpeed;
// built in sprint at 99%
let isSprinting=false;
let joySprint=Math.abs(joyRead['left'].tx)+Math.abs(joyRead['left'].ty);
if(joySprint>=.99 || keys['ShiftLeft']) {
currentSpeed *= sprintSpeed;
isSprinting=true;
}
// 1. Determine Direction Scalars (Cancels out if both keys pressed)
let moveForward = 0;
if (keys['KeyW'] || keys['ArrowUp']) moveForward += 1;
if (keys['KeyS'] || keys['ArrowDown']) moveForward -= 1;
let moveStrafe = 0;
let turnDir = 0;
if(isLocked) { // play mode controls
//if (keys['KeyQ']) document.exitPointerLock();
if (keys['KeyE']) turnDir -= 1;
if (keys['KeyQ']) turnDir += 1;
if (keys['KeyA']) moveStrafe -= 1;
if (keys['KeyD']) moveStrafe += 1;
if (keys['ArrowLeft']) turnDir += 1;
if (keys['ArrowRight']) turnDir -= 1;
} else { // edit mode controls
if (keys['KeyE']) moveStrafe += 1;
if (keys['KeyQ']) moveStrafe -= 1;
if (keys['KeyA'] || keys['ArrowLeft']) turnDir += 1;
if (keys['KeyD'] || keys['ArrowRight']) turnDir -= 1;
}
if(useJoysticks) {
moveForward+=joyRead['left'].ty;
moveStrafe-=joyRead['left'].tx;
if(moveForward>1) moveForward=1;
else if(moveForward<-1) moveForward=-1;
if(moveStrafe>1) moveStrafe=1;
else if(moveStrafe<-1) moveStrafe=-1;
// smooth camera joy
let eRx=easeIn(joyRead['right'].tx);
let eRy=easeIn(joyRead['right'].ty);
let eRxn=(joyRead['right'].tx<0)?true:false;
let eRyn=(joyRead['right'].ty<0)?true:false;
if(eRxn) eRx=-eRx;
if(eRyn) eRy=-eRy;
//turnDir+=joyRead['right'].tx;
turnDir+=eRx;
// up / down
//player.pitch += joyRead['right'].ty * delta;
player.pitch += eRy * delta;
player.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.pitch));
}
// 2. Apply Turning
const turnSpeed = 2.5 * delta;
player.rotation += turnDir * turnSpeed;
// 3. Calculate Final Movement Vector
// We clone the vectors so we don't mutate the camera's base orientation
const forwardVec = new THREE.Vector3(0, 0, -1).applyQuaternion(refList["playerGroup"].quaternion);
forwardVec.y = 0;
forwardVec.normalize();
const rightVec = new THREE.Vector3().crossVectors(forwardVec, new THREE.Vector3(0, 1, 0)).normalize();
// Reset horizontal velocity but keep vertical (for gravity/jumping)
player.velocity.x = 0;
player.velocity.z = 0;
// Add Forward/Back contribution
if (moveForward !== 0) {
player.velocity.add(forwardVec.multiplyScalar(moveForward * currentSpeed));
}
// Add Strafe contribution
if (moveStrafe !== 0) {
player.velocity.add(rightVec.multiplyScalar(moveStrafe * currentSpeed));
}
// Add idle movement to camera in a Lemniscate of Gerono (infinity symbol or a figure-eight (8))
if(moveForward != 0 || moveStrafe != 0 || turnDir != 0) {
idleTime=0;
activeTime+=delta*10;
if(activeTime>10) activeTime=10;
} else {
if(idleTime>1) {
activeTime-=delta;
if(activeTime<0) activeTime=0;
}
}
// https://gemini.google.com/share/18a942a56efd
// Natural Camera / Idle Sway / Camera Bobbing
// Idle Breathing / Camera Sway / Ambient Motion
//const intensity=.1*(Math.min(idleTime,1));
const intensity=0.0; // 0.0 - 1.0 ?
const angle=current/(lapse*.000001);
// pitch needs to oscillate twice as fast for infinity symbol pattern
const addpit=Math.sin(angle * radian * 2)*intensity*.25;
const addrot=Math.sin(angle * radian)*(intensity*2);
// 4. Update Camera Orientation
refList["playerGroup"].quaternion.setFromEuler(new THREE.Euler(0, player.rotation, 0, 'YXZ'));
refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch+addpit, addrot, 0, 'YXZ'));
// head bob walking
// strideSpeed: slow for walk, fast for sprint
const intensityBob=0.0;
const strideSpeed = isSprinting ? 2.0 : 1.0;
const walkAngle = current / (lapse * 0.000000025) * strideSpeed;
// 1. Vertical Bob (Position Y) - Moves up/down twice per stride
const bobY = Math.sin(walkAngle * radian * 2.0) * (intensityBob * 0.5);
// 2. Side Sway (Position X or Rotation Z) - Moves once per stride
const bobX = Math.cos(walkAngle * radian) * (intensityBob * .2);
// 3. Apply to Camera Position (Mental 'Neck' Offset)
const bobMove=new THREE.Vector3(bobX,bobY,0);
//const bobMove=new THREE.Vector3(0,0,0,);
//console.log("bobX: "+bobX+" bobY: "+bobY);
// 4. Subtle Tilt (Roll) - Adds a lot of realism
const bobRoll = Math.cos(walkAngle * radian) * (intensityBob * 0.01);
if(intensity<=0) { // or else will override idle above
//refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, player.rotation, bobRoll, 'YXZ'));
refList["camera"].quaternion.setFromEuler(new THREE.Euler(player.pitch, 0, bobRoll, 'YXZ'));
}
// 5. Physics & Gravity
if (((player.isGrounded || (doublejumpready>0 && doublejumpdelta>0))) && keys['Space']) {
keys['Space']=false;
player.velocity.y = 6.0
player.isCrouching = false;
if(!player.isGrounded) {
doublejumpready--;
doublejumpdelta=0;
console.log("double jump");
} else {
doublejumpdelta=0.6;
console.log("single jump");
}
player.isGrounded = false;
}
//refList["playerGroup"].position.add(player.velocity.clone().multiplyScalar(delta));
const nextPos = refList["playerGroup"].position.add(player.velocity.clone().multiplyScalar(delta));
refList["camera"].position.copy(bobMove);
refList["playerGroup"].position.copy(nextPos);
const targetHeight = player.isCrouching ? 0.8 : 1.7;
player.height = THREE.MathUtils.lerp(player.height, targetHeight, 0.1);
if (refList["playerGroup"].position.y < player.height) {
refList["playerGroup"].position.y = player.height;
player.velocity.y = 0;
player.isGrounded = true;
doublejumpready=1;
doublejumpdelta=0;
}
player.velocity.y -= 15.0 * delta;
}
const viewSize = 2; // This controls your zoom level (height of the view)
// 1. Calculate vFOV for 300mm lens (with 24mm sensor height)
// match blender camera at 300mm focal length about 4.1deg fov
const vFOV = 2 * Math.atan(24 / (2 * 300)) * (180 / Math.PI); // approx 4.12
function onWindowResize() {
refList["cameraP"].aspect = window.innerWidth / window.innerHeight;
setTimeout(() => {
const width = window.innerWidth;
const height = window.innerHeight;
// 1. Target your structural container wrapper
//const container = document.getElementById('mainframe') || $('mainframe');
//const width = container.clientWidth || window.innerWidth;
//const height = container.clientHeight || window.innerHeight;
const nextAspect = width / height;
//refList["renderer"].setSize(width, height, false);
// 2. Proportional Frustum Matching (Mimics a Perspective FOV lock)
if (width >= height) {
// Landscape screens: scale width horizontally, keep height uniform
refList["cameraO"].left = -viewSize * nextAspect;
refList["cameraO"].right = viewSize * nextAspect;
refList["cameraO"].top = viewSize;
refList["cameraO"].bottom = -viewSize;
} else {
// Portrait screens: scale height vertically, keep width uniform
refList["cameraO"].left = -viewSize;
refList["cameraO"].right = viewSize;
refList["cameraO"].top = viewSize / nextAspect;
refList["cameraO"].bottom = -viewSize / nextAspect;
}
//refList["renderer"].setSize(window.innerWidth, window.innerHeight);
// 2. Set canvas resolution buffer, passing false so it doesn't break CSS style
// 3. Flush updates to the WebGL context
refList["cameraO"].updateProjectionMatrix();
refList["cameraP"].updateProjectionMatrix();
refList["renderer"].setSize(width, height);
}, 50);
}
function hideLoadScreen() {
loading=-1;
$('loadtxt').innerHTML="Loading...";
$('loading').style.opacity=0;
$('loading').style.pointerEvents="none";
setTimeout(function() { $('loading').style.display="none"; },1000);
refreshJoysticks();
}
let mF={ // musicfade
fadeDir: 0, // 1 for fade-in, -1 for fade-out, 0 for none
fadeSpeed: .05, // Amount of volume change per second (0.5 = 2 second fade)
player: null, // ref to music player later
}
var dd=null;
var hh=0;
var mm=0;
var ss=0;
var phh="00";
var pmm="00";
var pss="00";
var tsp=0;
var ss_theta=0;
var ss_phi=0;
var testval=500; // set 0 - 2000 (500 = noon, 1500 = midnight)
var tmpColor={};
var testValOn=false; // set
var testValAuto=true; // set
var waitDensity=0.0;
var nextDensity=0.0;
let loadtime=0.0;
let loaddelta=0.0;
let doublejumpready=1;
let doublejumpdelta=0.0;
let lastTime = 0;
//let gradientBG="";
let updateClock = 0;
let fadeStatusLast = 0;
let nextBlink = 0.0;
let blinkDelta = 0.0;
let nextAnim = 0.0; // for sync mode // old nextLook
let lastRnd = 0; // for sync mode
let syncAnimations = true; // set
let nextSyncToggle = rndMinMax(10,40);
let dynamicSyncToggle = true; // set
let orthoMode = false;
let cameraMode = 0;
let wireframeOn = false;
let nudeOn = true;
let musicVolume=60;
let dt=0;
function animate(time) {
requestAnimationFrame(animate);
dt = (time - lastTime) / 1000;
lastTime = time;
loaddelta += dt;
idleTime += dt;
if(refList['twily']) {
// blink
// synced blink
// if(nextBlink<=0.0) {
// blinkDelta-=dt*blinkSpeed;
// if(blinkDelta<=0.0) {
// // trigger once reset
// nextBlink=rndMinMax(2,10);
// //console.log('twily'+i+') nextblink: '+targetAnim.nextBlink);
// blinkDelta=0.0;
// }
// const blink=(blinkDelta*2.0)-blinkDuration; // from positive max to negative max
// synced blink
for(let i=-1;i<instanceIdx;i++) {
const targetAnim=(i>-1)?refList['twilyInstance'][i]:twilyAnim;
const targetTwily=(i>-1)?refList['twilyInstance'][i]['twily']:refList['twily'];
//console.dir(targetTwily);
const twilypos=new THREE.Vector3();
twilypos.copy(targetTwily.position);
const dx = twilypos.x - refList['playerGroup'].position.x;
const dy = twilypos.z - refList['playerGroup'].position.z;
const currentDist = Math.sqrt(dx * dx + dy * dy);
//console.log("currentDist: "+currentDist);
if(targetAnim.nextLook<=0.0) {
targetAnim.nextLook=rndMinMax(1,20);
//if(targetAnim.lookWeight<=0.0) {
// targetAnim.lookWeight = 0.8; // sudden interest
//console.log("twily"+i+") look start");
targetAnim.isLooking=!targetAnim.isLooking;
//targetAnim.isLooking=true; // all look always
//} else {
// targetAnim.lookWeight -= dt;
// if(targetAnim.lookWeight <= 0.0) {
// targetAnim.lookWeight = 0.0;
// }
// //console.log("twily"+i+") look stop");
//}
} else {
targetAnim.nextLook-=dt;
}
if(currentDist<10.0) {
targetAnim.isLooking=true;
}
// Smoothly ease the lookWeight up or down EVERY FRAME based on the active state
const fadeSpeed = 1.0; // Higher = faster transition, Lower = slower transition
if (targetAnim.isLooking) {
// uncomment to only lookat when not sync
//if(syncAnimations) targetAnim.isLooking=false;
// Smoothly fade in toward 0.8 maximum interest
targetAnim.lookWeight += fadeSpeed * dt;
if (targetAnim.lookWeight > 0.8) targetAnim.lookWeight = 0.8;
//else if(i==-1) console.log("twily"+i+") increasing lookweight="+targetAnim.lookWeight);
} else {
// Smoothly fade out toward 0.0 complete rest
targetAnim.lookWeight -= fadeSpeed * dt;
if (targetAnim.lookWeight < 0.0) targetAnim.lookWeight = 0.0;
//else if(i==-1) console.log("twily"+i+") reducing lookweight="+targetAnim.lookWeight);
}
// individual blink
if(targetAnim.nextBlink<=0.0) {
// trigger continous
targetAnim.blinkDelta-=dt*blinkSpeed;
if(targetAnim.blinkDelta<=0.0) {
// trigger once reset
targetAnim.nextBlink=rndMinMax(2,10);
//console.log('twily'+i+') nextblink: '+targetAnim.nextBlink);
targetAnim.blinkDelta=0.0;
}
const blink=(targetAnim.blinkDelta*2.0)-blinkDuration; // from positive max to negative max
// individual blink
if(blink>0.0) {
// positive
// shapemesh.=blink;
targetAnim.shapemesh.morphTargetInfluences[1] = 1.0-blink;
targetAnim.shapemesh.morphTargetInfluences[2] = 1.0-blink;
//console.log('twily'+i+') blink down: '+blink);
} else {
// negative
// shapemesh.=Math.abs(blink);
targetAnim.shapemesh.morphTargetInfluences[1] = 1.0-Math.abs(blink);
targetAnim.shapemesh.morphTargetInfluences[2] = 1.0-Math.abs(blink);
//console.log('twily'+i+') blink up: '+Math.abs(blink));
}
// individual blink
} else {
targetAnim.nextBlink-=dt;
//console.log('twily'+i+') nextBlink: '+targetAnim.nextBlink);
if(targetAnim.nextBlink<=0.0) {
// trigger once
targetAnim.blinkDelta=blinkDuration;
//console.log('twily'+i+') blink start');
}
}
// individual blink
// targetAnim can access blink on both
//targetAnim.nextBlink
//targetAnim.blinkDelta
}
// synced blink
// } else { // if nextblink>0.0
// nextBlink-=dt;
// //console.log('twily'+i+') nextBlink: '+targetAnim.nextBlink);
// if(nextBlink<=0.0) {
// // trigger once
// blinkDelta=blinkDuration;
// //console.log('twily'+i+') blink start');
// }
// }
// synced blink
} // if twily
if(dynamicSyncToggle) {
if(nextSyncToggle<=0.0) {
nextSyncToggle=rndMinMax(30,90);
syncAnimations=!syncAnimations;
} else {
nextSyncToggle-=dt;
}
}
if(syncAnimations) {
nextAnim -= dt;
if(nextAnim <= 0) {
let idleReturn=(lastRnd>0)?true:false;
let rndAnim=0;
if(!idleReturn) {
rndAnim = rndMinMax(1,6);
if(rndAnim>=5) {
nextAnim=rndMinMax(10,30); // duration ponyidle
} else if(rndAnim>=3) {
nextAnim=rndMinMax(10,30); // duration squats
} else {
nextAnim=3.9167; // duration look
}
} else {
nextAnim=rndMinMax(10,40); // wait rndAnim
}
lastRnd=rndAnim;
for(let i=-1;i<instanceIdx;i++) {
const targetAnim=(i>-1)?refList['twilyInstance'][i]:twilyAnim;
if(rndAnim>=5) {
// multi animation with return
targetAnim.returnAnim = playSequence(
targetAnim.actions.idletopony,
targetAnim.actions.idlepony,
targetAnim.actions.ponytoidle,
targetAnim.actions.idle,
0.6,
0.8,
i
);
} else if(rndAnim>=3) {
// single animation with return
crossFadeTo(targetAnim.actions.squats,0.6,true,i);
//console.log('crossfade to idlelook');
targetAnim.returnAnim = function() {
crossFadeTo(targetAnim.actions.idle,0.6,true,i);
//console.log('crossfade to idle');
targetAnim.returnAnim = null
};
} else if(rndAnim>=1) {
// single animation with return
crossFadeTo(targetAnim.actions.idlelook,0.6,true,i);
//console.log('crossfade to idlelook');
targetAnim.returnAnim = function() {
crossFadeTo(targetAnim.actions.idle,0.6,true,i);
//console.log('crossfade to idle');
targetAnim.returnAnim = null
};
} else { // idle return?
crossFadeTo(targetAnim.actions.idle,0.6,true,i);
}
}
}
} else { // individual animations
for(let i=-1;i<instanceIdx;i++) {
const targetAnim=(i>-1)?refList['twilyInstance'][i]:twilyAnim;
targetAnim.nextAnim -= dt;
if(targetAnim.nextAnim <= 0) {
if(targetAnim.returnAnim == null) {
var rndAnim = rndMinMax(1,6);
if(rndAnim>=5) {
// multi animation with return
targetAnim.returnAnim = playSequence(
targetAnim.actions.idletopony,
targetAnim.actions.idlepony,
targetAnim.actions.ponytoidle,
targetAnim.actions.idle,
0.6,
0.8,
i
);
targetAnim.nextAnim=rndMinMax(10,30); // duration ponyidle
} else if(rndAnim>=3) {
// single animation with return
crossFadeTo(targetAnim.actions.squats,0.6,true,i);
//console.log('crossfade to idlelook');
targetAnim.returnAnim = function() {
crossFadeTo(targetAnim.actions.idle,0.6,true,i);
//console.log('crossfade to idle');
targetAnim.returnAnim = null
};
targetAnim.nextAnim=rndMinMax(10,30); // duration squats
} else {
// single animation with return
crossFadeTo(targetAnim.actions.idlelook,0.6,true,i);
//console.log('crossfade to idlelook');
targetAnim.returnAnim = function() {
crossFadeTo(targetAnim.actions.idle,0.6,true,i);
//console.log('crossfade to idle');
targetAnim.returnAnim = null
};
targetAnim.nextAnim=3.9167; // duration look
}
} else {
targetAnim.returnAnim();
targetAnim.nextAnim=rndMinMax(10,40); // wait rndAnim
}
}
}
}
// CAP DELTA: If the tab was hidden, delta might be huge.
// We "freeze" time at 0.1s max to prevent quantum tunneling through floors.
if (dt > 0.1) dt = 0.1;
if(loading!=-1 && loaddelta>0.3) {
loadtime += loaddelta;
loaddelta=0.0;
loading++;
if(loading>3) loading=0;
let loadtxt="Loading";
for(let i=0;i<loading;i++) loadtxt+=".";
if(loadtime>30.0) {
loadtxt+="<br /><span style=\"white-space: nowrap; break-word: none; font-size: 10pt;\">(Taking a long time to load models,<br />try refresh in case it got stuck again x))";
}
$('loadtxt').innerHTML=loadtxt;
//if(initloading) {
// const loadtxtwidth=$('loadtxt').clientWidth;
// $('loadtxt').style.width=(loadtxtwidth+5)+"px"; // hardset
// $('loadtxt').style.textAlign="left";
// initloading=false;
//}
}
if(doublejumpdelta>0) {
doublejumpdelta-=dt;
}
const delta = clock.getDelta(); // For mixers
const globalTime = clock.getElapsedTime();
//let barHeight=cDensity;
//if((musicOn || fadeStatusLast != 0) && audioContextInitiated) {
// if(winState['audio']['chk_music_clouds']) {
// // Get the current frequency data
// analyser.getByteFrequencyData(dataArray);
// // Draw bars for each frequency bin
// for (let i = 0; i < bufferLength; i++) {
// barHeight += dataArray[i]/255; // Amplitude value (0-255)
// }
// barHeight/=bufferLength;
// //barHeight*=2;
// }
//}
//refList["skyMaterial"].uniforms.cDensity.value=cDensity;
//refList["customDepthMat"].uniforms.cDensity.value=cDensity;
//refList["skyMaterial"].uniforms.cDensity.value=barHeight;
//refList["customDepthMat"].uniforms.cDensity.value=barHeight;
//console.log("cDensity="+cDensity);
// --- MUSIC FADE ENGINE ---
if (mF.fadeDir !== 0) {
mF.player = $('musicplayer');
let targetVol = musicVolume*.01;
if (mF.fadeDir === 1) { // Fading In
mF.player.volume = Math.min(targetVol, mF.player.volume + (mF.fadeSpeed * dt));
const fadeStatusNew=Math.floor(mF.player.volume*100);
//const fadeStatusNew=Math.floor((fadeStatusVol*100)/winState['audio']['slider_vol1']);
if(fadeStatusLast!=fadeStatusNew) {
fadeStatusLast=fadeStatusNew;
}
if (mF.player.volume >= targetVol) {
mF.fadeDir = 0;
}
//console.log("music fade in vol="+mF.player.volume);
}
else if (mF.fadeDir === -1) { // Fading Out
mF.player.volume = Math.max(0, mF.player.volume - (mF.fadeSpeed * dt));
//console.log("music fade out vol="+mF.player.volume);
const fadeStatusNew=Math.floor(mF.player.volume*100);
//const fadeStatusNew=Math.floor((fadeStatusVol*100)/winState['audio']['slider_vol1']);
if(fadeStatusLast!=fadeStatusNew) {
fadeStatusLast=fadeStatusNew;
}
if (mF.player.volume <= 0) {
mF.fadeDir = 0;
// DECISION: Reload or just Stop?
if (musicOn) {
music_play(true); // Logic for picking new track
console.log("player new track start");
} else {
mF.player.pause(); // Logic for manual toggle off
console.log("player music paused");
}
}
}
}
var lapse=86400; // realtime
if(testValOn) {
lapse=2000; // debugtime testval uncomment
}
// sun position calculations
dd=new Date();
if(updateClock<=0) {
hh=dd.getHours();
mm=dd.getMinutes();
ss=dd.getSeconds();
phh=(hh<10)?"0"+hh:hh;
pmm=(mm<10)?"0"+mm:mm;
pss=(ss<10)?"0"+ss:ss;
hh=hh*60*60;
mm=mm*60;
ss+=mm+hh;
//let tdate=phh+":"+pmm+":"+pss;
//$('tdate').innerHTML=tdate;
updateClock=0.49;
if(musicOn && documentReady) {
check_music();
}
} else {
hh=dd.getHours()*60*60;
mm=dd.getMinutes()*60;
ss=dd.getSeconds()+mm+hh;
updateClock-=dt;
}
//ss+=(6*60*60); // offset time, - 4-5 hours seem about accurate for sunset at 6pm front
ss-=(6*60*60);
if(ss>86400) ss-=86400;
if(ss<0) ss+=86400;
if(testValOn) {
// debugtime
if(testValAuto) {
testval++;
}
if(testval>2000) testval-=2000;
tsp=testval/1000; // debugtime
ss=testval;
} else {
// realtime
tsp=ss/43200; // realtime comment out for debugtime sunpos
}
ss_phi=((tsp)*180)+90;
if(ss_phi>360) ss_phi-=360;
//ss_theta=(-45);
var scal=0;
if(ss_phi<180) {
// make 86400 into 360 be 0 to 180 and 180 to 360 into 0 to 2 then 0 to 2 into -1 to 0 to 1, then math abs -1 0 1 into 1 0 1 then make 1 0 1 into 0 1 0 with 1- and 0 -1 0 with -1-
scal=Math.abs((ss_phi/90)-1); // 0 1 0
if(ss_phi<90) scal=-scal; //negative edge case
} else {
scal=1-Math.abs(1-((ss_phi-180)/90)-1); // 0 -1 0
}
//ss_theta=(scal*45);
//let smoothscal=SmoothStep(-1.0,1.0,scal);
let smoothscal=(easeInOut((scal+1.0)*.5)*2.0)-1.0; // curve the midnight/midday corner point up/down while arriving at center
ss_theta=(smoothscal*45);
let ssTun = ss - (25*lapse/100);
if(ssTun<0) ssTun+=lapse;
//console.log(Math.abs((ss/(lapse*.5))-1.0));
const daytime=Math.abs((ssTun/(lapse*.5))-1.0); // 1 0 1
let daynightIntensity=0;
let ambientIntensity=0;
//console.log("ss_phi: "+ss_phi+" ss_theta: "+ss_theta+" scal="+scal);
//console.log(" scal="+scal);
// phi and theta have been swapped here intentionally to tilt on a different axis than top/bottom~
sunTime=getDirection(ss_phi,ss_theta); // 90,170 = default front above ground, 180 phi = on ground
moonTime=getDirection(-ss_phi,-ss_theta); // 90,170 = default front above ground, 180 phi = on ground
//sunPhi should be 180 at 6am and 6pm, going to 300 at night and 60 on day
//sunTheta should be 90 at 6pm, -90 or 270 at 6am
if(daytime>=0.49) { // sun below horizon
refList["sunLight"].position.set(sunTime.x*sunOrbit,sunTime.y*sunOrbit,sunTime.z*sunOrbit);
daynightIntensity=((daytime - 0.49) * 2.0) * 5;
ambientIntensity=(1.0 - (daytime * 2.0)) * .3;
//console.dir(refList["sunLight"].position);
} else {
refList["sunLight"].position.set(-moonTime.x*sunOrbit,moonTime.y*sunOrbit,moonTime.z*sunOrbit);
daynightIntensity=(1.0 - (daytime * 2.0)) * 3;
ambientIntensity=(daytime * 2.0) * .3;
//console.dir(refList["sunLight"].position);
}
currentColor.lerpVectors(nightTint, dayTint, daytime);
//refList["sunLight"].color.setFromVector3(currentColor);
//console.log("daynightIntensity: "+daynightIntensity+" ambientIntensity: "+ambientIntensity+" ("+((daytime>=0.49)?"day":"night")+")"+" daytime = "+daytime);
const cameraWorldPosition = new THREE.Vector3();
refList["camera"].getWorldPosition(cameraWorldPosition);
const twily=refList['twily'];
if (twily) {
twily.traverse(child => {
if (child.isMesh && child.material && child.material.isShaderMaterial) {
child.material.uniforms.cameraPosition.value.copy(cameraWorldPosition);
child.material.uniforms.lightDir.value.copy(refList["sunLight"].position).normalize();
child.material.uniforms.lightDir2.value.copy(refList["sunLight"].position).normalize().negate();
child.material.uniforms.daytime.value=daytime;
//child.material.needsUpdate = true;
}
});
if (twilyAnim.mixer) {
twilyAnim.mixer.update(delta);
// Capture the clean animation frame before procedural changes alter it!
if (twilyAnim.neckbone) {
twilyAnim.baseQuat.copy(twilyAnim.neckbone.quaternion);
}
updateLookAt(twilyAnim, refList['playerGroup'], delta, 90);
if(twily.skeleton) {
twily.skeleton.update();
}
}
let instance=null;
for(let i=0;i<instanceIdx;i++) {
if(refList['twilyInstance'][i]) {
instance=refList['twilyInstance'][i];
instance.mixer.update(delta);
// Capture the clean animation frame for instances too!
if (instance.neckbone) {
instance.baseQuat.copy(instance.neckbone.quaternion);
}
updateLookAt(instance, refList['playerGroup'], delta, 90);
if(instance['twily'].skeleton) {
instance['twily'].skeleton.update();
}
}
}
}
//for(let i=0;i<instanceIdx;i++) {
// //refList['twilyInstance']['twily'+i].visible = true;
// console.log(refList['twilyInstance']['twily'+i]);
//}
// sky color calculations
var l=0; // segment selection
var t=ss; // range current
//const hr_1=(4.2*lapse/100); // percentage
const hr_1=(lapse/24); // percentage
const hr_3=hr_1*3;
const hr_4=hr_3+hr_1;
const hr_5=hr_4+hr_1;
const hr_9=hr_5+hr_4;
const hr_12=hr_9+hr_3;
const hr_13=hr_12+hr_1;
const hr_16=hr_13+hr_3;
const hr_21=hr_16+hr_5;
//ss-=(10*lapse/100);
//if(ss<0) ss+=lapse; // rollover
var m=hr_1; // range max
if(ss>hr_21) { // night end 3
l=7;
t=ss-hr_21;
m=hr_3;
} else if(ss>hr_16) { // midnight 5
l=6;
t=ss-hr_16;
m=hr_5;
} else if(ss>hr_13) { // night begin 3
l=5;
t=ss-hr_13;
m=hr_3;
} else if(ss>hr_12) { // evening 1
l=4;
t=ss-hr_12;
m=hr_1;
} else if(ss>hr_9) { // day end 3
l=3;
t=ss-hr_9;
m=hr_3;
} else if(ss>hr_4) { // midday 5
l=2;
t=ss-hr_4;
m=hr_5;
} else if(ss>hr_1) { // day begin 3
l=1;
t=ss-hr_1;
m=hr_3;
}
var l2=l+1;
//if(l2>3) l2=0;
if(l2>7) l2=0;
//console.log("l="+l+" l2="+l2+" t="+t+" m="+m);
tmpColor['r1']=skyColors['topColor'][l][0];
tmpColor['g1']=skyColors['topColor'][l][1];
tmpColor['b1']=skyColors['topColor'][l][2];
tmpColor['r2']=skyColors['topColor'][l2][0];
tmpColor['g2']=skyColors['topColor'][l2][1];
tmpColor['b2']=skyColors['topColor'][l2][2];
tmpColor['r3']=skyColors['bottomColor'][l][0];
tmpColor['g3']=skyColors['bottomColor'][l][1];
tmpColor['b3']=skyColors['bottomColor'][l][2];
tmpColor['r4']=skyColors['bottomColor'][l2][0];
tmpColor['g4']=skyColors['bottomColor'][l2][1];
tmpColor['b4']=skyColors['bottomColor'][l2][2];
let p = Math.min(t / m, 1); // progress
//const eP=p; // linear
const eP = easeInOut(p); // easedProgress
//const currentValue = lerp(startValue, endValue, eP);
tmpColor['rlt']=lerp(tmpColor['r1'],tmpColor['r2'],eP);
tmpColor['glt']=lerp(tmpColor['g1'],tmpColor['g2'],eP);
tmpColor['blt']=lerp(tmpColor['b1'],tmpColor['b2'],eP);
tmpColor['rlb']=lerp(tmpColor['r3'],tmpColor['r4'],eP);
tmpColor['glb']=lerp(tmpColor['g3'],tmpColor['g4'],eP);
tmpColor['blb']=lerp(tmpColor['b3'],tmpColor['b4'],eP);
tmpColor['fr']=lerp(tmpColor['rlb'],tmpColor['rlt'],.66)*.66;
tmpColor['fg']=lerp(tmpColor['glb'],tmpColor['glt'],.66)*.46;
tmpColor['fb']=lerp(tmpColor['blb'],tmpColor['blt'],.66)*.16;
scene.fog = new THREE.FogExp2(new THREE.Color(tmpColor['fr']/255,tmpColor['fg']/255,tmpColor['fb']/255), fogDensity);
const milliseconds=ss+(dd.getMilliseconds()/1000.0);
refList["camera"].updateMatrixWorld();
if(!initialized) return;
if (!tabHidden) {
// Optional: wait 2-3 frames to ensure the Raycaster can actually
// compute intersections against the bounding boxes
//gravityInitialized = true;
fakeGround = false;
}
handleMovement(delta,milliseconds,lapse);
if (initialized && refList["renderer"].domElement.width > 0) {
//refList["renderer"].state.buffers.color.setMask(true);
//refList["renderer"].state.buffers.depth.setMask(true);
//refList["renderer"].state.buffers.stencil.setMask(0xff); // Ensure stencil is clearable
//const gl = refList["renderer"].getContext();
//const bitmask = gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT;
//if (bitmask !== 0) {
refList["renderer"].clear(true, true, true);
//}
// render by custom layer order or render all
if(orthoMode) {
refList["renderer"].render(scene, refList["cameraO"]);
} else {
refList["renderer"].render(scene, refList["cameraP"]);
}
}
}
init();
animate(0);
var audioFilters=[ // hz,db,obj
[60,0.0,null],
[170,0.0,null],
[310,0.0,null],
[600,0.0,null],
[1000,0.0,null],
[3000,0.0,null],
[6000,0.0,null],
[12000,0.0,null],
[14000,0.0,null],
[16000,0.0,null]
];
function createAudioContext() {
audioContextInitiated=true;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create a source node from the audio element
source = audioContext.createMediaElementSource($('musicplayer'));
// Create an AnalyserNode for audio analysis
analyser = audioContext.createAnalyser();
analyser.fftSize = 128; // Number of samples for FFT (higher = more detail)
bufferLength = analyser.frequencyBinCount; // Half of fftSize
dataArray = new Uint8Array(bufferLength); // Array to store frequency data
for(var i=0;i<audioFilters.length;i++) {
audioFilters[i][2] = audioContext.createBiquadFilter();
var type="lowshelf";
if(i>6) {
type="highshelf";
} else if(i>3) {
type="peaking";
}
audioFilters[i][2].type=type;
audioFilters[i][2].frequency.value=audioFilters[i][0];
}
// Connect the audio source to the analyser and then to the output (speakers)
source.connect(analyser);
analyser.connect(audioFilters[0][2]);
for(var i=0;i<audioFilters.length-1;i++) {
audioFilters[i][2].connect(audioFilters[(i+1)][2]);
}
//analyser.connect(audioContext.destination);
audioFilters[9][2].connect(audioContext.destination);
}
</script>
</body>
</HTML>
Top