461 lines
15 KiB
Python
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)
|