""" 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)