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


"""
Вентилятор ASUS ROG Astral (RTX 5090): 7 широких серповидных лопастей,
тонкое серебристое кольцо, плоская ступица с хромовым ободком и логотипом ROG.
Blender 3.6 - 4.x. Запуск: Scripting -> Open -> Run Script.
"""
import bpy
import bmesh
from math import pi, sin, cos, radians

# ======================= ПАРАМЕТРЫ =======================
BLADES = 7              # 7 лопастей как на фото
R_IN = 0.26             # радиус корня лопасти (прячется под ступицей)
R_OUT = 0.985           # радиус кончика (уходит под кольцо)
SWEEP = 80              # на сколько градусов лопасть "закручена" от корня к краю
ROOT_W = 38             # ширина лопасти у ступицы (град)
TIP_W = 74              # ширина лопасти у края (град) - >51.4 даёт нахлёст как на фото
THICK = 0.012           # толщина лопасти
MIRROR = False          # True - закрутка в другую сторону
RGB_GLOW = False        # True - добавить RGB-кольцо на рамке (как на 1-м фото)
VIEW = 'TOP'            # 'TOP' - вид сверху, 'ANGLE' - перспектива
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 find_node(nodes, ntype):
    """Поиск ноды по типу (имена нод локализуются в русском Blender)."""
    for nd in nodes:
        if nd.type == ntype:
            return nd
    return None


def setp(node, names, val):
    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_surface(name, color, rough, metallic=0.0, coat=0.0, noise=0.0):
    m, n, l = new_mat(name)
    b = find_node(n, 'BSDF_PRINCIPLED')
    b.inputs["Base Color"].default_value = color
    b.inputs["Roughness"].default_value = rough
    b.inputs["Metallic"].default_value = metallic
    if coat:
        setp(b, ["Coat Weight", "Clearcoat"], coat)
        setp(b, ["Coat Roughness", "Clearcoat Roughness"], 0.15)
    if noise:
        coord = n.new("ShaderNodeTexCoord")
        tex = n.new("ShaderNodeTexNoise")
        tex.inputs["Scale"].default_value = 500
        tex.inputs["Detail"].default_value = 6
        l.new(coord.outputs["Object"], tex.inputs["Vector"])
        add_bump(n, l, b, tex, noise)
    return m


def mat_brushed(name, color, rough, scale=240, bump=0.10):
    m, n, l = new_mat(name)
    b = find_node(n, 'BSDF_PRINCIPLED')
    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, bump)
    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_BLADE = mat_surface("Blade", (0.040, 0.040, 0.046, 1), 0.34, 0.15, coat=0.35, noise=0.04)
M_CHROME = mat_brushed("Chrome", (0.85, 0.86, 0.90, 1), 0.10, scale=120, bump=0.03)
M_SILVER = mat_brushed("Silver", (0.72, 0.74, 0.78, 1), 0.24, scale=240, bump=0.08)
M_HUBFACE = mat_surface("HubFace", (0.075, 0.075, 0.080, 1), 0.38, 0.35, coat=0.2)
M_LOGO = mat_surface("Logo", (0.62, 0.62, 0.64, 1), 0.38, 0.55)
M_FRAME = mat_surface("Frame", (0.13, 0.13, 0.14, 1), 0.36, 0.7)
M_DARK = mat_surface("Dark", (0.012, 0.012, 0.014, 1), 0.5, 0.2)
M_FIN = mat_surface("Fin", (0.03, 0.03, 0.034, 1), 0.45, 0.8)
M_RGB = mat_rgb() if RGB_GLOW else None


# ======================= ХЕЛПЕРЫ ГЕОМЕТРИИ =======================
def revolve(name, profile, mat, closed=False, steps=128, loc=(0, 0, 0)):
    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):
    return [(cx + rx * cos(2 * pi * i / n), cz + rz * sin(2 * pi * i / n))
            for i in range(n)]


def curve_shape(name, pts, mat, extrude=0.003, 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 = 32, 14
    sgn = -1 if MIRROR else 1
    bm = bmesh.new()
    grid = []
    for i in range(NR + 1):
        t = i / NR
        r = R_IN + t * (R_OUT - R_IN)
        w = radians(ROOT_W + (TIP_W - ROOT_W) * t ** 1.1)
        center = radians(SWEEP) * t ** 1.25
        bulge = radians(7) * sin(pi * t)            # выпуклая передняя кромка
        h = 0.05 + 0.11 * t                         # разброс по высоте поперёк хорды
        row = []
        for j in range(NC + 1):
            c = j / NC
            theta = center + (c - 0.5) * w - bulge * (1 - c)
            z = (c - 0.5) * h \
                + 0.035 * sin(pi * c) * (0.6 + 0.4 * t) \
                - 0.015 * t * t
            row.append(bm.verts.new((sgn * r * cos(theta), r * sin(theta), z)))
        grid.append(row)
    for i in range(NR):
        for j in range(NC):
            quad = (grid[i][j], grid[i][j + 1], grid[i + 1][j + 1], grid[i + 1][j])
            bm.faces.new(quad[::-1] if MIRROR else quad)
    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_BLADE)
    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.003
    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] = (-1 if MIRROR else 1) * k * 2 * pi / BLADES
    link(d)
    blades.append(d)

