Загрузка данных
"""
Вентилятор 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 создан.")