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