# ======================= ТОНКОЕ СЕРЕБРИСТОЕ КОЛЬЦО =======================
rim = revolve("OuterRim", circle_pts(1.004, 0.0, 0.032, 0.026, n=14),
              M_CHROME, closed=True, steps=192)
lip = revolve("RimLip", circle_pts(0.968, 0.0, 0.012, 0.02, n=10),
              M_DARK, closed=True, steps=160)

# ======================= СТУПИЦА: плоский диск + хромовый ободок =======================
hub = revolve("HubFace", [(0.0, 0.062), (0.19, 0.060), (0.245, 0.054),
                          (0.252, 0.030), (0.252, -0.030), (0.0, -0.030)],
              M_HUBFACE, steps=128)
bezel = revolve("HubBezel", circle_pts(0.274, 0.020, 0.024, 0.042, n=16),
                M_CHROME, closed=True, steps=128)

# ======================= ЛОГОТИП ROG =======================
# контуры нарисованы в пикселях эталонного фото и переводятся в единицы сцены
S = 0.0014
CX, CY = 855, 1195


def lp(pts):
    return [((x - CX) * S, -(y - CY) * S) for x, y in pts]


LID = [(748, 1108), (780, 1085), (825, 1070), (875, 1068), (920, 1082),
       (952, 1108), (966, 1150), (970, 1210), (980, 1313),      # хвост-коготь
       (948, 1262), (938, 1200), (926, 1158), (900, 1130),
       (860, 1115), (815, 1118), (772, 1130), (742, 1128)]
BAR1 = [(745, 1142), (800, 1128), (812, 1138), (758, 1156)]
BAR2 = [(765, 1170), (835, 1150), (848, 1161), (780, 1184)]
WEDGE = [(800, 1196), (860, 1172), (915, 1182), (950, 1215),
         (958, 1262), (935, 1235), (895, 1212), (850, 1205)]

LZ = 0.064
logo_parts = [curve_shape(n, lp(p), M_LOGO, loc=(0, 0, LZ))
              for n, p in (("LogoLid", LID), ("LogoBar1", BAR1),
                           ("LogoBar2", BAR2), ("LogoWedge", WEDGE))]

# ======================= ВРАЩАЮЩАЯСЯ ГРУППА =======================
rotor = link(bpy.data.objects.new("Rotor", None))
for o in blades + [rim, lip, hub, bezel] + logo_parts:
    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 = []
    for sx, sy, a0 in [(1, 1, 0), (-1, 1, 90), (-1, -1, 180), (1, -1, 270)]:
        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.34, 0.32, 0.14)))
frame.location = (0, 0, -0.09)
frame.data.materials.append(M_FRAME)

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.012
fb.segments = 3
fb.limit_method = 'ANGLE'

# широкая серебристая дуга-обрамление вокруг вентилятора
revolve("FrameBezel", [(1.10, 0.050), (1.13, 0.064), (1.22, 0.058),
                       (1.27, 0.046), (1.27, 0.0), (1.10, 0.0)],
        M_SILVER, closed=True, steps=192)

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

# ======================= ЗАДНЯЯ ЧАСТЬ: радиатор =======================
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, -0.72))
floor = bpy.context.active_object
floor.name = "BackPlate"
floor.scale = (2.8, 2.8, 0.02)
floor.data.materials.append(M_DARK)

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, 2.0, 0.45)
fin.location = (-1.15, 0, -0.48)
fin.data.materials.append(M_FIN)
arr = fin.modifiers.new("Array", 'ARRAY')
arr.count = 74
arr.use_relative_offset = False
arr.use_constant_offset = True
arr.constant_offset_displace = (0.031, 0, 0)

motor = revolve("Motor", [(0.0, -0.030), (0.24, -0.030), (0.24, -0.25),
                          (0.0, -0.25)], M_DARK, steps=64)

# ======================= СЦЕНА =======================
world = scn.world or bpy.data.worlds.new("World")
scn.world = world
world.use_nodes = True
bg = find_node(world.node_tree.nodes, 'BACKGROUND')
bg.inputs["Color"].default_value = (1, 1, 1, 1)
bg.inputs["Strength"].default_value = 1.0


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 = link(bpy.data.objects.new("Cam", cam_d))
if VIEW == 'TOP':
    cam_d.type = 'ORTHO'
    cam_d.ortho_scale = 2.9
    cam.location = (0, 0, 6)
else:
    cam_d.type = 'PERSP'
    cam_d.lens = 70
    cam.location = (2.6, -3.4, 3.6)
    cam.rotation_euler = (radians(52), 0, radians(37))
scn.camera = cam

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("Готово: вентилятор ROG Astral создан.")