Загрузка данных
"""
Задняя планка видеокарты (детальная версия): 3 x DisplayPort + 1 x HDMI, ГНЁЗДА.
Двухслотовая планка с сотовой вентиляцией, загнутым ушком под винт,
печатная плата с дорожками, процедурные текстуры (без внешних картинок).
Запуск: Blender -> Scripting -> Создать -> вставить код -> Текст -> Запустить скрипт.
Единицы: 1 единица Blender = 1 мм. Гнёзда смотрят в сторону -Y.
Если всё чёрное: в 3D-виде переключи отображение на Material Preview (3-й шарик справа вверху).
"""
import bpy
import bmesh
import mathutils
from math import pi, sin, cos, sqrt
# ---------- Настройки ----------
PORTS = ["DP", "DP", "DP", "HDMI"] # порядок снизу вверх
PITCH_Z = 21.0 # расстояние между центрами портов, мм
FIRST_Z = 12.0 # высота центра первого порта, мм
DUAL_SLOT = True # True: двухслотовая планка с вентиляцией, False: один слот
SLOT_W = 20.32 # ширина одного слота, мм
PLATE_Z0, PLATE_Z1 = -6.0, 90.0 # нижний/верхний край планки, мм
PLATE_T = 1.2 # толщина планки, мм
EAR_LEN = 9.0 # длина загнутого ушка, мм
WALL = 0.3 # толщина металлической рамки гнезда, мм
CLEAR = 0.15 # зазор для штекера, мм
DEPTH = 12.0 # глубина гнезда, мм
PCB_LEN = 75.0 # длина показанной части платы, мм
PCB_T = 1.6 # толщина платы, мм
PREFIX = "GPU_"
# ---------- Материалы с процедурными текстурами ----------
def make_material(name, c0, c1, metallic, rough, rough_var=0.1, noise_scale=6.0,
stretch=(1.0, 1.0, 1.0), bump=0.1, traces=None, detail=6.0):
"""c0/c1 - цвета (темный/светлый) для шума; traces=(цвет_дорожек, цвет_фона)."""
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name)
try:
mat.use_nodes = True
except Exception:
pass
nt = mat.node_tree
nt.nodes.clear()
out = nt.nodes.new("ShaderNodeOutputMaterial")
bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
bsdf.inputs["Base Color"].default_value = c1
bsdf.inputs["Metallic"].default_value = metallic
bsdf.inputs["Roughness"].default_value = rough
try:
tc = nt.nodes.new("ShaderNodeTexCoord")
mp = nt.nodes.new("ShaderNodeMapping")
nt.links.new(tc.outputs["Object"], mp.inputs["Vector"])
mp.inputs["Scale"].default_value = tuple(noise_scale * s for s in stretch)
nz = nt.nodes.new("ShaderNodeTexNoise")
nz.inputs["Scale"].default_value = 1.0
nz.inputs["Detail"].default_value = detail
nz.inputs["Roughness"].default_value = 0.6
nt.links.new(mp.outputs["Vector"], nz.inputs["Vector"])
# цвет
ramp = nt.nodes.new("ShaderNodeValToRGB")
ramp.color_ramp.elements[0].color = c0
ramp.color_ramp.elements[1].color = c1
nt.links.new(nz.outputs["Fac"], ramp.inputs["Fac"])
color_src = ramp.outputs["Color"]
# дорожки текстолита: границы ячеек Вороного
if traces is not None:
mp2 = nt.nodes.new("ShaderNodeMapping")
nt.links.new(tc.outputs["Object"], mp2.inputs["Vector"])
mp2.inputs["Scale"].default_value = (0.5, 0.5, 0.5)
vn = nt.nodes.new("ShaderNodeTexVoronoi")
vn.feature = "DISTANCE_TO_EDGE"
vn.inputs["Scale"].default_value = 1.0
nt.links.new(mp2.outputs["Vector"], vn.inputs["Vector"])
tr = nt.nodes.new("ShaderNodeValToRGB")
tr.color_ramp.interpolation = "CONSTANT"
tr.color_ramp.elements[0].position = 0.0
tr.color_ramp.elements[0].color = traces[0]
e = tr.color_ramp.elements.new(0.04)
e.color = traces[1]
nt.links.new(vn.outputs["Distance"], tr.inputs["Fac"])
color_src = tr.outputs["Color"]
nt.links.new(color_src, bsdf.inputs["Base Color"])
# неоднородность шероховатости
mr = nt.nodes.new("ShaderNodeMapRange")
mr.inputs["From Min"].default_value = 0.3
mr.inputs["From Max"].default_value = 0.7
mr.inputs["To Min"].default_value = max(rough - rough_var, 0.02)
mr.inputs["To Max"].default_value = min(rough + rough_var, 1.0)
nt.links.new(nz.outputs["Fac"], mr.inputs["Value"])
nt.links.new(mr.outputs["Result"], bsdf.inputs["Roughness"])
# микрорельеф
bp = nt.nodes.new("ShaderNodeBump")
bp.inputs["Strength"].default_value = bump
bp.inputs["Distance"].default_value = 0.05
nt.links.new(nz.outputs["Fac"], bp.inputs["Height"])
nt.links.new(bp.outputs["Normal"], bsdf.inputs["Normal"])
except Exception as ex:
print("Текстуры материала", name, "не применены:", ex)
return mat
def build_materials():
m = {}
# шлифованный металл планки: полосы вдоль оси Z
m["bracket"] = make_material("GPU_Bracket", (0.55, 0.57, 0.60, 1), (0.82, 0.84, 0.86, 1),
1.0, 0.35, 0.12, noise_scale=7.0, stretch=(8.0, 8.0, 0.25), bump=0.25)
# металл рамок: светлее и глаже
m["shell"] = make_material("GPU_Shell", (0.70, 0.72, 0.75, 1), (0.92, 0.93, 0.95, 1),
1.0, 0.22, 0.08, noise_scale=5.0, stretch=(6.0, 6.0, 0.4), bump=0.12)
# чёрный пластик с зерном
m["plastic"] = make_material("GPU_Plastic", (0.012, 0.012, 0.014, 1), (0.04, 0.04, 0.045, 1),
0.0, 0.5, 0.15, noise_scale=40.0, bump=0.35, detail=3.0)
# позолота
m["gold"] = make_material("GPU_Gold", (0.80, 0.55, 0.15, 1), (1.0, 0.80, 0.30, 1),
1.0, 0.25, 0.1, noise_scale=20.0, bump=0.05)
# текстолит с дорожками
m["pcb"] = make_material("GPU_PCB", (0.01, 0.10, 0.03, 1), (0.02, 0.18, 0.05, 1),
0.0, 0.45, 0.1, noise_scale=30.0, bump=0.15,
traces=((0.55, 0.38, 0.10, 1), (0.015, 0.14, 0.04, 1)))
return m
def assign(obj, mat):
obj.data.materials.clear()
obj.data.materials.append(mat)
# ---------- Профили (u - длинная сторона -> Z, v - короткая -> X) ----------
def hdmi_uv(w, h, c=1.0):
return [(-w / 2, h / 2), (w / 2, h / 2), (w / 2, 0.0),
(w / 2 - c, -h / 2), (-w / 2 + c, -h / 2), (-w / 2, 0.0)]
def dp_uv(w, h, c=1.5):
return [(-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2 - c),
(w / 2 - c, h / 2), (-w / 2, h / 2)]
def hex_uv(r):
"""Шестиугольник вершиной вверх (по Z)."""
pts = []
for k in range(6):
a = pi / 2 + k * pi / 3
pts.append((r * sin(a), r * cos(a))) # (u=z, v=x)
return pts
def stadium_xy(cx, cy, hl, r, n=10):
"""Овальный паз в плоскости XY (для винта)."""
pts = []
for k in range(n + 1):
a = -pi / 2 + pi * k / n
pts.append((cx + hl + r * cos(a), cy + r * sin(a)))
for k in range(n + 1):
a = pi / 2 + pi * k / n
pts.append((cx - hl + r * cos(a), cy + r * sin(a)))
return pts
SPEC = {
"HDMI": dict(w=13.9, h=4.45, c=1.0, prof=hdmi_uv, top=10, bot=9),
"DP": dict(w=16.1, h=4.76, c=1.5, prof=dp_uv, top=10, bot=10),
}
def port_profiles(kind):
s = SPEC[kind]
wi, hi = s["w"] + CLEAR, s["h"] + CLEAR
inner = s["prof"](wi, hi, s["c"])
outer = s["prof"](wi + 2 * WALL, hi + 2 * WALL, s["c"])
lip = s["prof"](wi + 2 * WALL + 1.0, hi + 2 * WALL + 1.0, s["c"])
return s, wi, hi, inner, outer, lip
# ---------- Построитель сетки ----------
class MB:
def __init__(self):
self.v = []
self.f = []
def _loop_prism(self, a, b):
"""a, b - два списка вершин одинаковой длины (нижнее и верхнее основание)."""
n = len(a)
base = len(self.v)
self.v += a + b
self.f.append(tuple(base + i for i in reversed(range(n))))
self.f.append(tuple(base + n + i for i in range(n)))
for i in range(n):
j = (i + 1) % n
self.f.append((base + i, base + j, base + n + j, base + n + i))
def box(self, x0, x1, y0, y1, z0, z1):
b = len(self.v)
self.v += [(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0),
(x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)]
for q in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4),
(2, 3, 7, 6), (1, 2, 6, 5), (3, 0, 4, 7)):
self.f.append(tuple(b + i for i in q))
def prism_y(self, uv, xc, zc, y0, y1):
self._loop_prism([(xc + v, y0, zc + u) for (u, v) in uv],
[(xc + v, y1, zc + u) for (u, v) in uv])
def prism_x(self, yz, x0, x1):
self._loop_prism([(x0, y, z) for (y, z) in yz], [(x1, y, z) for (y, z) in yz])
def prism_z(self, xy, z0, z1):
self._loop_prism([(x, y, z0) for (x, y) in xy], [(x, y, z1) for (x, y) in xy])
def tube_y(self, outer_uv, inner_uv, xc, zc, y0, y1):
n = len(outer_uv)
b = len(self.v)
self.v += [(xc + v, y0, zc + u) for (u, v) in outer_uv]
self.v += [(xc + v, y1, zc + u) for (u, v) in outer_uv]
self.v += [(xc + v, y0, zc + u) for (u, v) in inner_uv]
self.v += [(xc + v, y1, zc + u) for (u, v) in inner_uv]
for i in range(n):
j = (i + 1) % n
self.f.append((b + i, b + j, b + n + j, b + n + i))
self.f.append((b + 2 * n + i, b + 3 * n + i, b + 3 * n + j, b + 2 * n + j))
self.f.append((b + i, b + 2 * n + i, b + 2 * n + j, b + j))
self.f.append((b + n + i, b + n + j, b + 3 * n + j, b + 3 * n + i))
def build(self, name, mat=None):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(self.v, [], self.f)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(mesh)
bm.free()
if mat is not None:
assign(obj, mat)
return obj
# ---------- Сцена ----------
def cleanup():
for ob in list(bpy.data.objects):
if ob.name.startswith(PREFIX):
bpy.data.objects.remove(ob, do_unlink=True)
def build_bracket(mats, centers):
x0 = -SLOT_W / 2
x1 = SLOT_W / 2 + (SLOT_W if DUAL_SLOT else 0.0)
# Профиль в плоскости YZ: планка + загнутое ушко сверху
L = [(0.0, PLATE_Z0), (PLATE_T, PLATE_Z0), (PLATE_T, PLATE_Z1 + PLATE_T),
(-EAR_LEN, PLATE_Z1 + PLATE_T), (-EAR_LEN, PLATE_Z1), (0.0, PLATE_Z1)]
mb = MB()
mb.prism_x(L, x0, x1)
plate = mb.build(PREFIX + "Bracket")
# Вырезы: порты, соты, паз под винт
cut = MB()
for kind, zc in zip(PORTS, centers):
outer = port_profiles(kind)[4]
cut.prism_y(outer, 0.0, zc, -1.0, PLATE_T + 1.0)
if DUAL_SLOT:
r, gap = 2.3, 0.9
reff = r + gap
dx, dy = sqrt(3) * reff, 1.5 * reff
xs = SLOT_W / 2 + 3.2
xmax = x1 - 3.0
j = 0
z = PLATE_Z0 + 5.0
while z < PLATE_Z1 - 4.0:
off = (j % 2) * dx / 2
i = 0
while True:
x = xs + off + i * dx
if x > xmax:
break
cut.prism_y(hex_uv(r), x, z, -1.0, PLATE_T + 1.0)
i += 1
z += dy
j += 1
cut.prism_z(stadium_xy((x0 + x1) / 2, -EAR_LEN / 2, 3.0, 2.2),
PLATE_Z1 - 1.0, PLATE_Z1 + PLATE_T + 1.0)
cutter = cut.build(PREFIX + "Cutter")
bpy.context.view_layer.update()
mod = plate.modifiers.new("Holes", "BOOLEAN")
mod.operation = "DIFFERENCE"
mod.solver = "EXACT"
mod.object = cutter
dg = bpy.context.evaluated_depsgraph_get()
new_mesh = bpy.data.meshes.new_from_object(plate.evaluated_get(dg))
plate.modifiers.remove(mod)
old = plate.data
plate.data = new_mesh
bpy.data.meshes.remove(old)
cmesh = cutter.data
bpy.data.objects.remove(cutter, do_unlink=True)
bpy.data.meshes.remove(cmesh)
# Фаски на кромках
bev = plate.modifiers.new("Bevel", "BEVEL")
bev.width = 0.12
bev.segments = 2
assign(plate, mats["bracket"])
for p in plate.data.polygons:
p.use_smooth = False
return plate
def build_ports(mats, centers):
shell = MB()
lips = MB()
wall = MB()
tongue = MB()
pins = MB()
tt, ct = 1.0, 0.12
tip = 2.5
for kind, zc in zip(PORTS, centers):
s, wi, hi, inner, outer, lip = port_profiles(kind)
# рамка и декоративный бортик спереди планки
shell.tube_y(outer, inner, 0.0, zc, 0.0, DEPTH + 1.0)
lips.tube_y(lip, inner, 0.0, zc, -0.3, 0.0)
# пружинные выступы на внутренних длинных стенках
for du in (-(wi / 2 - 3.0), (wi / 2 - 3.0)):
shell.box(hi / 2 - 0.35, hi / 2, 2.0, 5.0, zc + du - 1.0, zc + du + 1.0)
shell.box(-hi / 2, -hi / 2 + 0.35, 2.0, 5.0, zc + du - 1.0, zc + du + 1.0)
# задняя стенка и язычок
wall.prism_y(inner, 0.0, zc, DEPTH, DEPTH + 1.0)
tw = wi - 2.0
tongue.box(-tt / 2, tt / 2, tip, DEPTH, zc - tw / 2, zc + tw / 2)
# фаска-носик язычка (выступающие скосы у кончика)
tongue.box(-tt / 2 - 0.1, tt / 2 + 0.1, tip + 0.4, tip + 1.0, zc - tw / 2 + 0.6, zc + tw / 2 - 0.6)
# контакты
for count, sign in ((s["top"], 1), (s["bot"], -1)):
x = sign * (tt / 2 + ct / 2)
for i in range(count):
u = zc + (i - (count - 1) / 2) * 1.0
pins.box(x - ct / 2, x + ct / 2, tip + 0.5, DEPTH - 0.5, u - 0.25, u + 0.25)
shell.build(PREFIX + "Shells", mats["shell"])
lips.build(PREFIX + "Bezels", mats["shell"])
wall.build(PREFIX + "BackWalls", mats["plastic"])
tongue.build(PREFIX + "Tongues", mats["plastic"])
pins.build(PREFIX + "Pins", mats["gold"])
def build_pcb(mats):
hmax = max((SPEC[k]["h"] + CLEAR) / 2 + WALL for k in PORTS)
pcb = MB()
pcb.box(-hmax - PCB_T, -hmax, PLATE_T + 0.3, PLATE_T + 0.3 + PCB_LEN,
PLATE_Z0 + 1.0, PLATE_Z1 - 3.0)
pcb.build(PREFIX + "PCB", mats["pcb"])
def build_scene_extras():
# Камера
cam_data = bpy.data.cameras.new(PREFIX + "Camera")
cam_data.lens = 40
cam = bpy.data.objects.new(PREFIX + "Camera", cam_data)
bpy.context.collection.objects.link(cam)
cam.location = (45.0, -110.0, 75.0)
target = mathutils.Vector((8.0, 0.0, 42.0))
direction = target - cam.location
cam.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
bpy.context.scene.camera = cam
# Свет
for name, loc, energy, size in (("Key", (10, -90, 80), 12000, 60), ("Fill", (80, -60, 20), 4000, 40)):
ld = bpy.data.lights.new(PREFIX + name, "AREA")
ld.energy = energy
ld.size = size
lo = bpy.data.objects.new(PREFIX + name, ld)
bpy.context.collection.objects.link(lo)
lo.location = loc
lo.rotation_euler = (pi / 2.3, 0, 0.3 if name == "Fill" else 0.0)
# Показать в режиме Material Preview, вид спереди
try:
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
area.spaces[0].shading.type = "MATERIAL"
except Exception as ex:
print("Не удалось переключить вид:", ex)
def main():
cleanup()
mats = build_materials()
centers = [FIRST_Z + i * PITCH_Z for i in range(len(PORTS))]
build_bracket(mats, centers)
build_ports(mats, centers)
build_pcb(mats)
build_scene_extras()
print("Готово: планка видеокарты, порты:", PORTS)
main()