1
0
mirror of https://github.com/rzzzwilson/pymlac.git synced 2025-06-10 09:32:41 +00:00
Files
rzzzwilson.pymlac/pymlac/tests/star3d.py
2015-05-20 19:02:17 +07:00

281 lines
10 KiB
Python
Executable File

#!/usr/bin/env python
"""
A cute 3D starfield effect.
My first experiment with PyGame, and the most Python I've ever written!
There's plenty of room for further optimization here, thats left as an exercise for the reader ;)
Will McGugan - will@willmcgugan.com
http://www.willmcgugan.com
"""
__copyright__ = "2005 Will McGugan"
__version__ = "1.0"
__license__ = "Public Domain"
__author__ = "Will McGugan"
import random, math, pygame
from pygame.locals import *
try:
import psyco
psyco.full()
except ImportError:
print "This demo runs much smoother with the 'Psyco' module"
print "You can get it from http://psyco.sourceforge.net/"
def DegreesToRadians( degrees ):
"Converts an angle from degrees to radians."
return degrees * ( math.pi / 180. )
# Look ma, globals!
# Some values you can fiddle with...
# Size of the window
# Actualy looks better in lo-res with chuncky pixels! (Try it in fullscreen)
SCREEN_WIDTH= 800 # 640
SCREEN_HEIGHT= 600 # 480
# 1 for fullscreen, 0 for windowed
FULLSCREEN_MODE= 1
# Stars per Z unit
STAR_DENSITY= 1
# Distance of far plane
STAR_DISTANCE= 4000.
# Field of view
FOV= DegreesToRadians( 90. )
# Rotation speed of camera
ROTATION_SPEED= DegreesToRadians( 40. )
# Speed of camera
SPEED= 2000.
# Use wu-pixels (anti-aliased points)
WU_PIXELS= 0
class Star( object ):
"A 'star' here is just a point in 3D space."
def __init__( self, x, y, z ):
"Initialise a star at point x, y, z"
self.x, self.y, self.z = x, y, z
def render( self, surface, x, y, bright ):
"""Renders a star of brightness 'bright' at x, y.
Uses 'wu-pixel' algorithm derived from http://freespace.virgin.net/hugo.elias/graphics/x_wupixl.htm
Basically it draws a point at fractional coordinates."""
if not WU_PIXELS:
b= int( min( 255., bright * 255. ) )
surface.set_at( ( int(x), int(y) ), ( b, b, b ) )
return
if x >= 0. and x < SCREEN_WIDTH and y >= 0. and y < SCREEN_HEIGHT:
# Get the fractional, and the whole part of the coordinates
fx, ix = math.modf( x )
fy, iy = math.modf( y )
ix = int(ix)
iy = int(iy)
# Scale brightness (a value between 0 and 1), to a colour value (0-255)
bright= min( bright*255., 255. )
# Calculate the brightness of each sub pixel, see the link above
btl = int( (1.-fx) * (1.-fy) * bright )
btr = int( fx * (1.-fy) * bright )
bbl = int( (1.-fx)* fy * bright )
bbr = int( fx * fy * bright )
# Plot the pixel on screen
surface.set_at( ( ix, iy ), ( btl, btl, btl ) )
surface.set_at( ( ix+1, iy ), ( btr, btr, btr ) )
surface.set_at( ( ix, iy+1 ), ( bbl, bbl, bbl ) )
surface.set_at( ( ix+1, iy+1 ), ( bbr, bbr, bbr ) )
class Starfield( object ):
"Stores and renders starfield"
def __init__( self, density = 1. ):
"Density is in stars per unit of z"
self.density = density
# List of stars
self.stars = []
# The point in the distance where we have generated new stars
self.generated_z = 0
def render( self, surface, fov, angle, position_z, distance ):
"Renders all the stars."
# Screen dimensions
view_width = float( SCREEN_WIDTH )
view_height = float( SCREEN_HEIGHT )
# Aspect ratio of screen
aspect = view_width / view_height
# Calculate the distance where 3D units equal screen units
view_distance = ( view_width / 2. ) / math.tan( fov / 2. )
# Calculate the maximum exents of projected pixels
far_width = math.tan( fov / 2. ) * distance * 2.;
far_height = far_width / aspect
max_diameter = math.sqrt( far_width * far_width + far_height * far_height )
# This is the radius of a cone that the camera would fit inside
# Its used to cull stars that are no longer potentialy visible
screen_radius= math.sqrt( view_width * view_width + view_height * view_height ) / 2.
# Generate new stars in the distance, untill we have at least enough stars to render a screen-full
generate_distance = 10
while self.generated_z < position_z + distance:
self.add_stars( self.generated_z, max_diameter, max_diameter, generate_distance )
self.generated_z += generate_distance
# clear screen
background_colour = 0, 0, 0
surface.fill( background_colour )
# Adjustment required to put stars in the centre of the screen
centre_x = view_width / 2.
centre_y = view_height / 2.
# Precalculate 1 over distance, because it is generally quicker to multiply by 1/x that it is to divide by x
oodistance = 1. / distance
# Create a list of all the stars which are currently visible, or could become visible
visible_stars = []
# Multiply brightness by constant scale so they are clearer
bright_scale = 1.5
# Precalculate sin and cos of the angle
sin_angle= math.sin( angle )
cos_angle= math.cos( angle )
# Project ( transform 3D to 2D ) and render all stars
for star in self.stars:
remove_star = False
star_z= star.z - position_z
# If the star is in front of the camera...
if star.z > position_z + .1:
# If it is within the visible z range...
if star.z <= position_z + distance:
# Precalculate 1 over z
oostar_z = 1. / star_z
screen_x = star.x * cos_angle - star.y * sin_angle
screen_y = star.x * sin_angle + star.y * cos_angle
screen_x *= view_distance * oostar_z
screen_y *= view_distance * oostar_z
# If the star is within the camera cone...
if math.fabs( screen_x ) < screen_radius and math.fabs( screen_y ) < screen_radius:
star_z_over_distance= star_z * oodistance
# Calculate a brightness value so the stars fade in, rather than pop in
bright = ( 1. - star_z_over_distance * star_z_over_distance ) * bright_scale
star.render( surface, screen_x + centre_x, screen_y + centre_y, bright )
else:
# The star is out of the potentialy visible cone in front of the camera
remove_star = True
else:
# The star as behind the camera, and will not be seen again
remove_star = True
if not remove_star:
visible_stars.append( star )
# Replace star list with only visible stars, so we dont continue to process any stars no longer visible
self.stars = visible_stars
def add_stars( self, z, width, height, distance ):
"Add random stars in to the distance."
new_star_count = int( distance * self.density )
centre_x = width / 2.
centre_y = height / 2.
for _ in xrange( new_star_count ):
new_star = Star( random.random() * width - centre_x,
random.random() * height - centre_y,
z + random.random() * distance )
self.stars.append( new_star )
def main():
print "3D stars effect"
print "Will McGugan will@willmcgugan.com"
print "Try my game - Ping Ball (http://www.pingball.com )"
print ""
print "Move the mouse left and right to change rotation, up and down to change speed"
print "Press (W) to toggle wu-pixels"
random.seed()
pygame.init()
screen_dimensions= SCREEN_WIDTH, SCREEN_HEIGHT
if FULLSCREEN_MODE:
screen = pygame.display.set_mode( screen_dimensions, FULLSCREEN )
pygame.mouse.set_visible( False )
else:
screen = pygame.display.set_mode( screen_dimensions )
pygame.display.set_caption( '3D Stars Example' )
backbuffer= pygame.Surface( screen_dimensions, SWSURFACE, screen )
# Camera z
# For this example x will be the width of the screen, y will be the height, and z will be 'in' the screen
cam_z= 0.
# Camera angle
angle= 0.
# Create a clock so we can position the camera correctly
clock = pygame.time.Clock()
# Create our starfield
starfield = Starfield( STAR_DENSITY )
# So we can toggle wu-pixels
global WU_PIXELS
done= 0
while not done:
# Calculate the speed and rotation speed based on current mouse position
mouse_x, mouse_y = pygame.mouse.get_pos()
half_width = float( SCREEN_WIDTH ) / 2.
rotation_speed = ROTATION_SPEED * ( float( mouse_x ) - half_width ) / half_width ;
camera_speed = SPEED * float( SCREEN_HEIGHT - mouse_y ) / float( SCREEN_HEIGHT )
# Move the 'camera' forward, using the time and speed to calculate its position
time_passed = clock.tick() / 1000.
angle += time_passed * rotation_speed
cam_z += time_passed * camera_speed
# Lock the display, render the stars
screen.lock()
starfield.render( screen, FOV, angle, cam_z, STAR_DISTANCE )
screen.unlock()
# Flip next frame
pygame.display.flip()
# Process events
for e in pygame.event.get():
if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
done = 1
break
elif e.type == KEYUP and e.key == K_w:
WU_PIXELS = (1,0)[WU_PIXELS]
print "Wu-pixels", ("off","on")[WU_PIXELS]
if __name__ == '__main__':
main()