Make it look more like actual mountains
/*
@nwWrld name: AudioDreamMountains
@nwWrld category: Audio
@nwWrld imports: BaseThreeJsModule, THREE, Noise, AudioAnalyzer
*/
class AudioDreamMountains extends BaseThreeJsModule {
static methods = [
{
name: "start",
executeOnLoad: true,
options: [
{ name: "sensitivity", defaultVal: 2.0, type: "number", min: 0.1, max: 5.0 },
{ name: "mountainIntensity", defaultVal: 1.0, type: "number", min: 0.1, max: 100.0 },
{ name: "speed", defaultVal: 1.0, type: "number", min: 0.1, max: 10.0 },
],
},
{
name: "setSensitivity",
executeOnLoad: false,
options: [{ name: "value", defaultVal: 2.0, type: "number", min: 0.1, max: 5.0 }],
},
{
name: "setMountainIntensity",
executeOnLoad: false,
options: [{ name: "value", defaultVal: 1.0, type: "number", min: 0.1, max: 100.0 }],
},
{
name: "setSpeed",
executeOnLoad: false,
options: [{ name: "value", defaultVal: 1.0, type: "number", min: 0.1, max: 3.0 }],
},
{
name: "setAudioReactive",
executeOnLoad: false,
options: [{ name: "enabled", defaultVal: true, type: "boolean" }],
},
{
name: "setSnowIntensity",
executeOnLoad: false,
options: [{ name: "value", defaultVal: 1.0, type: "number", min: 0.0, max: 3.0 }],
},
];
constructor(container) {
super(container);
if (!THREE || !Noise) return;
this.name = AudioDreamMountains.name;
this.customGroup = new THREE.Group();
this.mountains = [];
this.mountainSegments = [];
this.snowParticles = null;
this.noise = new Noise(Math.random());
this.analyzer = null;
this.audioReady = false;
this.pollInterval = null;
this.sensitivity = 2.0;
this.mountainIntensity = 1.0;
this.speed = 1.0;
this.audioReactive = true;
this.volume = 0;
this.bass = 0;
this.mid = 0;
this.treble = 0;
this.time = 0;
this.cameraX = 0;
this.currentMountainHeight = 0;
this.snowIntensity = 1.0;
this.sky = null;
this.moon = null;
this.moonRings = [];
this.spotlight = null;
this.rimLight = null;
this.ambientLight = null;
this.init();
}
async start({ sensitivity = 2.0, mountainIntensity = 1.0, speed = 1.0 } = {}) {
const sensVal = Number(sensitivity);
const intensityVal = Number(mountainIntensity);
const speedVal = Number(speed);
this.sensitivity = Math.max(0.1, Math.min(5.0, Number.isFinite(sensVal) ? sensVal : 2.0));
this.mountainIntensity = Math.max(0.1, Math.min(3.0, Number.isFinite(intensityVal) ? intensityVal : 1.0));
this.speed = Math.max(0.1, Math.min(3.0, Number.isFinite(speedVal) ? speedVal : 1.0));
this.snowIntensity = 1.0;
await this.tryInitializeAudio();
if (!this.audioReady) this.startStreamPolling();
}
async tryInitializeAudio() {
if (this.audioReady || this.destroyed) return;
const sdk = globalThis.nwWrldSdk;
const stream = sdk?.audio?.getStream?.();
if (stream) {
this.analyzer = new AudioAnalyzer();
const initialized = await this.analyzer.init(stream);
if (initialized) {
this.audioReady = true;
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
}
}
startStreamPolling() {
if (this.pollInterval) return;
this.pollInterval = setInterval(() => {
if (this.destroyed) {
clearInterval(this.pollInterval);
this.pollInterval = null;
return;
}
this.tryInitializeAudio();
}, 1000);
}
init() {
if (!this.renderer || !this.scene || !this.camera || this.destroyed) return;
this.scene.fog = new THREE.FogExp2(0x1a1a2e, 0.008);
this.renderer.setClearColor(0x1a1a2e);
this.setupLighting();
this.createMountains();
this.createSnowParticles();
this.createMountainSky();
this.setModel(this.customGroup);
this.setCustomAnimate(this.audioAnimate.bind(this));
}
setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
this.ambientLight = ambientLight;
const spotlight = new THREE.SpotLight(0xffffff, 20, 40, Math.PI * 0.2, 0.4);
spotlight.position.set(0, 20, 10);
spotlight.target.position.set(0, 0, 0);
spotlight.castShadow = true;
this.scene.add(spotlight);
this.scene.add(spotlight.target);
this.spotlight = spotlight;
const rimLight = new THREE.SpotLight(0x88ccff, 12, 30, Math.PI * 0.25, 0.5);
rimLight.position.set(-15, 15, -10);
rimLight.target.position.set(0, 0, 0);
this.scene.add(rimLight);
this.scene.add(rimLight.target);
this.rimLight = rimLight;
}
createMountains() {
this.mountainSegments = [];
const segmentSize = 200;
const segmentCount = 3;
const mountainsPerSegment = 15;
for (let segIndex = 0; segIndex < segmentCount; segIndex++) {
const segmentGroup = new THREE.Group();
segmentGroup.position.set((segIndex - 1) * segmentSize, 0, 0);
for (let i = 0; i < mountainsPerSegment; i++) {
const width = 8 + Math.random() * 15;
const height = 4 + Math.random() * 12;
const depth = 8 + Math.random() * 15;
const geometry = new THREE.ConeGeometry(width, height, 8, 1);
const originalPositions = geometry.attributes.position.array.slice();
const material = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0.2,
roughness: 0.7,
side: THREE.DoubleSide,
emissive: 0xffffff,
emissiveIntensity: 0.1,
});
const mountain = new THREE.Mesh(geometry, material);
mountain.position.set(
(i - mountainsPerSegment / 2) * 12,
height / 2,
(Math.random() - 0.5) * 100 - 30
);
mountain.rotation.y = (Math.random() - 0.5) * 0.5;
mountain.userData = {
originalPositions: originalPositions,
baseHeight: height,
baseWidth: width,
};
segmentGroup.add(mountain);
this.mountains.push(mountain);
}
segmentGroup.userData = {
segmentIndex: segIndex,
};
this.customGroup.add(segmentGroup);
this.mountainSegments.push(segmentGroup);
}
}
createSnowParticles() {
const positions = [];
const colors = [];
for (let i = 0; i < 800; i++) {
positions.push(
(Math.random() - 0.5) * 600,
Math.random() * 60,
(Math.random() - 0.5) * 300
);
colors.push(1, 1, 1);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.25,
vertexColors: true,
transparent: true,
opacity: 0.9,
sizeAttenuation: true,
});
this.snowParticles = new THREE.Points(geometry, material);
this.customGroup.add(this.snowParticles);
}
createMountainSky() {
const skyGeo = new THREE.SphereGeometry(500, 32, 32);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {
topColor: { value: new THREE.Color(0x1a1a2e) },
bottomColor: { value: new THREE.Color(0x16213e) },
offset: { value: 33 },
exponent: { value: 0.6 }
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition).y + offset * 0.01;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`
});
this.sky = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(this.sky);
const moonGeo = new THREE.SphereGeometry(15, 32, 32);
const moonMat = new THREE.MeshBasicMaterial({
color: 0xe8e8e8,
transparent: true,
opacity: 0.95
});
this.moon = new THREE.Mesh(moonGeo, moonMat);
this.moon.position.set(0, 30, -120);
this.scene.add(this.moon);
this.moonRings = [];
for (let i = 0; i < 8; i++) {
const ringGeo = new THREE.RingGeometry(15 + i * 2.5, 15 + i * 2.5 + 0.4, 64);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x88ccff,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7 - i * 0.08
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.position.copy(this.moon.position);
ring.lookAt(0, 0, 0);
this.scene.add(ring);
this.moonRings.push(ring);
}
}
audioAnimate() {
if (this.destroyed) return;
this.time += 0.016;
if (this.audioReactive && this.analyzer && this.audioReady) {
this.volume = this.analyzer.getVolume() * this.sensitivity;
this.bass = this.analyzer.getBass() * this.sensitivity;
this.mid = this.analyzer.getMid() * this.sensitivity;
this.treble = this.analyzer.getTreble() * this.sensitivity;
} else if (!this.audioReactive) {
this.volume = 0.3 * this.sensitivity;
this.bass = 0.2 * this.sensitivity;
this.mid = 0.3 * this.sensitivity;
this.treble = 0.2 * this.sensitivity;
}
const flySpeed = this.speed * (1 + this.volume * 0.5);
this.cameraX += flySpeed * 0.3;
this.updateMountains();
this.updateSnow();
this.updateLighting();
this.updateCamera();
}
updateMountains() {
if (!this.mountainSegments || this.mountainSegments.length === 0) return;
const mountainHeight = this.mountainIntensity * 1.5 * (0.8 + this.bass * 0.6);
this.currentMountainHeight = mountainHeight;
const segmentSize = 200;
this.mountainSegments.forEach((segmentGroup) => {
const relativeX = segmentGroup.position.x - this.cameraX;
if (relativeX < -segmentSize * 1.5) {
segmentGroup.position.x += segmentSize * 3;
}
segmentGroup.children.forEach((mountain) => {
const positions = mountain.geometry.attributes.position;
const originalPositions = mountain.userData.originalPositions;
const baseHeight = mountain.userData.baseHeight;
for (let i = 0; i < positions.count; i++) {
const i3 = i * 3;
const ox = originalPositions[i3];
const oy = originalPositions[i3 + 1];
const oz = originalPositions[i3 + 2];
const worldX = ox + mountain.position.x + segmentGroup.position.x;
const worldZ = oz + mountain.position.z;
const noise1 = this.noise.simplex2(worldX * 0.05, worldZ * 0.05 + this.time * 0.3) * mountainHeight * 0.3;
const noise2 = this.noise.simplex2(worldX * 0.1, worldZ * 0.1 + this.time * 0.5) * mountainHeight * 0.2;
const wave = Math.sin(worldX * 0.1 + this.time * 1.5) * mountainHeight * 0.1;
const heightMod = oy > 0 ? 1 + (noise1 + noise2 + wave) / baseHeight : 1;
positions.array[i3] = ox * (1 + (noise1 + noise2) * 0.05);
positions.array[i3 + 1] = oy * heightMod;
positions.array[i3 + 2] = oz * (1 + (noise1 + noise2) * 0.05);
}
positions.needsUpdate = true;
mountain.geometry.computeVertexNormals();
const brightness = 0.7 + this.mid * 0.3;
const snowBrightness = 0.9 + this.treble * 0.1;
mountain.material.color.setRGB(brightness, brightness, snowBrightness);
mountain.material.emissiveIntensity = 0.1 + this.treble * 0.2;
});
});
}
updateSnow() {
if (!this.snowParticles || this.snowIntensity === 0) return;
const positions = this.snowParticles.geometry.attributes.position;
const colors = this.snowParticles.geometry.attributes.color;
for (let i = 0; i < positions.count; i++) {
const i3 = i * 3;
positions.array[i3 + 1] -= 0.1 * this.snowIntensity * (1 + this.treble * 0.3);
positions.array[i3] += Math.sin(this.time * 2 + i) * 0.02;
positions.array[i3 + 2] += Math.cos(this.time * 1.5 + i) * 0.02;
if (positions.array[i3 + 1] < -5) {
positions.array[i3] = this.cameraX + (Math.random() - 0.5) * 400;
positions.array[i3 + 1] = 40 + Math.random() * 20;
positions.array[i3 + 2] = (Math.random() - 0.5) * 300;
}
const brightness = 0.7 + this.treble * 0.3;
colors.array[i3] = brightness;
colors.array[i3 + 1] = brightness;
colors.array[i3 + 2] = brightness;
}
positions.needsUpdate = true;
colors.needsUpdate = true;
this.snowParticles.material.size = 0.2 * this.snowIntensity + this.treble * 0.15;
this.snowParticles.material.opacity = 0.9 * Math.min(this.snowIntensity, 1.0);
}
updateLighting() {
if (this.spotlight) {
this.spotlight.intensity = 20 + this.bass * 25;
this.spotlight.position.x = this.cameraX;
this.spotlight.target.position.x = this.cameraX;
}
if (this.rimLight) {
this.rimLight.intensity = 12 + this.mid * 18;
this.rimLight.position.x = this.cameraX - 15;
this.rimLight.target.position.x = this.cameraX;
}
if (this.moon) {
this.moon.position.x = this.cameraX;
}
if (this.moonRings) {
this.moonRings.forEach((ring) => {
ring.position.x = this.cameraX;
});
}
}
updateCamera() {
const baseHeight = 18;
const mountainHeightOffset = this.currentMountainHeight * 0.3;
const height = baseHeight + mountainHeightOffset + Math.sin(this.time * 0.15) * 2;
this.camera.position.x = this.cameraX;
this.camera.position.y = height;
this.camera.position.z = 12;
this.camera.lookAt(this.cameraX + 25, height - 6, 0);
}
setSensitivity({ value = 2.0 } = {}) {
const val = Number(value);
this.sensitivity = Math.max(0.1, Math.min(5.0, Number.isFinite(val) ? val : 2.0));
}
setMountainIntensity({ value = 1.0 } = {}) {
const val = Number(value);
this.mountainIntensity = Math.max(0.1, Math.min(3.0, Number.isFinite(val) ? val : 1.0));
}
setSpeed({ value = 1.0 } = {}) {
const val = Number(value);
this.speed = Math.max(0.1, Math.min(3.0, Number.isFinite(val) ? val : 1.0));
}
setAudioReactive({ enabled = true } = {}) {
this.audioReactive = Boolean(enabled);
}
setSnowIntensity({ value = 1.0 } = {}) {
const val = Number(value);
this.snowIntensity = Math.max(0.0, Math.min(3.0, Number.isFinite(val) ? val : 1.0));
}
destroy() {
this.destroyed = true;
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.analyzer) {
this.analyzer.destroy();
this.analyzer = null;
}
if (this.mountains) {
this.mountains.forEach((mountain) => {
mountain.geometry.dispose();
mountain.material.dispose();
this.customGroup.remove(mountain);
});
this.mountains = [];
}
if (this.snowParticles) {
this.snowParticles.geometry.dispose();
this.snowParticles.material.dispose();
this.customGroup.remove(this.snowParticles);
}
if (this.sky) {
this.sky.geometry.dispose();
this.sky.material.dispose();
this.scene.remove(this.sky);
}
if (this.moon) {
this.moon.geometry.dispose();
this.moon.material.dispose();
this.scene.remove(this.moon);
}
if (this.moonRings) {
this.moonRings.forEach((ring) => {
ring.geometry.dispose();
ring.material.dispose();
this.scene.remove(ring);
});
this.moonRings = [];
}
if (this.spotlight) {
this.scene.remove(this.spotlight);
this.scene.remove(this.spotlight.target);
}
if (this.rimLight) {
this.scene.remove(this.rimLight);
this.scene.remove(this.rimLight.target);
}
if (this.ambientLight) {
this.scene.remove(this.ambientLight);
}
if (this.customGroup) {
this.scene.remove(this.customGroup);
}
super.destroy();
}
}
export default AudioDreamMountains;