372 lines
14 KiB
HTML
372 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Three.js - Cosmic Bloom Garden</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: #000000;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
color: white;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
max-width: 400px;
|
|
z-index: 100;
|
|
}
|
|
#info h2 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 18px;
|
|
}
|
|
#info .web-source {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid rgba(255,255,255,0.3);
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
}
|
|
#controls {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 15px 25px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-size: 12px;
|
|
z-index: 100;
|
|
}
|
|
.control-row {
|
|
margin: 5px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.control-row label {
|
|
min-width: 80px;
|
|
}
|
|
.control-row input {
|
|
flex: 1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info">
|
|
<h2>Cosmic Bloom Garden</h2>
|
|
<p><strong>Technique:</strong> Post-Processing with UnrealBloomPass</p>
|
|
<p><strong>Learning:</strong> Implemented EffectComposer pipeline with bloom effects using emissive materials. Learned how to set up multi-pass rendering, configure bloom parameters (threshold, strength, radius), and integrate post-processing into the render loop.</p>
|
|
<div class="web-source">
|
|
<strong>Web Sources:</strong><br>
|
|
<a href="https://github.com/mrdoob/three.js/blob/dev/examples/webgl_postprocessing_unreal_bloom.html" target="_blank" style="color: #4fc3f7;">Three.js Official Bloom Example</a><br>
|
|
<a href="https://waelyasmina.net/articles/post-processing-with-three-js-the-what-and-how/" target="_blank" style="color: #4fc3f7;">Wael Yasmina's Post-Processing Guide</a><br>
|
|
<em>Applied: EffectComposer setup, RenderPass → UnrealBloomPass → OutputPass pipeline, emissive materials for glow</em>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<div class="control-row">
|
|
<label>Strength:</label>
|
|
<input type="range" id="strength" min="0" max="3" step="0.1" value="1.5">
|
|
<span id="strengthValue">1.5</span>
|
|
</div>
|
|
<div class="control-row">
|
|
<label>Radius:</label>
|
|
<input type="range" id="radius" min="0" max="1" step="0.01" value="0.4">
|
|
<span id="radiusValue">0.4</span>
|
|
</div>
|
|
<div class="control-row">
|
|
<label>Threshold:</label>
|
|
<input type="range" id="threshold" min="0" max="1" step="0.01" value="0.15">
|
|
<span id="thresholdValue">0.15</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
|
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
|
|
|
|
// Scene setup
|
|
let camera, scene, renderer, controls;
|
|
let composer, bloomPass;
|
|
let glowingSpheres = [];
|
|
let clock;
|
|
|
|
// Bloom parameters
|
|
const params = {
|
|
threshold: 0.15,
|
|
strength: 1.5,
|
|
radius: 0.4,
|
|
exposure: 1
|
|
};
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// Clock for animations
|
|
clock = new THREE.Clock();
|
|
|
|
// Camera setup
|
|
camera = new THREE.PerspectiveCamera(
|
|
60,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
camera.position.set(0, 5, 15);
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x000510);
|
|
scene.fog = new THREE.Fog(0x000510, 20, 50);
|
|
|
|
// Renderer (WebGL with tone mapping for better bloom)
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.toneMapping = THREE.ReinhardToneMapping;
|
|
renderer.toneMappingExposure = params.exposure;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// OrbitControls for interaction
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.minDistance = 5;
|
|
controls.maxDistance = 30;
|
|
|
|
// Create visualization
|
|
createCosmicGarden();
|
|
|
|
// Setup Post-Processing Pipeline
|
|
// This is the key learning: EffectComposer chains multiple passes
|
|
setupPostProcessing();
|
|
|
|
// Setup UI controls
|
|
setupControls();
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
|
|
function createCosmicGarden() {
|
|
// Ambient light for base illumination
|
|
const ambientLight = new THREE.AmbientLight(0x222244, 0.3);
|
|
scene.add(ambientLight);
|
|
|
|
// Point light for dramatic lighting
|
|
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
|
|
pointLight.position.set(10, 10, 10);
|
|
scene.add(pointLight);
|
|
|
|
// Create glowing spheres in a garden pattern
|
|
// Emissive materials are key for bloom effects
|
|
const colors = [
|
|
0xff0080, // Hot pink
|
|
0x00ffff, // Cyan
|
|
0xff8800, // Orange
|
|
0x00ff88, // Green
|
|
0x8800ff, // Purple
|
|
0xffff00, // Yellow
|
|
0xff0044, // Red
|
|
0x0088ff, // Blue
|
|
];
|
|
|
|
// Create multiple rows of glowing orbs
|
|
for (let row = 0; row < 3; row++) {
|
|
for (let col = 0; col < 8; col++) {
|
|
const geometry = new THREE.SphereGeometry(0.5, 32, 32);
|
|
|
|
// Emissive material is crucial for bloom
|
|
// Higher emissive intensity = stronger glow
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: colors[col],
|
|
emissive: colors[col],
|
|
emissiveIntensity: 2.5,
|
|
metalness: 0.8,
|
|
roughness: 0.2
|
|
});
|
|
|
|
const sphere = new THREE.Mesh(geometry, material);
|
|
|
|
// Position in grid pattern
|
|
sphere.position.x = (col - 3.5) * 2.5;
|
|
sphere.position.y = 1 + row * 2.5;
|
|
sphere.position.z = -row * 3;
|
|
|
|
// Store animation data
|
|
sphere.userData = {
|
|
originalY: sphere.position.y,
|
|
phase: Math.random() * Math.PI * 2,
|
|
speed: 0.5 + Math.random() * 0.5,
|
|
amplitude: 0.3 + Math.random() * 0.4
|
|
};
|
|
|
|
scene.add(sphere);
|
|
glowingSpheres.push(sphere);
|
|
}
|
|
}
|
|
|
|
// Create glowing ground plane
|
|
const groundGeometry = new THREE.PlaneGeometry(50, 50);
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x001133,
|
|
emissive: 0x001133,
|
|
emissiveIntensity: 0.5,
|
|
metalness: 0.9,
|
|
roughness: 0.1
|
|
});
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = -Math.PI / 2;
|
|
ground.position.y = 0;
|
|
scene.add(ground);
|
|
|
|
// Add glowing particles in the background
|
|
const particleCount = 500;
|
|
const particleGeometry = new THREE.BufferGeometry();
|
|
const positions = new Float32Array(particleCount * 3);
|
|
const colors = new Float32Array(particleCount * 3);
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
positions[i * 3] = (Math.random() - 0.5) * 50;
|
|
positions[i * 3 + 1] = Math.random() * 30;
|
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 50;
|
|
|
|
// Bright colors for particles
|
|
const color = new THREE.Color();
|
|
color.setHSL(Math.random(), 1.0, 0.7);
|
|
colors[i * 3] = color.r;
|
|
colors[i * 3 + 1] = color.g;
|
|
colors[i * 3 + 2] = color.b;
|
|
}
|
|
|
|
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
|
|
const particleMaterial = new THREE.PointsMaterial({
|
|
size: 0.15,
|
|
vertexColors: true,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
blending: THREE.AdditiveBlending
|
|
});
|
|
|
|
const particles = new THREE.Points(particleGeometry, particleMaterial);
|
|
scene.add(particles);
|
|
}
|
|
|
|
function setupPostProcessing() {
|
|
// Key Learning: Post-processing pipeline setup
|
|
// EffectComposer manages multiple rendering passes
|
|
|
|
// 1. Create the composer with the renderer
|
|
composer = new EffectComposer(renderer);
|
|
|
|
// 2. First pass: Render the scene normally
|
|
const renderScene = new RenderPass(scene, camera);
|
|
composer.addPass(renderScene);
|
|
|
|
// 3. Second pass: Apply Unreal Bloom effect
|
|
// Parameters: resolution, strength, radius, threshold
|
|
bloomPass = new UnrealBloomPass(
|
|
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
|
params.strength, // Bloom strength (intensity)
|
|
params.radius, // Bloom radius (spread)
|
|
params.threshold // Luminance threshold (what glows)
|
|
);
|
|
composer.addPass(bloomPass);
|
|
|
|
// 4. Final pass: Output to screen with tone mapping
|
|
const outputPass = new OutputPass();
|
|
composer.addPass(outputPass);
|
|
|
|
// Note: Order matters! RenderPass → Effects → OutputPass
|
|
}
|
|
|
|
function setupControls() {
|
|
// Connect UI controls to bloom parameters
|
|
const strengthSlider = document.getElementById('strength');
|
|
const radiusSlider = document.getElementById('radius');
|
|
const thresholdSlider = document.getElementById('threshold');
|
|
|
|
strengthSlider.addEventListener('input', (e) => {
|
|
bloomPass.strength = parseFloat(e.target.value);
|
|
document.getElementById('strengthValue').textContent = e.target.value;
|
|
});
|
|
|
|
radiusSlider.addEventListener('input', (e) => {
|
|
bloomPass.radius = parseFloat(e.target.value);
|
|
document.getElementById('radiusValue').textContent = e.target.value;
|
|
});
|
|
|
|
thresholdSlider.addEventListener('input', (e) => {
|
|
bloomPass.threshold = parseFloat(e.target.value);
|
|
document.getElementById('thresholdValue').textContent = e.target.value;
|
|
});
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
// Important: Resize composer too!
|
|
composer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const elapsedTime = clock.getElapsedTime();
|
|
|
|
// Animate glowing spheres with floating motion
|
|
glowingSpheres.forEach((sphere) => {
|
|
const data = sphere.userData;
|
|
sphere.position.y = data.originalY +
|
|
Math.sin(elapsedTime * data.speed + data.phase) * data.amplitude;
|
|
|
|
// Pulse the emissive intensity for extra glow
|
|
sphere.material.emissiveIntensity = 2.0 +
|
|
Math.sin(elapsedTime * data.speed * 2 + data.phase) * 0.5;
|
|
});
|
|
|
|
// Update controls
|
|
controls.update();
|
|
|
|
// Key Learning: Render using composer instead of renderer.render()
|
|
// This applies all post-processing effects
|
|
composer.render();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|