Загрузка данных


"""
Детализированный вентилятор видеокарты (в стиле ASUS ROG Astral) для Blender.
Запуск: Scripting -> Open -> Run Script. Работает в Blender 3.6 - 4.x.
Единицы: радиус ротора = 1.0 (при реальных 95 мм масштабируйте на 0.0475).
"""
import bpy
import bmesh
from math import pi, sin, cos, atan2, radians

# ======================= ПАРАМЕТРЫ =======================
BLADES = 11            # число лопастей
R_IN = 0.27            # радиус лопасти у ступицы
R_OUT = 0.97           # радиус лопасти у обода
SWEEP = 42             # загиб лопасти назад (град)
THICK = 0.010          # толщина лопасти
SPIN_PER_FRAME = 3     # градусов за кадр (анимация вращения)
RENDER_SAMPLES = 128
RES = 1600

# ======================= ОЧИСТКА =======================
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.curves):
    for item in list(blk):
        blk.remove(item)

scn = bpy.context.scene
col = scn.collection


def link(obj):
    col.objects.link(obj)
    return obj


def setp(node, names, val):
    """Безопасная установка входа Principled (имена различаются по версиям)."""
    for n in names:
        if n in node.inputs:
            node.inputs[n].default_value = val
            return


# ======================= МАТЕРИАЛЫ =======================
def new_mat(name):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    return m, m.node_tree.nodes, m.node_tree.links


def add_bump(nodes, links, bsdf, tex, strength, dist=0.01):
    bump = nodes.new("ShaderNodeBump")
    bump.inputs["Strength"].default_value = strength
    bump.inputs["Distance"].default_value = dist
    links.new(tex.outputs[0], bump.inputs["Height"])
    links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])


