// Starfield3D.cpp - 3D Parallax Starfield Implementation #include "Starfield3D.h" #include #include Starfield3D::Starfield3D() : rng(std::random_device{}()), width(800), height(600), centerX(400), centerY(300) { } void Starfield3D::init(int w, int h, int starCount) { width = w; height = h; centerX = width * 0.5f; centerY = height * 0.5f; stars.resize(starCount); createStarfield(); } void Starfield3D::resize(int w, int h) { width = w; height = h; centerX = width * 0.5f; centerY = height * 0.5f; } void Starfield3D::setMagnetTarget(float localX, float localY, float strength) { if (strength <= 0.0f) { clearMagnetTarget(); return; } magnetActive = true; magnetStrength = strength; magnetX = std::clamp(localX, 0.0f, static_cast(width)); magnetY = std::clamp(localY, 0.0f, static_cast(height)); } void Starfield3D::clearMagnetTarget() { magnetActive = false; magnetStrength = 0.0f; } float Starfield3D::randomFloat(float min, float max) { std::uniform_real_distribution dist(min, max); return dist(rng); } int Starfield3D::randomRange(int min, int max) { std::uniform_int_distribution dist(min, max - 1); return dist(rng); } void Starfield3D::setRandomDirection(Star3D& star) { star.targetVx = randomFloat(-MAX_VELOCITY, MAX_VELOCITY); star.targetVy = randomFloat(-MAX_VELOCITY, MAX_VELOCITY); // Allow stars to move both toward and away from viewer if (randomFloat(0.0f, 1.0f) < REVERSE_PROBABILITY) { // Move away from viewer (positive Z) star.targetVz = STAR_SPEED * randomFloat(0.5f, 1.0f); } else { // Move toward viewer (negative Z) star.targetVz = -STAR_SPEED * randomFloat(0.7f, 1.3f); } star.changing = true; star.changeTimer = randomFloat(30.0f, 120.0f); // Direction change lasts 30-120 frames } void Starfield3D::updateStar(int index) { Star3D& star = stars[index]; // Avoid spawning stars on (or very near) the view axis. A star with x≈0 and y≈0 // projects to the exact center, and when it happens to be bright it looks like a // static "big" star. constexpr float SPAWN_RANGE = 25.0f; constexpr float MIN_AXIS_RADIUS = 2.5f; // in star-space units for (int attempt = 0; attempt < 8; ++attempt) { star.x = randomFloat(-SPAWN_RANGE, SPAWN_RANGE); star.y = randomFloat(-SPAWN_RANGE, SPAWN_RANGE); if ((star.x * star.x + star.y * star.y) >= (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) { break; } } // If we somehow still ended up too close, push it out deterministically. if ((star.x * star.x + star.y * star.y) < (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) { star.x = (star.x < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS; star.y = (star.y < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS; } star.z = randomFloat(1.0f, MAX_DEPTH); // Give stars initial velocities in all possible directions if (randomFloat(0.0f, 1.0f) < 0.5f) { // Half stars start moving toward viewer star.vx = randomFloat(-0.1f, 0.1f); star.vy = randomFloat(-0.1f, 0.1f); star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f); } else { // Half stars start moving in random directions star.vx = randomFloat(-0.2f, 0.2f); star.vy = randomFloat(-0.2f, 0.2f); // 30% chance to start moving away if (randomFloat(0.0f, 1.0f) < 0.3f) { star.vz = STAR_SPEED * randomFloat(0.5f, 0.8f); } else { star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f); } } // Ensure newly spawned stars have some lateral drift so they don't appear to // "stick" near the center line. if (std::abs(star.vx) < 0.02f && std::abs(star.vy) < 0.02f) { const float sx = (star.x < 0.0f ? -1.0f : 1.0f); const float sy = (star.y < 0.0f ? -1.0f : 1.0f); star.vx = sx * randomFloat(0.04f, 0.14f); star.vy = sy * randomFloat(0.04f, 0.14f); } star.targetVx = star.vx; star.targetVy = star.vy; star.targetVz = star.vz; star.changing = false; star.changeTimer = 0.0f; star.type = randomRange(0, COLOR_COUNT); // Give some stars initial direction variations if (randomFloat(0.0f, 1.0f) < 0.4f) { setRandomDirection(star); } } void Starfield3D::createStarfield() { for (size_t i = 0; i < stars.size(); ++i) { updateStar(static_cast(i)); } } void Starfield3D::update(float deltaTime) { const float frameRate = 60.0f; // Target 60 FPS for consistency const float frameMultiplier = deltaTime * frameRate; for (size_t i = 0; i < stars.size(); ++i) { Star3D& star = stars[i]; // Randomly change direction occasionally if (!star.changing && randomFloat(0.0f, 1.0f) < DIRECTION_CHANGE_PROBABILITY * frameMultiplier) { setRandomDirection(star); } // Update velocities to approach target values if (star.changing) { // Smoothly transition to target velocities const float change = VELOCITY_CHANGE * frameMultiplier; star.vx += (star.targetVx - star.vx) * change; star.vy += (star.targetVy - star.vy) * change; star.vz += (star.targetVz - star.vz) * change; // Decrement change timer star.changeTimer -= frameMultiplier; if (star.changeTimer <= 0.0f) { star.changing = false; } } // Update position using current velocity star.x += star.vx * frameMultiplier; star.y += star.vy * frameMultiplier; star.z += star.vz * frameMultiplier; // Handle boundaries - reset star if it moves out of bounds, too close, or too far if (star.z <= MIN_Z || star.z >= MAX_Z || std::abs(star.x) > 50.0f || std::abs(star.y) > 50.0f) { updateStar(static_cast(i)); } } } void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale) { Uint8 alpha = static_cast(std::clamp(color.a * alphaScale, 0.0f, 255.0f)); SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); // Draw star as a small rectangle (1x1 pixel) SDL_FRect rect{x, y, 1.0f, 1.0f}; SDL_RenderFillRect(renderer, &rect); } void Starfield3D::draw(SDL_Renderer* renderer, float offsetX, float offsetY, float alphaScale, bool grayscale) { // Small visual jitter applied to the visual center so a single bright star // doesn't remain perfectly fixed at the exact pixel center of the viewport. const float jitterAmp = 1.6f; // max pixel offset const uint32_t now = SDL_GetTicks(); const float tms = static_cast(now) * 0.001f; const float centerJitterX = std::sin(tms * 1.7f) * jitterAmp + std::cos(tms * 0.9f) * 0.4f; const float centerJitterY = std::sin(tms * 1.1f + 3.7f) * (jitterAmp * 0.6f); const bool useMagnet = magnetActive && magnetStrength > 0.0f; for (const Star3D& star : stars) { // Calculate perspective projection factor const float k = DEPTH_FACTOR / star.z; // Calculate screen position with perspective float px = star.x * k + centerX + centerJitterX; float py = star.y * k + centerY + centerJitterY; if (useMagnet) { float dx = magnetX - px; float dy = magnetY - py; float dist = std::sqrt(dx * dx + dy * dy); float pull = magnetStrength / (magnetStrength + dist + 1.0f); pull = std::clamp(pull, 0.0f, 0.35f); px += dx * pull; py += dy * pull; } // Only draw stars that are within the viewport if (px >= 0.0f && px <= static_cast(width) && py >= 0.0f && py <= static_cast(height)) { SDL_Color baseColor = STAR_COLORS[star.type % COLOR_COUNT]; if (grayscale) { Uint8 gray = static_cast(0.299f * baseColor.r + 0.587f * baseColor.g + 0.114f * baseColor.b); baseColor.r = baseColor.g = baseColor.b = gray; } drawStar(renderer, px + offsetX, py + offsetY, baseColor, alphaScale); } } }