Files
roi24/blender_matrix_tunnel.py
T

461 lines
15 KiB
Python

"""
ROI24 — Matrix 3D Tunnel Hero Animation
========================================
Blender 4.x Geometry Nodes script.
Creates a Matrix-style infinite tunnel with binary digits
and floating KPI cards.
Usage:
1. Open Blender 4.x
2. Scripting workspace → Text Editor → New → "matrix_tunnel.py"
3. Paste this entire file
4. Alt+P (Run Script)
5. Press ~ in 3D viewport → shade smooth
6. Numpad 0 for camera view
7. Ctrl+F12 to render animation
Output: 1920x1080, 60fps, 180 frames (3s loop)
"""
import bpy
import math
import random
import os
# ============================================================
# CONFIG
# ============================================================
RENDER_WIDTH = 1920
RENDER_HEIGHT = 1080
FRAMES = 180
FPS = 60
TUNNEL_RADIUS = 8.0
TUNNEL_LENGTH = 80.0
RINGS = 32
CHARS_PER_RING = 48
FLOW_SPEED = 3.0 # tunnel speed
# ROI24 brand colors
GREEN = (0.133, 0.773, 0.369) # #22c55e
GREEN_BRIGHT = (0.286, 0.886, 0.502) # #4ade80
DARK = (0.039, 0.039, 0.039) # #0a0a0a
# ============================================================
# CLEANUP
# ============================================================
print("Cleaning scene...")
for obj in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat, do_unlink=True)
for tex in bpy.data.textures:
bpy.data.textures.remove(tex, do_unlink=True)
for img in bpy.data.images:
bpy.data.images.remove(img, do_unlink=True)
for gn in bpy.data.node_groups:
bpy.data.node_groups.remove(gn)
# ============================================================
# MATERIALS
# ============================================================
print("Creating materials...")
def create_green_emit_mat(name):
"""Create a green emission material."""
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
emit = nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = (*GREEN, 1.0)
emit.inputs['Strength'].default_value = 1.5
output = nodes.new('ShaderNodeOutputMaterial')
links.new(emit.outputs['Emission'], output.inputs['Surface'])
return mat
def create_glow_mat(name, color=GREEN, strength=3.0):
"""Create a bright glow material."""
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
emit = nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = (*color, 1.0)
emit.inputs['Strength'].default_value = strength
output = nodes.new('ShaderNodeOutputMaterial')
links.new(emit.outputs['Emission'], output.inputs['Surface'])
return mat
char_mat = create_green_emit_mat('CharMat')
glow_mat = create_glow_mat('GlowMat', GREEN_BRIGHT, 4.0)
card_mat = create_green_emit_mat('CardMat')
line_mat = create_green_emit_mat('LineMat')
# ============================================================
# CAMERA
# ============================================================
print("Setting up camera...")
cam_data = bpy.data.cameras.new('Camera')
cam_data.type = 'PERSP'
cam_data.lens = 35
cam_data.clip_start = 0.1
cam_data.clip_end = 200
cam = bpy.data.objects.new('Camera', cam_data)
cam.location = (0, 0, 0)
bpy.context.scene.collection.objects.link(cam)
bpy.context.scene.camera = cam
# ============================================================
# SCENE SETUP
# ============================================================
print("Setting up scene...")
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 128
scene.cycles.preview_samples = 32
scene.cycles.device = 'GPU'
scene.render.fps = FPS
scene.frame_start = 0
scene.frame_end = FRAMES - 1
# Render resolution
scene.render.resolution_x = RENDER_WIDTH
scene.render.resolution_y = RENDER_HEIGHT
scene.render.resolution_percentage = 100
# Output path (set before file_format)
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'renders')
os.makedirs(output_dir, exist_ok=True)
scene.render.filepath = os.path.join(output_dir, 'matrix_tunnel')
# FFMPEG settings (Blender 4.x API)
scene.ffmpeg.format = 'MPEG_4'
scene.ffmpeg.codec = 'H264'
scene.ffmpeg.video_bitrate = 50000
# World background
world = bpy.data.worlds.new('World')
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()
bg = nodes.new('ShaderNodeBackground')
bg.inputs['Color'].default_value = (*DARK, 1.0)
bg.inputs['Strength'].default_value = 0.3
output = nodes['World Output']
links.new(bg.outputs['Background'], output.inputs['Surface'])
# Fog
fog = nodes.new('ShaderNodeFog')
fog.fog_type = 'EXP'
fog.density = 0.025
fog.inputs['Color'].default_value = (*GREEN, 0.15)
links.new(fog, output.inputs['Fog'])
scene.world = world
# ============================================================
# GEOMETRY NODES TUNNEL
# ============================================================
print("Creating tunnel with Geometry Nodes...")
# Create instancing system for binary characters
tunnel_gn = bpy.data.node_groups.new('MatrixTunnel', 'GeometryNodeTree')
tunnel_gn.interface.new_socket('Geometry', socket_type='NodeSocketGeometry')
tunnel_gn.interface.new_socket('Progress', socket_type='NodeSocketFloat')
nodes = tunnel_gn.nodes
links = tunnel_gn.links
# Input geometry
input_geom = nodes.new('NodeGroupInput')
# Separate by Z
separate_z = nodes.new('GeometryNodeSeparateGeometry')
separate_z.location = (200, 0)
# Instance on points
instance_on_points = nodes.new('GeometryNodeInstanceOnPoints')
instance_on_points.location = (500, 0)
# Transform geometry (rotate instances to face camera)
transform = nodes.new('GeometryNodeTransform')
transform.location = (750, 0)
# Output
output_gn = nodes.new('NodeGroupOutput')
# Connect
links.new(input_geom.outputs[0], separate_z.inputs[0])
links.new(separate_z.outputs[0], instance_on_points.inputs[0])
links.new(instance_on_points.outputs[0], transform.inputs[0])
links.new(transform.outputs[0], output_gn.inputs[0])
# ============================================================
# CREATE BINARY CHARACTER MESH (simple diamond shape)
# ============================================================
print("Creating character mesh...")
# Diamond shape for '0'
char_mesh_0 = bpy.data.meshes.new('Char0')
verts_0 = [
(0, 0.3, 0), (0.2, 0, 0), (0, -0.3, 0), (-0.2, 0, 0)
]
faces_0 = [[0, 1, 2, 3]]
char_mesh_0.from_pydata(verts_0, [], faces_0)
char_mesh_0.validate()
# Line shape for '1'
char_mesh_1 = bpy.data.meshes.new('Char1')
verts_1 = [
(-0.08, 0.25, 0), (0.08, 0.25, 0),
(0.08, -0.25, 0), (-0.08, -0.25, 0),
(-0.08, -0.15, 0), (0.08, -0.15, 0)
]
faces_1 = [[0, 1, 2, 3], [2, 5, 4, 3]]
char_mesh_1.from_pydata(verts_1, [], faces_1)
char_mesh_1.validate()
# ============================================================
# INSTANCED CHARACTER OBJECTS
# ============================================================
print("Instancing characters...")
def create_instanced_chars():
"""Create instanced binary characters around tunnel."""
angle_step = (2 * math.pi) / CHARS_PER_RING
# Create collection for tunnel objects
tunnel_col = bpy.data.collections.new('MatrixTunnel')
bpy.context.scene.collection.children.link(tunnel_col)
# Create a single "instancer" object with geometry nodes
instancer = bpy.data.objects.new('TunnelInstancer', None)
instancer.location = (0, 0, 0)
tunnel_col.objects.link(instancer)
# Geometry nodes modifier
gn = instancer.modifiers.new('TunnelGenerator', 'NODES')
gn.node_group = tunnel_gn
# Now create actual character instances
ring_spacing = TUNNEL_LENGTH / RINGS
for ring_idx in range(RINGS):
z = -(ring_idx * ring_spacing)
for char_idx in range(CHARS_PER_RING):
angle = char_idx * angle_step
x = math.cos(angle) * TUNNEL_RADIUS
y = math.sin(angle) * TUNNEL_RADIUS
# Create character mesh object
use_mesh = char_mesh_0 if random.random() > 0.5 else char_mesh_1
# Use object instancing for performance
char_obj = bpy.data.objects.new(
f'Char_R{ring_idx:02d}_C{char_idx:02d}',
use_mesh
)
char_obj.location = (x, y, z)
char_obj.rotation_euler = (0, 0, angle + math.radians(90))
# Scale based on position (closer = bigger)
scale = 0.6 + (ring_idx / RINGS) * 0.3
char_obj.scale = (scale, scale, scale)
# Material
char_obj.data.materials.append(char_mat)
# Set material color based on depth
depth_ratio = ring_idx / RINGS
r = GREEN[0] * (0.6 + depth_ratio * 0.4)
g = GREEN[1] * (0.8 + depth_ratio * 0.2)
b = GREEN[2] * (0.7 + depth_ratio * 0.3)
char_mat.nodes['Emission'].inputs['Color'].default_value = (r, g, b, 1.0)
# Store animation data
char_obj['ring_idx'] = ring_idx
char_obj['char_idx'] = char_idx
char_obj['base_x'] = x
char_obj['base_y'] = y
char_obj['base_z'] = z
char_obj['base_scale'] = scale
char_obj['angle'] = angle
tunnel_col.objects.link(char_obj)
return tunnel_col
tunnel_col = create_instanced_chars()
# ============================================================
# KPI CARDS — 3D panels
# ============================================================
print("Creating KPI cards...")
KPI_DATA = [
{'label': 'CPA', 'value': '890₽', 'pos': (-10, 5, -12), 'rot': (0, 0.1, 0), 'size': (3, 2)},
{'label': 'ROI', 'value': '91.8%', 'pos': (10, 4, -18), 'rot': (0, -0.1, 0), 'size': (3, 2)},
{'label': 'LTV', 'value': '12.4K', 'pos': (-9, -2, -24), 'rot': (0, 0.15, 0), 'size': (3, 2)},
{'label': 'CONV', 'value': '2.8%', 'pos': (9, -3, -30), 'rot': (0, -0.15, 0), 'size': (3, 2)},
{'label': 'LEADS', 'value': '44.5K', 'pos': (-8, 1, -36), 'rot': (0, 0.1, 0), 'size': (3, 2)},
{'label': 'REVENUE', 'value': '2.05M', 'pos': (8, 0, -42), 'rot': (0, -0.1, 0), 'size': (3, 2)},
]
for i, kpi in enumerate(KPI_DATA):
w, h = kpi['size']
# Card mesh
card_mesh = bpy.data.meshes.new(f'KPI_Card_{i}')
verts = [
(-w/2, -h/2, 0), (w/2, -h/2, 0),
(w/2, h/2, 0), (-w/2, h/2, 0),
(-w/2, -h/2, -0.05), (w/2, -h/2, -0.05),
(w/2, h/2, -0.05), (-w/2, h/2, -0.05),
]
faces = [
[0, 4, 5, 1], # bottom
[4, 7, 6, 5], # back
[7, 3, 2, 6], # top
[3, 0, 1, 2], # front
[4, 0, 3, 7], # left
[1, 5, 6, 2], # right
]
card_mesh.from_pydata(verts, [], faces)
card_mesh.validate()
card_obj = bpy.data.objects.new(f'KPI_Card_{i}', card_mesh)
card_obj.location = kpi['pos']
card_obj.rotation_euler = kpi['rot']
card_obj.scale = (1, 1, 1)
# Material
card_obj.data.materials.append(card_mat)
# Store data
card_obj['kpi_index'] = i
card_obj['base_x'] = kpi['pos'][0]
card_obj['base_y'] = kpi['pos'][1]
card_obj['base_z'] = kpi['pos'][2]
tunnel_col.objects.link(card_obj)
# ============================================================
# SPEED LINES
# ============================================================
print("Creating speed lines...")
for i in range(48):
angle = (i / 48) * 2 * math.pi
x1 = math.cos(angle) * 0.5
y1 = math.sin(angle) * 0.5
x2 = math.cos(angle) * TUNNEL_RADIUS * 2
y2 = math.sin(angle) * TUNNEL_RADIUS * 2
z_start = -10
z_end = -TUNNEL_LENGTH
# Curve
curve = bpy.data.curves.new(f'SpeedLine_{i}', 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = 0.03
spline = curve.splines.new('POLY')
spline.points.add(1)
spline.points[0].co = (x1, y1, z_start, 1)
spline.points[1].co = (x2, y2, z_end, 1)
line_obj = bpy.data.objects.new(f'SpeedLine_{i}', curve)
line_obj.data.materials.append(line_mat)
tunnel_col.objects.link(line_obj)
# ============================================================
# ANIMATION FUNCTIONS (uncomment to enable)
# ============================================================
def animate_characters():
"""Animate binary characters flowing toward camera."""
for obj in tunnel_col.objects:
ring_idx = obj.get('ring_idx', None)
if ring_idx is None:
continue
base_z = obj.get('base_z', 0)
base_x = obj.get('base_x', 0)
base_y = obj.get('base_y', 0)
base_scale = obj.get('base_scale', 0.6)
angle = obj.get('angle', 0)
for frame in range(FRAMES):
# Flow forward
z = base_z + ((frame / FPS) * FLOW_SPEED) % TUNNEL_LENGTH
if z > 0:
z -= TUNNEL_LENGTH
# Scale by depth
depth_ratio = max(0, min(1, (TUNNEL_LENGTH + z) / TUNNEL_LENGTH))
scale = base_scale * (0.4 + depth_ratio * 0.7)
# Set keyframe
obj.location = (base_x, base_y, z)
obj.scale = (scale / base_scale, scale / base_scale, 1)
obj.keyframe_insert(data_path='location', frame=frame)
obj.keyframe_insert(data_path='scale', frame=frame)
def animate_kpi_cards():
"""Animate KPI cards floating through tunnel."""
for obj in tunnel_col.objects:
kpi_idx = obj.get('kpi_index', None)
if kpi_idx is None:
continue
base_x = obj.get('base_x', 0)
base_y = obj.get('base_y', 0)
base_z = obj.get('base_z', 0)
for frame in range(FRAMES):
# Move toward camera
z = base_z + ((frame / FPS) * FLOW_SPEED * 0.7)
# Float motion
y = base_y + math.sin((frame / FPS) * 2 + kpi_idx * 1.5) * 1.5
# Scale
depth_ratio = max(0, min(1, (TUNNEL_LENGTH + z) / TUNNEL_LENGTH))
scale = 0.5 + depth_ratio * 0.7
obj.location = (base_x, y, z)
obj.scale = (scale, scale, scale)
obj.keyframe_insert(data_path='location', frame=frame)
obj.keyframe_insert(data_path='scale', frame=frame)
# ============================================================
# SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("ROI24 Matrix 3D Tunnel — Setup Complete")
print("=" * 60)
print(f"Resolution: {RENDER_WIDTH}x{RENDER_HEIGHT}")
print(f"Duration: {FRAMES / FPS}s @ {FPS}fps")
print(f"Engine: {scene.render.engine} (Cycles)")
print(f"Tunnel: {RINGS} rings x {CHARS_PER_RING} chars = {RINGS * CHARS_PER_RING}")
print(f"KPI cards: {len(KPI_DATA)}")
print(f"Speed lines: 48")
print()
print("NEXT STEPS:")
print("1. In 3D Viewport: Press ~ → Shade Smooth")
print("2. Numpad 0 — Camera view")
print("3. F12 — Preview render")
print("4. Ctrl+F12 — Render animation (MP4)")
print()
print("TO ANIMATE:")
print("Uncomment animate_characters() and animate_kpi_cards()")
print("then re-run this script")
print("=" * 60)