def mat_plastic():
    m, n, l = new_mat("BladePlastic")
    b = n["Principled BSDF"]
    b.inputs["Base Color"].default_value = (0.012, 0.012, 0.014, 1)
    b.inputs["Roughness"].default_value = 0.42
    setp(b, ["Coat Weight", "Clearcoat"], 0.25)
    setp(b, ["Coat Roughness", "Clearcoat Roughness"], 0.2)
    coord = n.new("ShaderNodeTexCoord")
    noise = n.new("ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = 450
    noise.inputs["Detail"].default_value = 6
    l.new(coord.outputs["Object"], noise.inputs["Vector"])
    add_bump(n, l, b, noise, 0.06)
    return m


def mat_brushed(name, color, rough=0.26, scale=220):
    m, n, l = new_mat(name)
    b = n["Principled BSDF"]
    b.inputs["Base Color"].default_value = color
    b.inputs["Metallic"].default_value = 1.0
    b.inputs["Roughness"].default_value = rough
    coord = n.new("ShaderNodeTexCoord")
    wave = n.new("ShaderNodeTexWave")
    wave.wave_type = 'RINGS'
    wave.rings_direction = 'Z'
    wave.inputs["Scale"].default_value = scale
    wave.inputs["Distortion"].default_value = 2.5
    wave.inputs["Detail"].default_value = 4
    l.new(coord.outputs["Object"], wave.inputs["Vector"])
    add_bump(n, l, b, wave, 0.12)
    return m


def mat_metal(name, color, rough, metallic=1.0):
    m, n, l = new_mat(name)
    b = n["Principled BSDF"]
    b.inputs["Base Color"].default_value = color
    b.inputs["Metallic"].default_value = metallic
    b.inputs["Roughness"].default_value = rough
    return m


def mat_rgb():
    """Подсветка: градиент фиолетовый -> синий по углу."""
    m, n, l = new_mat("RGBGlow")
    for nd in list(n):
        n.remove(nd)
    out = n.new("ShaderNodeOutputMaterial")
    emi = n.new("ShaderNodeEmission")
    emi.inputs["Strength"].default_value = 9.0
    coord = n.new("ShaderNodeTexCoord")
    sep = n.new("ShaderNodeSeparateXYZ")
    at = n.new("ShaderNodeMath")
    at.operation = 'ARCTAN2'
    ab = n.new("ShaderNodeMath")
    ab.operation = 'ABSOLUTE'
    dv = n.new("ShaderNodeMath")
    dv.operation = 'DIVIDE'
    dv.inputs[1].default_value = pi
    ramp = n.new("ShaderNodeValToRGB")
    ramp.color_ramp.elements[0].color = (0.55, 0.08, 1.0, 1)
    ramp.color_ramp.elements[1].color = (0.05, 0.25, 1.0, 1)
    l.new(coord.outputs["Object"], sep.inputs[0])
    l.new(sep.outputs["Y"], at.inputs[0])
    l.new(sep.outputs["X"], at.inputs[1])
    l.new(at.outputs[0], ab.inputs[0])
    l.new(ab.outputs[0], dv.inputs[0])
    l.new(dv.outputs[0], ramp.inputs["Fac"])
    l.new(ramp.outputs["Color"], emi.inputs["Color"])
    l.new(emi.outputs[0], out.inputs["Surface"])
    return m


M_PLASTIC = mat_plastic()
M_SILVER = mat_brushed("BrushedSilver", (0.80, 0.82, 0.86, 1))
M_HUB = mat_brushed("HubDark", (0.035, 0.035, 0.04, 1), rough=0.33, scale=300)
M_FRAME = mat_metal("FrameDark", (0.025, 0.025, 0.03, 1), 0.5, 0.35)
M_FIN = mat_metal("Aluminium", (0.7, 0.7, 0.72, 1), 0.4)
M_COPPER = mat_metal("Copper", (0.92, 0.45, 0.26, 1), 0.25)
M_RGB = mat_rgb()


# ======================= ХЕЛПЕРЫ ГЕОМЕТРИИ =======================
def revolve(name, profile, mat, closed=False, steps=128, loc=(0, 0, 0)):
    """Тело вращения вокруг оси Z по профилю [(r, z), ...]."""
    bm = bmesh.new()
    vs = [bm.verts.new((r, 0, z)) for r, z in profile]
    es = [bm.edges.new((vs[i], vs[i + 1])) for i in range(len(vs) - 1)]
    if closed:
        es.append(bm.edges.new((vs[-1], vs[0])))
    bmesh.ops.spin(bm, geom=vs + es, cent=(0, 0, 0), axis=(0, 0, 1),
                   angle=2 * pi, steps=steps, use_merge=True)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = link(bpy.data.objects.new(name, me))
    ob.location = loc
    ob.data.materials.append(mat)
    for p in me.polygons:
        p.use_smooth = True
    return ob


def circle_pts(cx, cz, rx, rz, n=10, a0=0):
    return [(cx + rx * cos(a0 + 2 * pi * i / n), cz + rz * sin(a0 + 2 * pi * i / n))
            for i in range(n)]


def curve_shape(name, pts, mat, extrude=0.004, loc=(0, 0, 0)):
    cu = bpy.data.curves.new(name, 'CURVE')
    cu.dimensions = '2D'
    cu.extrude = extrude
    cu.fill_mode = 'BOTH'
    sp = cu.splines.new('POLY')
    sp.points.add(len(pts) - 1)
    for p, (x, y) in zip(sp.points, pts):
        p.co = (x, y, 0, 1)
    sp.use_cyclic_u = True
    ob = link(bpy.data.objects.new(name, cu))
    ob.location = loc
    ob.data.materials.append(mat)
    return ob


# ======================= ЛОПАСТЬ =======================
def make_blade():
    NR, NC = 24, 10
    bm = bmesh.new()
    grid = []
    for i in range(NR + 1):
        t = i / NR
        r = R_IN + t * (R_OUT - R_IN)
        width = radians(46) * (0.42 + 0.58 * t ** 0.7)
        sweep = radians(SWEEP) * t ** 1.5 + radians(7) * sin(pi * t)
        row = []
        for j in range(NC + 1):
            c = j / NC
            theta = sweep + (c - 0.5) * width
            # наклон (pitch) растёт к краю + изгиб профиля (camber)
            z = (c - 0.5) * (0.07 + 0.09 * t) \
                + 0.030 * sin(pi * c) * (1 - 0.3 * t) \
                - 0.010 * t
            row.append(bm.verts.new((r * cos(theta), r * sin(theta), z)))
        grid.append(row)
    for i in range(NR):
        for j in range(NC):
            bm.faces.new((grid[i][j], grid[i][j + 1],
                          grid[i + 1][j + 1], grid[i + 1][j]))
    me = bpy.data.meshes.new("BladeMesh")
    bm.to_mesh(me)
    bm.free()
    ob = link(bpy.data.objects.new("Blade", me))
    ob.data.materials.append(M_PLASTIC)
    for p in me.polygons:
        p.use_smooth = True
    sol = ob.modifiers.new("Solidify", 'SOLIDIFY')
    sol.thickness = THICK
    sol.offset = 0
    bev = ob.modifiers.new("Bevel", 'BEVEL')
    bev.width = 0.0025
    bev.segments = 2
    bev.limit_method = 'ANGLE'
    sub = ob.modifiers.new("Subsurf", 'SUBSURF')
    sub.levels = 1
    sub.render_levels = 2
    return ob


base = make_blade()
blades = [base]
for k in range(1, BLADES):
    d = base.copy()                      # общая геометрия, свои модификаторы
    d.rotation_euler[2] = k * 2 * pi / BLADES
    link(d)
    blades.append(d)

# ======================= ВРАЩАЮЩИЙСЯ ОБОД =======================
rim_profile = [(0.93, 0.040), (0.97, 0.058), (1.02, 0.060), (1.055, 0.040),
               (1.065, 0.000), (1.055, -0.040), (1.02, -0.060),
               (0.97, -0.058), (0.93, -0.040), (0.92, 0.000)]
rim = revolve("OuterRim", rim_profile, M_SILVER, closed=True, steps=192)

# внутренняя тёмная кромка, к которой "приклеены" кончики лопастей
lip = revolve("RimLip", circle_pts(0.945, 0.0, 0.018, 0.03, n=12), M_PLASTIC,
              closed=True, steps=160)

# ======================= СТУПИЦА =======================
hub_profile = [(0.0, 0.075), (0.17, 0.075), (0.21, 0.068), (0.255, 0.048),
               (0.285, 0.010), (0.285, -0.020), (0.27, -0.035), (0.0, -0.035)]
hub = revolve("Hub", hub_profile, M_HUB)

hub_ring = revolve("HubRing", circle_pts(0.222, 0.071, 0.017, 0.012, n=10),
                   M_SILVER, closed=True, steps=128)

# внутренний кольцевой паз вокруг логотипа
groove = revolve("HubGroove", circle_pts(0.19, 0.0765, 0.005, 0.004, n=8),
                 M_SILVER, closed=True, steps=96)

# ======================= ЛОГОТИП ROG (стилизованный глаз) =======================
LZ = 0.080


def crescent(sign):
    n = 24
    outer = []
    inner = []
    for i in range(n + 1):
        x = -0.17 + 0.34 * i / n
        outer.append((x, sign * 0.095 * (1 - (x / 0.17) ** 2) ** 0.9))
    for i in range(n + 1):
        x = -0.12 + 0.24 * i / n
        inner.append((x, sign * 0.050 * (1 - (x / 0.12) ** 2) ** 0.9))
    return outer + inner[::-1]


logo_top = curve_shape("LogoTop", crescent(+1), M_SILVER, loc=(0, 0.004, LZ))
logo_bot = curve_shape("LogoBottom", crescent(-1), M_SILVER, loc=(0, -0.004, LZ))
iris = revolve("LogoIris", [(0.0, 0.0), (0.028, 0.0), (0.030, 0.004),
                            (0.026, 0.008), (0.0, 0.008)], M_RGB,
               loc=(0, 0, LZ - 0.003), steps=48)

# ======================= ВРАЩАЮЩАЯСЯ ГРУППА =======================
rotor = link(bpy.data.objects.new("Rotor", None))
for o in blades + [rim, lip, hub, hub_ring, groove, logo_top, logo_bot, iris]:
    o.parent = rotor

drv = rotor.driver_add("rotation_euler", 2).driver
drv.type = 'SCRIPTED'
drv.expression = "-frame*%.5f" % radians(SPIN_PER_FRAME)

# ======================= НЕПОДВИЖНАЯ РАМКА (шрауд) =======================
def rounded_plate(half, rad, thick, n=14):
    pts = []
    centers = [(1, 1, 0), (-1, 1, 90), (-1, -1, 180), (1, -1, 270)]
    for sx, sy, a0 in centers:
        for i in range(n + 1):
            a = radians(a0 + 90 * i / n)
            pts.append((sx * (half - rad) + rad * cos(a),
                        sy * (half - rad) + rad * sin(a)))
    bm = bmesh.new()
    vs = [bm.verts.new((x, y, 0)) for x, y in pts]
    face = bm.faces.new(vs)
    res = bmesh.ops.extrude_face_region(bm, geom=[face])
    nv = [e for e in res['geom'] if isinstance(e, bmesh.types.BMVert)]
    bmesh.ops.translate(bm, verts=nv, vec=(0, 0, thick))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    me = bpy.data.meshes.new("FramePlate")
    bm.to_mesh(me)
    bm.free()
    return me


frame = link(bpy.data.objects.new("Frame", rounded_plate(1.20, 0.26, 0.14)))
frame.location = (0, 0, -0.09)
frame.data.materials.append(M_FRAME)
for p in frame.data.polygons:
    p.use_smooth = False

bpy.ops.mesh.primitive_cylinder_add(radius=1.10, depth=0.6, vertices=192)
cutter = bpy.context.active_object
cutter.name = "FrameCutter"
cutter.display_type = 'WIRE'
cutter.hide_render = True

bo = frame.modifiers.new("Hole", 'BOOLEAN')
bo.operation = 'DIFFERENCE'
bo.object = cutter
bo.solver = 'EXACT'
fb = frame.modifiers.new("Bevel", 'BEVEL')
fb.width = 0.010
fb.segments = 3
fb.limit_method = 'ANGLE'

# RGB-кольцо на рамке
glow = revolve("FrameGlow", [(1.105, 0.046), (1.14, 0.046), (1.14, 0.058),
                             (1.105, 0.058)], M_RGB, closed=True, steps=192)

# угловые винты
for sx in (-1, 1):
    for sy in (-1, 1):
        bpy.ops.mesh.primitive_cylinder_add(radius=0.035, depth=0.012,
                                            vertices=6,
                                            location=(sx * 1.07, sy * 1.07, 0.054))
        s = bpy.context.active_object
        s.name = "Screw"
        s.data.materials.append(M_SILVER)

# ======================= ЗАДНЯЯ ЧАСТЬ: мотор, радиатор, трубки =======================
motor = revolve("Motor", [(0.0, -0.035), (0.26, -0.035), (0.26, -0.20),
                          (0.0, -0.20)], M_FRAME, steps=64)

fin = bpy.data.objects.new("Fin", bpy.data.meshes.new("FinMesh"))
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bm.to_mesh(fin.data)
bm.free()
link(fin)
fin.scale = (0.010, 1.9, 0.45)
fin.location = (-1.1, 0, -0.45)
fin.data.materials.append(M_FIN)
arr = fin.modifiers.new("Array", 'ARRAY')
arr.count = 70
arr.use_relative_offset = False
arr.use_constant_offset = True
arr.constant_offset_displace = (0.032, 0, 0)

for i, y in enumerate((-0.64, -0.32, 0.0, 0.32, 0.64)):
    bpy.ops.mesh.primitive_cylinder_add(radius=0.045, depth=2.5, vertices=32,
                                        location=(0, y, -0.45),
                                        rotation=(0, pi / 2, 0))
    pipe = bpy.context.active_object
    pipe.name = "HeatPipe%d" % i
    pipe.data.materials.append(M_COPPER)
    bpy.ops.object.shade_smooth()

# ======================= СЦЕНА: свет, камера, рендер =======================
world = scn.world or bpy.data.worlds.new("World")
scn.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs["Color"].default_value = (1, 1, 1, 1)
bg.inputs["Strength"].default_value = 0.9


def area(name, loc, energy, size):
    ld = bpy.data.lights.new(name, 'AREA')
    ld.energy = energy
    ld.size = size
    ob = link(bpy.data.objects.new(name, ld))
    ob.location = loc
    return ob


area("Key", (0, 0, 5), 900, 5)
area("RimL", (-4, -2, 2.5), 400, 3)
area("RimR", (4, 2, 2.5), 400, 3)

cam_d = bpy.data.cameras.new("Cam")
cam_d.type = 'ORTHO'
cam_d.ortho_scale = 2.7
cam = link(bpy.data.objects.new("Cam", cam_d))
cam.location = (0, 0, 6)
scn.camera = cam
# Для перспективного ракурса:
# cam_d.type='PERSP'; cam.location=(2.2,-3.2,2.4); cam.rotation_euler=(radians(62),0,radians(35))

scn.render.engine = 'CYCLES'
scn.cycles.samples = RENDER_SAMPLES
scn.cycles.use_denoising = True
scn.render.resolution_x = scn.render.resolution_y = RES
scn.view_settings.view_transform = 'Standard'
scn.frame_start, scn.frame_end = 1, 120

print("Готово: детализированный вентилятор создан.")