-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
23e1605
commit 300665c
Showing
1 changed file
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
import numpy as np | ||
from PIL import Image, ImageDraw | ||
import io | ||
import matplotlib.pyplot as plt | ||
from IPython.display import display, Image as IPImage | ||
from noise import pnoise3 | ||
|
||
def calculate_ambient_occlusion(world, size, x, y, z): | ||
directions = [ | ||
(-1, -1, 0), (1, -1, 0), (-1, 1, 0), (1, 1, 0), | ||
(-1, 0, -1), (1, 0, -1), (-1, 0, 1), (1, 0, 1), | ||
(0, -1, -1), (0, 1, -1), (0, -1, 1), (0, 1, 1), | ||
(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1) | ||
] | ||
occlusion = 0 | ||
for dx, dy, dz in directions: | ||
nx, ny, nz = x + dx, y + dy, z + dz | ||
if 0 <= nx < size and 0 <= ny < size and 0 <= nz < size: | ||
if world[nx, ny, nz] > 0: | ||
occlusion += 1 | ||
return occlusion / len(directions) | ||
|
||
def precompute_ambient_occlusion(world, size): | ||
ao_matrix = np.zeros_like(world, dtype=np.float32) | ||
for x in range(size): | ||
for y in range(size): | ||
for z in range(size): | ||
if world[x, y, z] > 0: | ||
ao_matrix[x, y, z] = calculate_ambient_occlusion(world, size, x, y, z) | ||
return ao_matrix | ||
|
||
class NoiseGenerator: | ||
def __init__(self, noise_type='perlin', scale=10.0, octaves=4, persistence=0.5, lacunarity=2.0): | ||
self.noise_type = noise_type | ||
self.scale = scale | ||
self.octaves = octaves | ||
self.persistence = persistence | ||
self.lacunarity = lacunarity | ||
|
||
def generate_noise(self, x, y, z): | ||
if self.noise_type == 'perlin': | ||
return pnoise3(x / self.scale, y / self.scale, z / self.scale, octaves=self.octaves, persistence=self.persistence, lacunarity=self.lacunarity) | ||
elif self.noise_type == 'simplex': | ||
return snoise3(x / self.scale, y / self.scale, z / self.scale, octaves=self.octaves, persistence=self.persistence, lacunarity=self.lacunarity) | ||
else: | ||
raise ValueError("Unsupported noise type") | ||
|
||
class VoxelWorld: | ||
themes = { | ||
'Moon': {'voxel_color': (150, 100, 150), 'light_intensity': 0.7, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Gray': {'voxel_color': (130, 130, 130), 'light_intensity': 0.8, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Rose': {'voxel_color': (180, 100, 100), 'light_intensity': 0.6, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Lilac': {'voxel_color': (160, 160, 200), 'light_intensity': 0.9, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Snow': {'voxel_color': (200, 200, 250), 'light_intensity': 0.5, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Mint': {'voxel_color': (180, 255, 200), 'light_intensity': 0.7, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Peach': {'voxel_color': (255, 204, 170), 'light_intensity': 0.8, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Sky': {'voxel_color': (135, 206, 235), 'light_intensity': 0.9, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Lavender': {'voxel_color': (230, 230, 250), 'light_intensity': 0.6, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Lemon': {'voxel_color': (255, 255, 204), 'light_intensity': 0.5, 'fog_intensity': 0.1, 'light_source_position': (64, 64, 128)}, | ||
'Ice': {'voxel_color': (200, 255, 255), 'light_intensity': 0.9, 'fog_intensity': 0.05, 'light_source_position': (64, 64, 128)}, | ||
'Obsidian': {'voxel_color': (50, 50, 50), 'light_intensity': 0.9, 'fog_intensity': 0.05, 'light_source_position': (64, 64, 128)}, | ||
'Mercury': {'voxel_color': (230, 230, 230), 'light_intensity': 0.9, 'fog_intensity': 0.05, 'light_source_position': (64, 64, 128)}, | ||
} | ||
|
||
def __init__(self, voxel_matrix, theme_name='Lilac', resolution=2, zoom=1.0, show_light_source=False, time_render=False, dark_bg=False, color_matrix=None, transparency_matrix=None, specularity_matrix=None): | ||
self.size = voxel_matrix.shape[0] | ||
self.world = voxel_matrix | ||
|
||
theme = VoxelWorld.themes[theme_name] | ||
self.voxel_color = theme['voxel_color'] | ||
self.light_intensity = theme['light_intensity'] | ||
self.fog_intensity = theme['fog_intensity'] | ||
self.light_source_position = theme['light_source_position'] | ||
|
||
self.resolution = resolution | ||
self.zoom = zoom | ||
self.show_light_source = show_light_source | ||
self.time_render = time_render | ||
self.dark_bg = dark_bg | ||
self.ao_matrix = precompute_ambient_occlusion(voxel_matrix, self.size) | ||
self.color_matrix = color_matrix if color_matrix is not None else np.zeros((self.size, self.size, self.size, 3), dtype=np.uint8) | ||
self.transparency_matrix = transparency_matrix if transparency_matrix is not None else np.ones((self.size, self.size, self.size), dtype=np.float32) | ||
self.specularity_matrix = specularity_matrix if specularity_matrix is not None else np.zeros((self.size, self.size, self.size), dtype=np.float32) | ||
|
||
def update(self, voxel_matrix=None, color_matrix=None, transparency_matrix=None, specularity_matrix=None): | ||
if voxel_matrix is not None: | ||
self.world = voxel_matrix | ||
self.ao_matrix = precompute_ambient_occlusion(voxel_matrix, self.size) | ||
if color_matrix is not None: | ||
self.color_matrix = color_matrix | ||
if transparency_matrix is not None: | ||
self.transparency_matrix = transparency_matrix | ||
if specularity_matrix is not None: | ||
self.specularity_matrix = specularity_matrix | ||
|
||
def render(self, viewing_angle=(45, 30)): | ||
if self.time_render: | ||
start_time = time.time() | ||
|
||
angle_x, angle_y = viewing_angle | ||
angle_x_rad = np.radians(angle_x) | ||
angle_y_rad = np.radians(angle_y) | ||
|
||
img_size = int(self.size * self.resolution * self.zoom * 2) | ||
margin = int(self.size * self.resolution * self.zoom) | ||
bg_color = (50, 50, 50, 255) if self.dark_bg else (220, 220, 220, 255) | ||
image = Image.new('RGBA', (img_size + margin, img_size + margin), bg_color) | ||
draw = ImageDraw.Draw(image) | ||
|
||
voxel_center_x = self.size // 2 | ||
voxel_center_y = self.size // 2 | ||
voxel_center_z = self.size // 2 | ||
center_x = (voxel_center_x - voxel_center_y) * np.cos(angle_x_rad) * self.resolution * self.zoom | ||
center_y = (voxel_center_x + voxel_center_y) * np.sin(angle_x_rad) * self.resolution * self.zoom - voxel_center_z * np.tan(angle_y_rad) * self.resolution * self.zoom | ||
offset_x = (img_size + margin) // 2 - int(center_x) | ||
offset_y = (img_size + margin) // 2 - int(center_y) | ||
|
||
for x in range(self.size): | ||
for y in range(self.size): | ||
self.render_col(draw, x, y, angle_x_rad, angle_y_rad, offset_x, offset_y) | ||
|
||
if self.fog_intensity > 0: | ||
image = self.apply_fog(image) | ||
|
||
if self.show_light_source: | ||
image = self.add_light_source_sphere(image, offset_x, offset_y) | ||
|
||
bbox = image.getbbox() | ||
if bbox: | ||
image = image.crop(bbox) | ||
|
||
if self.time_render: | ||
elapsed_time = time.time() - start_time | ||
print(f"Rendering time: {elapsed_time:.2f} seconds") | ||
|
||
return image | ||
|
||
def render_col(self, draw, x, y, angle_x_rad, angle_y_rad, offset_x, offset_y): | ||
for z in range(self.size): | ||
if self.world[x, y, z] > 0: | ||
ao = self.ao_matrix[x, y, z] | ||
brightness = int((1.0 - ao) * 255 * self.light_intensity) | ||
base_color = tuple(min(255, int(c * brightness / 255)) for c in self.voxel_color) | ||
voxel_color = tuple(self.color_matrix[x, y, z]) if np.any(self.color_matrix[x, y, z]) else base_color | ||
transparency = self.transparency_matrix[x, y, z] | ||
specularity = self.specularity_matrix[x, y, z] | ||
self.draw_voxel(draw, x, y, z, voxel_color, transparency, specularity, angle_x_rad, angle_y_rad, offset_x, offset_y) | ||
|
||
def draw_voxel(self, draw, x, y, z, color, transparency, specularity, angle_x_rad, angle_y_rad, offset_x, offset_y): | ||
ox = int((x - y) * np.cos(angle_x_rad) * self.resolution * self.zoom + offset_x) | ||
oy = int((x + y) * np.sin(angle_x_rad) * self.resolution * self.zoom - z * np.tan(angle_y_rad) * self.resolution * self.zoom + offset_y) | ||
|
||
size = int(self.resolution * self.zoom) | ||
color_with_transparency = tuple(int(c * transparency) for c in color) + (int(255 * transparency),) | ||
|
||
draw.polygon([ | ||
(ox, oy), | ||
(ox + size, oy + size // 2), | ||
(ox, oy + size), | ||
(ox - size, oy + size // 2), | ||
], fill=color_with_transparency) | ||
|
||
draw.polygon([ | ||
(ox, oy + size), | ||
(ox - size, oy + size // 2), | ||
(ox - size, oy + size + size // 2), | ||
(ox, oy + size + size // 2), | ||
], fill=tuple(int(c // 2 * transparency) for c in color) + (int(255 * transparency),)) | ||
|
||
draw.polygon([ | ||
(ox, oy + size), | ||
(ox + size, oy + size // 2), | ||
(ox + size, oy + size + size // 2), | ||
(ox, oy + size + size // 2), | ||
], fill=tuple(int(c // 3 * transparency) for c in color) + (int(255 * transparency),)) | ||
|
||
def apply_fog(self, image): | ||
fog_overlay = Image.new('RGBA', image.size, (220, 220, 220, int(255 * self.fog_intensity * 0.5))) | ||
return Image.alpha_composite(image, fog_overlay) | ||
|
||
def add_light_source_sphere(self, image, offset_x, offset_y): | ||
sphere_radius = 8 | ||
light_color = (255, 165, 0, 255) # Bright arid orange | ||
light_position_x = int(self.light_source_position[0]) | ||
light_position_y = int(self.light_source_position[1]) | ||
draw = ImageDraw.Draw(image) | ||
draw.ellipse([light_position_x + offset_x - sphere_radius, light_position_y + offset_y - sphere_radius, light_position_x + offset_x + sphere_radius, light_position_y + offset_y + sphere_radius], fill=light_color) | ||
return image | ||
|
||
@staticmethod | ||
def show_themes(): | ||
size = 8 | ||
|
||
def generate_perlin_voxel_world(size): | ||
voxel_matrix = np.zeros((size, size, size), dtype=np.uint8) | ||
noise_gen = NoiseGenerator(scale=5.0) | ||
for x in range(size): | ||
for y in range(size): | ||
for z in range(size): | ||
if noise_gen.generate_noise(x, y, z) > 0: | ||
voxel_matrix[x, y, z] = 1 | ||
return voxel_matrix | ||
|
||
viewing_angle = (45, 30) | ||
images = [] | ||
|
||
for theme_name in VoxelWorld.themes.keys(): | ||
voxel_matrix = generate_perlin_voxel_world(size) | ||
world = VoxelWorld(voxel_matrix, theme_name, resolution=10, zoom=1.5, dark_bg=False) | ||
image = world.render(viewing_angle) | ||
images.append(image) | ||
|
||
# Create a 4x4 grid of subplots | ||
fig, axs = plt.subplots(4, 4, figsize=(15, 15)) | ||
|
||
# Flatten the 2D array of subplots into a 1D array | ||
axs = axs.flatten() | ||
|
||
for ax, img, theme_name in zip(axs, images, VoxelWorld.themes.keys()): | ||
ax.imshow(img) | ||
ax.axis('off') | ||
ax.set_title(theme_name) | ||
|
||
plt.show() | ||
|
||
plt.show() | ||
|
||
class Animations: | ||
@staticmethod | ||
def create_voxel_img(voxel_matrix, theme_name, resolution=2, viewing_angle=(45, 30), zoom=1.0, show_light_source=False, time_render=False, dark_bg=False, color_matrix=None, transparency_matrix=None, specularity_matrix=None): | ||
world = VoxelWorld(voxel_matrix, theme_name, resolution, zoom, show_light_source, time_render, dark_bg, color_matrix, transparency_matrix, specularity_matrix) | ||
image = world.render(viewing_angle) | ||
image = image.convert('RGB') | ||
|
||
byte_stream = io.BytesIO() | ||
image.save(byte_stream, format='PNG') | ||
byte_stream.seek(0) | ||
return byte_stream | ||
|
||
@staticmethod | ||
def gen_gif(stack, theme_name, resolution=2, viewing_angle=(45, 30), zoom=1.0, show_light_source=False, dark_bg=False, color_matrix_stack=None, transparency_matrix_stack=None, specularity_matrix_stack=None): | ||
images = [] | ||
for i, voxel_matrix in enumerate(stack): | ||
color_matrix = color_matrix_stack[i] if color_matrix_stack is not None else None | ||
transparency_matrix = transparency_matrix_stack[i] if transparency_matrix_stack is not None else None | ||
specularity_matrix = specularity_matrix_stack[i] if specularity_matrix_stack is not None else None | ||
byte_stream = VoxelWorld.Animations.create_voxel_img(voxel_matrix, theme_name, resolution, viewing_angle, zoom, show_plane, show_light_source, False, dark_bg, color_matrix, transparency_matrix, specularity_matrix) | ||
image = Image.open(byte_stream) | ||
images.append(image) | ||
|
||
gif_byte_stream = io.BytesIO() | ||
images[0].save(gif_byte_stream, format='GIF', save_all=True, append_images=images[1:], loop=0, duration=100) | ||
gif_byte_stream.seek(0) | ||
return gif_byte_stream | ||
|
||
@staticmethod | ||
def rotate_voxel_matrix(voxel_matrix, angle): | ||
size = voxel_matrix.shape[0] | ||
rotated_matrix = np.zeros_like(voxel_matrix) | ||
angle_rad = np.radians(angle) | ||
cos_angle = np.cos(angle_rad) | ||
sin_angle = np.sin(angle_rad) | ||
|
||
for x in range(size): | ||
for y in range(size): | ||
for z in range(size): | ||
if (voxel_matrix[x, y, z] > 0): | ||
new_x = int(x * cos_angle - z * sin_angle) | ||
new_z = int(x * sin_angle + z * cos_angle) | ||
if 0 <= new_x < size and 0 <= new_z < size: | ||
rotated_matrix[new_x, y, new_z] = 1 | ||
return rotated_matrix | ||
|
||
def display_gif(gif_byte_stream): | ||
display(IPImage(data=gif_byte_stream.getvalue())) |