Загрузка данных
"""
Вентилятор 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.30 # радиус корня лопасти (прячется под ступицей)
R_OUT = 1.00 # радиус кончика (прячется под кольцом)
SWEEP = 68 # закрутка лопасти от корня к краю (град)
W_END = 52 # ширина лопасти у ступицы и у кольца (град) ~ шаг 51.4: лопасти смыкаются
W_MID = 22 # ширина в середине (град): тут открываются серповидные окна
BULGE = 7 # выпуклость передней кромки (град)
THICK = 0.016 # толщина лопасти
MIRROR = False # True - закрутка в другую сторону
RGB_GLOW = False # True - RGB-кольцо на рамке
LOGO_SVG = "" # путь к SVG логотипа ROG, например r"C:\\rog.svg" (если пусто - рисуется встроенный)
LOGO_SIZE = 0.45 # ширина логотипа
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.075, 0.075, 0.082, 1), 0.36, 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.17, 0.17, 0.18, 1), 0.40, 0.3, coat=0.2)
M_LOGO = mat_surface("Logo", (0.48, 0.48, 0.50, 1), 0.38, 0.6)
M_FRAME = mat_surface("Frame", (0.025, 0.025, 0.030, 1), 0.45, 0.4)
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, contours, mat, extrude=0.003, loc=(0, 0, 0)):
"""contours - список контуров; вложенный контур становится отверстием."""
cu = bpy.data.curves.new(name, 'CURVE')
cu.dimensions = '2D'
cu.extrude = extrude
cu.fill_mode = 'BOTH'
for pts in contours:
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 = 40, 16
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)
s_ = sin(pi * t)
wd = W_END - (W_END - W_MID) * s_ ** 0.85 # широкая у концов, уже в середине
w = radians(wd)
center = radians(SWEEP) * t ** 1.15
bulge = radians(BULGE) * s_ # выпуклая передняя кромка
lead = center - 0.5 * w - bulge
trail = center + 0.5 * w
h = 0.06 + 0.10 * t # перепад высоты поперёк хорды
row = []
for j in range(NC + 1):
c = j / NC
theta = lead + (trail - lead) * c
z = (c - 0.5) * h \
+ 0.045 * sin(pi * c) * (0.5 + 0.5 * t) \
- 0.012 * 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.004
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", [(0.945, 0.036), (0.957, 0.046), (1.025, 0.046),
(1.037, 0.036), (1.037, -0.036), (1.025, -0.046),
(0.957, -0.046), (0.945, -0.036)],
M_SILVER, closed=True, steps=192)
lip = revolve("RimLip", circle_pts(0.935, 0.0, 0.010, 0.03, n=10),
M_DARK, closed=True, steps=160)
# риски/цифры на кольце (насечки через Array по кругу)
bpy.ops.object.empty_add(location=(0, 0, 0), rotation=(0, 0, radians(5)))
tick_off = bpy.context.active_object
tick_off.name = "TickOffset"
tick_off.hide_viewport = True
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
tick = bpy.data.objects.new("Ticks", bpy.data.meshes.new("TickMesh"))
bm.to_mesh(tick.data)
bm.free()
link(tick)
tick.scale = (0.0035, 0.022, 0.002)
tick.location = (0, 0.99, 0.047)
tick.data.materials.append(M_DARK)
ta = tick.modifiers.new("Array", 'ARRAY')
ta.count = 72
ta.use_relative_offset = False
ta.use_object_offset = True
ta.offset_object = tick_off
# ======================= СТУПИЦА: диск + хромовый ободок =======================
HUB_R = 0.35
hub = revolve("HubFace", [(0.0, 0.064), (0.22, 0.062), (0.30, 0.056),
(0.312, 0.035), (0.312, -0.035), (0.0, -0.035)],
M_HUBFACE, steps=128)
bezel = revolve("HubBezel", circle_pts(0.332, 0.022, 0.030, 0.046, n=18),
M_CHROME, closed=True, steps=128)
# ======================= ЛОГОТИП ROG =======================
LZ = 0.067
S_ = LOGO_SIZE / 295.0
CX, CY = 685, 1160 # центр логотипа на эталонном фото (px)
def lp(pts):
return [((x - CX) * S_, -(y - CY) * S_) for x, y in pts]
EYE_OUT = [(588, 1160), (625, 1130), (680, 1100), (745, 1084), (800, 1080),
(832, 1084), (826, 1118), (806, 1165), (782, 1210), (762, 1232),
(725, 1236), (680, 1222), (640, 1200), (610, 1180)]
EYE_IN = [(640, 1170), (680, 1140), (740, 1120), (790, 1112), (780, 1150),
(760, 1185), (745, 1205), (715, 1200), (672, 1190)]
PUPIL = [(705, 1172), (780, 1152), (755, 1190)]
WING = [(537, 1138), (575, 1122), (615, 1128), (650, 1146), (615, 1146), (580, 1150)]
TICK_L = [(570, 1168), (610, 1172), (640, 1190), (600, 1188)]
logo_parts = []
if LOGO_SVG:
try:
before = set(bpy.data.objects)
bpy.ops.import_curve.svg(filepath=LOGO_SVG)
new = [o for o in bpy.data.objects if o not in before]
bpy.ops.object.select_all(action='DESELECT')
for o in new:
o.select_set(True)
bpy.context.view_layer.objects.active = new[0]
if len(new) > 1:
bpy.ops.object.join()
ob = bpy.context.view_layer.objects.active
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
ob.location = (0, 0, LZ)
k = LOGO_SIZE / max(ob.dimensions.x, ob.dimensions.y)
ob.scale = (k, k, k)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
ob.data.dimensions = '2D'
ob.data.fill_mode = 'BOTH'
ob.data.extrude = 0.003
ob.data.materials.clear()
ob.data.materials.append(M_LOGO)
logo_parts = [ob]
except Exception as e:
print("SVG не загрузился, использую встроенный логотип:", e)
if not logo_parts:
logo_parts = [
curve_shape("LogoEye", [lp(EYE_OUT), lp(EYE_IN)], M_LOGO, loc=(0, 0, LZ)),
curve_shape("LogoPupil", [lp(PUPIL)], M_LOGO, loc=(0, 0, LZ)),
curve_shape("LogoWing", [lp(WING)], M_LOGO, loc=(0, 0, LZ)),
curve_shape("LogoTick", [lp(TICK_L)], M_LOGO, loc=(0, 0, LZ)),
]
# ======================= ВРАЩАЮЩАЯСЯ ГРУППА =======================
rotor = link(bpy.data.objects.new("Rotor", None))
for o in blades + [rim, lip, hub, bezel, tick] + 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 создан.")