<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Psychedelic 3D Creature Simulator</title>
<style>
body { margin: 0; overflow: hidden; background-color: #191970; /* Match initial bg */ }
canvas { display: block; }
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display:block;
color: white;
font-family: monospace;
text-shadow: 1px 1px 2px black; /* Add shadow for visibility */
}
</style>
</head>
<body>
<div id="info">Psychedelic 3D Creature Simulator<br/>Use mouse/touch to look around and zoom.</div>
<canvas id="c"></canvas>
<!-- Import Map for Three.js Modules -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
<!-- Main Three.js Logic -->
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // Needed for loading advanced models
let scene, camera, renderer, controls, clock;
let ground, sun;
const creatures = [];
const environmentObjects = [];
const worldBoundary = 200; // How far creatures roam before wrapping
const worldSize = worldBoundary * 2.5; // Make ground larger than roam area
// Define the new color palette
const colorPalette = {
blue: 0x0077FF, // Brighter Blue
red: 0xFF0000,
yellow: 0xFFFF00,
violet: 0x9400D3, // DarkViolet
darkBlue: 0x00008B, // DarkBlue
purple: 0x8A2BE2, // BlueViolet
brightYellow: 0xFFD700, // Gold
darkViolet: 0x4B0082, // Indigo
crimson: 0xDC143C
};
const environmentPalette = [colorPalette.blue, colorPalette.red, colorPalette.yellow, colorPalette.violet];
// --- Initialization ---
function init() {
clock = new THREE.Clock();
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(colorPalette.darkBlue); // Dark Blue background
scene.fog = new THREE.Fog(colorPalette.darkBlue, worldBoundary * 0.8, worldBoundary * 1.5); // Match background
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, worldBoundary * 2);
camera.position.set(0, 20, 40);
// Renderer
const canvas = document.querySelector('#c');
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Lighting (Keep lights white for neutral illumination)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(50, 80, 30);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 200;
const shadowCamSize = 80;
sun.shadow.camera.left = -shadowCamSize;
sun.shadow.camera.right = shadowCamSize;
sun.shadow.camera.top = shadowCamSize;
sun.shadow.camera.bottom = -shadowCamSize;
scene.add(sun);
scene.add(sun.target);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.1;
controls.minDistance = 5;
controls.maxDistance = worldBoundary * 0.7;
// Ground
const groundGeometry = new THREE.PlaneGeometry(worldSize, worldSize);
// Use a violet shade for the ground
const groundMaterial = new THREE.MeshStandardMaterial({ color: colorPalette.purple, side: THREE.DoubleSide });
ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Create Environment
createEnvironment();
// Create Creatures
createCreatures();
// Handle Window Resize
window.addEventListener('resize', onWindowResize);
// Start Animation Loop
animate();
}
// --- Environment Creation ---
function createEnvironment() {
const density = 0.1;
const objectCount = Math.floor(worldSize * worldSize * density / 50);
// Trees
for (let i = 0; i < objectCount / 2; i++) {
createTree(getRandomPositionOnGround());
}
// Plants/Flowers/Fruit Trees (Simplified)
for (let i = 0; i < objectCount; i++) {
createPlant(getRandomPositionOnGround());
}
}
function createTree(position) {
const trunkHeight = THREE.MathUtils.randFloat(4, 8);
const trunkRadius = trunkHeight * 0.1;
const foliageHeight = trunkHeight * THREE.MathUtils.randFloat(1.5, 2.5);
const foliageRadius = trunkHeight * THREE.MathUtils.randFloat(0.6, 1.0);
const trunkGeom = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8);
// Dark Violet trunk color
const trunkMat = new THREE.MeshStandardMaterial({ color: colorPalette.darkViolet });
const trunk = new THREE.Mesh(trunkGeom, trunkMat);
trunk.position.set(position.x, trunkHeight / 2, position.z);
trunk.castShadow = true;
trunk.receiveShadow = true;
scene.add(trunk);
environmentObjects.push(trunk);
// Foliage (Sphere) - Random color from the environment palette
const foliageGeom = new THREE.SphereGeometry(foliageRadius, 8, 6);
const foliageColor = environmentPalette[Math.floor(Math.random() * environmentPalette.length)];
const foliageMat = new THREE.MeshStandardMaterial({ color: foliageColor });
const foliage = new THREE.Mesh(foliageGeom, foliageMat);
foliage.position.set(position.x, trunkHeight + foliageRadius*0.5, position.z);
foliage.castShadow = true;
foliage.receiveShadow = true;
scene.add(foliage);
environmentObjects.push(foliage);
// Simple Fruits (Bright Yellow)
if (Math.random() < 0.2) {
for(let f=0; f<5; f++) {
const fruitGeom = new THREE.SphereGeometry(foliageRadius * 0.1, 6, 6);
// Bright Yellow fruit
const fruitMat = new THREE.MeshStandardMaterial({ color: colorPalette.brightYellow });
const fruit = new THREE.Mesh(fruitGeom, fruitMat);
const fruitPos = new THREE.Vector3()
.randomDirection()
.multiplyScalar(foliageRadius * Math.random())
.add(foliage.position);
fruit.position.copy(fruitPos);
fruit.castShadow = true;
scene.add(fruit);
environmentObjects.push(fruit);
}
}
}
function createPlant(position) {
const plantHeight = THREE.MathUtils.randFloat(0.5, 1.5);
const plantRadius = plantHeight * THREE.MathUtils.randFloat(0.3, 0.6);
// Simple representation (Cone) - Random color from the environment palette
const plantGeom = new THREE.ConeGeometry(plantRadius, plantHeight, 6);
const plantColor = environmentPalette[Math.floor(Math.random() * environmentPalette.length)];
const plantMat = new THREE.MeshStandardMaterial({ color: plantColor });
const plant = new THREE.Mesh(plantGeom, plantMat);
plant.position.set(position.x, plantHeight / 2, position.z);
plant.castShadow = true;
scene.add(plant);
environmentObjects.push(plant);
}
function getRandomPositionOnGround() {
const x = THREE.MathUtils.randFloat(-worldBoundary, worldBoundary);
const z = THREE.MathUtils.randFloat(-worldBoundary, worldBoundary);
return new THREE.Vector3(x, 0, z);
}
// --- Creature Creation (Simplified Geometry with New Colors) ---
function createCreatures() {
const numEach = 10;
// Butterflies (Still random bright colors - they fit the theme)
for (let i = 0; i < numEach; i++) {
const geom = new THREE.SphereGeometry(0.2, 8, 6);
const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color().setHSL(Math.random(), 1.0, 0.7) }); // Keep random HSL
const butterfly = new THREE.Mesh(geom, mat);
butterfly.position.copy(getRandomPositionOnGround()).add(new THREE.Vector3(0, THREE.MathUtils.randFloat(2, 6), 0));
setupCreature(butterfly, 'butterfly', THREE.MathUtils.randFloat(2, 5), true);
scene.add(butterfly);
creatures.push(butterfly);
}
// Birds (Blue)
for (let i = 0; i < numEach; i++) {
const geom = new THREE.ConeGeometry(0.3, 0.8, 8);
const mat = new THREE.MeshStandardMaterial({ color: colorPalette.blue }); // Blue birds
const bird = new THREE.Mesh(geom, mat);
bird.position.copy(getRandomPositionOnGround()).add(new THREE.Vector3(0, THREE.MathUtils.randFloat(5, 15), 0));
bird.rotation.x = Math.PI / 2;
setupCreature(bird, 'bird', THREE.MathUtils.randFloat(5, 10), true);
bird.castShadow = true;
scene.add(bird);
creatures.push(bird);
}
// Tigers (Red)
for (let i = 0; i < numEach / 2; i++) {
const geom = new THREE.CapsuleGeometry(0.8, 1.5, 4, 8);
const mat = new THREE.MeshStandardMaterial({ color: colorPalette.crimson }); // Red tigers
const tiger = new THREE.Mesh(geom, mat);
tiger.position.copy(getRandomPositionOnGround()).add(new THREE.Vector3(0, 0.8, 0));
tiger.rotation.x = Math.PI / 2;
setupCreature(tiger, 'tiger', THREE.MathUtils.randFloat(3, 6), false);
tiger.castShadow = true;
scene.add(tiger);
creatures.push(tiger);
}
// Cats (Yellow)
for (let i = 0; i < numEach / 2; i++) {
const geom = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
const mat = new THREE.MeshStandardMaterial({ color: colorPalette.brightYellow }); // Yellow cats
const cat = new THREE.Mesh(geom, mat);
cat.position.copy(getRandomPositionOnGround()).add(new THREE.Vector3(0, 0.4, 0));
cat.rotation.x = Math.PI / 2;
setupCreature(cat, 'cat', THREE.MathUtils.randFloat(2, 5), false);
cat.castShadow = true;
scene.add(cat);
creatures.push(cat);
}
// Dogs (Violet)
for (let i = 0; i < numEach / 2; i++) {
const geom = new THREE.CapsuleGeometry(0.6, 1.2, 4, 8);
const mat = new THREE.MeshStandardMaterial({ color: colorPalette.violet }); // Violet dogs
const dog = new THREE.Mesh(geom, mat);
dog.position.copy(getRandomPositionOnGround()).add(new THREE.Vector3(0, 0.6, 0));
dog.rotation.x = Math.PI / 2;
setupCreature(dog, 'dog', THREE.MathUtils.randFloat(4, 8), false);
dog.castShadow = true;
scene.add(dog);
creatures.push(dog);
}
}
// Helper to set up creature movement data (No changes needed here)
function setupCreature(mesh, type, speed, canFly) {
mesh.userData = {
type: type,
speed: speed,
canFly: canFly,
velocity: new THREE.Vector3(),
target: getRandomTarget(mesh.position, canFly),
wanderlust: 0.02,
flyHeight: canFly ? THREE.MathUtils.randFloat(2, type === 'bird' ? 15 : 6) : mesh.position.y,
baseY: mesh.position.y,
flutter: Math.random() * Math.PI * 2
};
}
function getRandomTarget(currentPosition, canFly) {
const target = getRandomPositionOnGround();
if (canFly) {
target.y = THREE.MathUtils.randFloat(2, 15);
} else {
target.y = currentPosition.y; // Use base Y for ground creatures target
// Correction: Use the stored baseY if available, otherwise current Y
target.y = currentPosition.userData?.baseY ?? currentPosition.y;
}
target.sub(currentPosition).normalize().multiplyScalar(THREE.MathUtils.randFloat(20, 80)).add(currentPosition);
const innerBoundary = worldBoundary * 0.95;
target.x = THREE.MathUtils.clamp(target.x, -innerBoundary, innerBoundary);
target.z = THREE.MathUtils.clamp(target.z, -innerBoundary, innerBoundary);
if(canFly) {
target.y = THREE.MathUtils.clamp(target.y, 1, 20);
} else {
target.y = currentPosition.userData?.baseY ?? currentPosition.y; // Ensure ground target is at base Y
}
return target;
}
// --- Animation Loop (No changes needed here) ---
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
creatures.forEach(creature => {
updateCreature(creature, delta, elapsed);
});
controls.update();
renderer.render(scene, camera);
}
// --- Creature Movement Logic (Small correction for ground creature Y target) ---
function updateCreature(creature, delta, elapsed) {
const data = creature.userData;
const direction = new THREE.Vector3().subVectors(data.target, creature.position);
const distanceToTarget = direction.length();
if (distanceToTarget < data.speed * delta * 5 || distanceToTarget < 1.0) {
data.target = getRandomTarget(creature.position, data.canFly);
direction.subVectors(data.target, creature.position); // Recalculate direction
}
direction.normalize();
data.velocity.lerp(direction.multiplyScalar(data.speed), data.wanderlust);
const moveStep = data.velocity.clone().multiplyScalar(delta);
creature.position.add(moveStep);
if (!(creature.geometry instanceof THREE.SphereGeometry)) {
const lookTargetPos = creature.position.clone().add(data.velocity.clone().normalize().multiplyScalar(2)); // Look slightly ahead
if(!data.canFly) {
lookTargetPos.y = data.baseY; // Keep horizontal look for ground animals
}
creature.lookAt(lookTargetPos);
if (creature.geometry instanceof THREE.CapsuleGeometry || creature.geometry instanceof THREE.ConeGeometry) {
creature.rotateX(Math.PI / 2); // Geometry orientation correction
}
}
if (data.canFly) {
data.flutter += delta * 10;
const flutterAmount = data.type === 'butterfly' ? 0.05 : 0.02;
creature.position.y += Math.sin(data.flutter) * flutterAmount;
if(Math.abs(creature.position.y - data.flyHeight) > 0.5) {
creature.position.y += Math.sign(data.flyHeight - creature.position.y) * delta * 0.5;
}
} else {
creature.position.y = data.baseY; // Keep ground creatures strictly on ground
}
// Wrap around
if (Math.abs(creature.position.x) > worldBoundary) {
creature.position.x = -creature.position.x * 0.98;
}
if (Math.abs(creature.position.z) > worldBoundary) {
creature.position.z = -creature.position.z * 0.98;
}
if(data.canFly){
creature.position.y = THREE.MathUtils.clamp(creature.position.y, 0.5, 25);
}
}
// --- Utility: Handle Window Resize (No changes needed here) ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
// --- Start ---
init();
</script>
</body>
</html>
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.