1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonSCADコミュニティ・開発者に感謝申し上げます。ありがとうございます。

参考サイト

本プログラムのシーン構成および曲線の方程式は、以下の素晴らしい作品・解説を
参考に(インスパイアされて)PythonSCADコードとして新たに再構築したものです。
素敵な知見を共有してくださった作者様に深く感謝いたします。
ありがとうございます。

01. 波紋アクセサリートレイ(直径12cm)

中心から外へ広がる同心円の波紋を、固めてお皿にして。
波の谷にアクセサリーが収まるように、外周はフチを立ち上げて。
直径12cm

1. 完成コード(PythonSCAD)

# ==========================================
# 波紋アクセサリートレイ(直径12cm)
# 作成日: 2026/09/03
# ==========================================

from pythonscad import *
from math import pi, sin, cos

# --- パラメータ設定 (単位: mm) ---
diameter        = 120.0   # 皿の外径(12cm)
outer_radius    = diameter / 2.0

bottom_thick    = 3.0     # 底の最小厚み
rim_width       = 7.0     # 外周フチの幅
rim_height      = 14.0    # 外周フチの高さ
rim_round       = 1.5     # フチ上端の丸み

floor_radius    = outer_radius - rim_width  # 波紋が入る内側半径

ripple_count    = 5       # 同心円の波の数
ripple_height   = 2.4     # 波の山の高さ
dish_slope      = 1.2     # 外側へ少し高くする傾き

profile_samples = 160     # 断面の細かさ
arc_steps       = 10      # フチ丸みの細かさ
fn_val          = 180     # 回転体の円滑度

dish_color      = "bisque"


# --- 関数(モジュール)定義 ---

def ripple_z(r):
    """
    半径 r の位置における波紋面の高さを計算する。
    cos波を使い、山と谷が交互に出るようにする。
    谷の部分に指輪やピアスなどが収まりやすい。
    """
    wave_pitch = floor_radius / ripple_count

    # cos は 1 のとき山、-1 のとき谷
    wave = 0.5 * (1.0 + cos(2.0 * pi * r / wave_pitch))

    # 外側へほんの少し高くして、アクセサリーが中央へ戻りやすい形にする
    slope = dish_slope * (r / floor_radius)

    return bottom_thick + slope + ripple_height * wave


def create_ripple_profile():
    """
    rotate_extrude 用の2D断面を作る。
    X方向 = 半径方向
    Y方向 = 高さ方向
    """
    top_points = []

    # 中心から外側まで、波紋の上面を作る
    for i in range(profile_samples + 1):
        r = floor_radius * i / profile_samples
        z = ripple_z(r)
        top_points.append([r, z])

    floor_z_at_rim = ripple_z(floor_radius)

    # 断面ポリゴンを反時計回りに作成
    profile_pts = []

    # 底面:中心から外周へ
    profile_pts.append([0.0, 0.0])
    profile_pts.append([outer_radius, 0.0])

    # 外周フチの外側壁
    profile_pts.append([outer_radius, rim_height - rim_round])

    # 外周フチ上端の外側丸み
    for j in range(arc_steps + 1):
        theta = (pi / 2.0) * j / arc_steps
        x = outer_radius - rim_round + rim_round * cos(theta)
        z = rim_height - rim_round + rim_round * sin(theta)
        profile_pts.append([x, z])

    # フチ上面
    profile_pts.append([floor_radius + rim_round, rim_height])

    # フチ内側上端の丸み
    center_x = floor_radius + rim_round
    center_z = rim_height - rim_round
    for j in range(arc_steps + 1):
        theta = (pi / 2.0) + (pi / 2.0) * j / arc_steps
        x = center_x + rim_round * cos(theta)
        z = center_z + rim_round * sin(theta)
        profile_pts.append([x, z])

    # フチの内側壁を波紋面まで下げる
    profile_pts.append([floor_radius, floor_z_at_rim])

    # 波紋面を外側から中心へ戻る
    for i in range(profile_samples, -1, -1):
        profile_pts.append(top_points[i])

    raw_profile = polygon(points=profile_pts)

    # rotate_extrude の安全対策:
    # 万一、断面がX<0側にはみ出しても、ここで切り落とす
    negative_side_cutter = polygon(points=[
        [-200.0, -50.0],
        [-0.001, -50.0],
        [-0.001, rim_height + 50.0],
        [-200.0, rim_height + 50.0]
    ])

    safe_profile = raw_profile - negative_side_cutter

    return safe_profile


def create_dish():
    """波紋皿本体を生成する"""
    profile_2d = create_ripple_profile()
    dish = rotate_extrude(profile_2d, fn=fn_val)
    return dish


# --- メイン処理 ---

# 1. 断面を回転させて、直径12cmの波紋皿を作る
dish_body = create_dish()

# 2. 色をつける
result = color(dish_body, dish_color)

# 3. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-06 165403.png


2. 形のポイント

  • 直径12cm = 120mm のアクセサリートレイです
  • 中心から外側へ向かって、同心円状の波紋が広がります
  • 波は cos を使って作っているので、山と谷がなめらかにつながります
  • 谷の部分に、指輪・ピアス・ネックレスなどの小物が収まりやすい形です
  • 外周には 高さ14mmの立ち上がりフチ を付けています
  • フチ上端には rim_round = 1.5 の丸みを入れて、手に当たっても痛くなりにくい形にしています

3. 調整すると楽しいパラメータ

ripple_count = 5

波の本数を増やしたいときは、ここを 67 にします。

ripple_height = 2.4

波の山をもっとはっきりさせたいときは、3.0 くらいにします。

rim_height = 14.0

フチをもっと高くしたいときは、18.0 などにします。

02. ねじれた一輪挿し(テラコッタ / 溝6本 / ゆるめのねじれ)

ねじれた一輪挿しを作って。
表面に溝を入れて、上にいくほどらせん状にねじれるように。
ドライフラワーを挿す前提でコード生成してください。
テラコッタ:溝6本・ゆるめのねじれ・ぽってりした瓶型

1. 完成コード(PythonSCAD)

# ==========================================
# ねじれた一輪挿し(テラコッタ / 溝6本 / ゆるめのねじれ)
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import pi, sin, cos

# --- パラメータ設定 (単位: mm) ---

# 全体寸法
vase_height      = 105.0   # 一輪挿しの高さ
max_body_radius  = 25.5    # ぽってりした胴の最大半径
base_radius      = 13.5    # 底付近の半径
neck_radius      = 10.5    # 首の細い部分の半径
lip_radius       = 12.5    # 口元の少しふくらんだ半径

# 溝・ねじれ
groove_count     = 6       # 溝の本数
groove_depth     = 2.4     # 溝の深さ
groove_power     = 3.0     # 溝の細さ(大きいほど細め)
twist_degrees    = 75.0    # 全高でのねじれ角度(ゆるめ)

# ドライフラワー用の差し込み穴
stem_hole_radius = 4.2     # 茎を挿す中心穴の半径
stem_hole_depth  = 56.0    # 穴の深さ(貫通させない)
mouth_radius     = 6.2     # 口元だけ少し広げる半径
mouth_depth      = 8.0     # 口元の広がり深さ

# メッシュ分割
z_segments       = 52      # 高さ方向の分割数
angle_segments   = 144     # 円周方向の分割数
fn_val           = 72      # 円柱カッター用の分割数

# 安全マージン
eps              = 0.1     # ブーリアン時のチラつき防止


# --- 関数(モジュール)定義 ---

def clamp01(t):
    """0〜1の範囲に収める"""
    if t < 0.0:
        return 0.0
    if t > 1.0:
        return 1.0
    return t


def smoothstep(t):
    """なめらかに変化する補間関数"""
    t = clamp01(t)
    return t * t * (3.0 - 2.0 * t)


def mix(a, b, t):
    """aからbへなめらかに補間"""
    return a + (b - a) * smoothstep(t)


def body_radius_at_z(z):
    """
    高さzにおける瓶の基本半径を返す。
    ぽってりした胴 → なだらかな肩 → 細い首 → 少し広がる口、という瓶型。
    """
    t = z / vase_height

    if t < 0.10:
        # 底から胴へ、ゆるくふくらむ
        local_t = t / 0.10
        return mix(base_radius, 20.0, local_t)

    if t < 0.42:
        # 胴の最大ふくらみ
        local_t = (t - 0.10) / 0.32
        return mix(20.0, max_body_radius, local_t)

    if t < 0.58:
        # 胴の丸みを少し保つ
        local_t = (t - 0.42) / 0.16
        return mix(max_body_radius, 23.0, local_t)

    if t < 0.78:
        # 肩の部分:ぽってりした胴から首へ絞る
        local_t = (t - 0.58) / 0.20
        return mix(23.0, neck_radius, local_t)

    if t < 0.92:
        # 細い首
        local_t = (t - 0.78) / 0.14
        return mix(neck_radius, neck_radius, local_t)

    # 口元だけ少し広げる
    local_t = (t - 0.92) / 0.08
    return mix(neck_radius, lip_radius, local_t)


def groove_amount(theta, z):
    """
    らせん状の溝のへこみ量を0〜1で返す。
    theta は円周角度、z は高さ。
    上に行くほど位相がずれて、ゆるくねじれる。
    """
    height_t = z / vase_height
    twist_rad = twist_degrees * pi / 180.0

    # theta - twist_rad * height_t により、上へ行くほど溝の位置が回転する
    phase = groove_count * (theta - twist_rad * height_t)

    # cosが1の位置を溝の中心にする
    raw = (1.0 + cos(phase)) / 2.0

    # powerをかけることで溝を細める
    return raw ** groove_power


def create_twisted_grooved_body():
    """
    ねじれ溝つきの外形をポリヘドロンで生成する。
    半径を角度と高さで少し変化させることで、らせん状の溝を直接作る。
    """
    points = []
    faces = []

    # 側面の頂点
    for zi in range(z_segments + 1):
        z = vase_height * zi / z_segments
        base_r = body_radius_at_z(z)

        for ai in range(angle_segments):
            theta = 2.0 * pi * ai / angle_segments

            groove = groove_amount(theta, z)
            r = base_r - groove_depth * groove

            x = r * cos(theta)
            y = r * sin(theta)

            points.append([x, y, z])

    # 側面の面
    for zi in range(z_segments):
        for ai in range(angle_segments):
            next_ai = (ai + 1) % angle_segments

            p00 = zi * angle_segments + ai
            p10 = zi * angle_segments + next_ai
            p11 = (zi + 1) * angle_segments + next_ai
            p01 = (zi + 1) * angle_segments + ai

            faces.append([p00, p10, p11, p01])

    # 底面中心点
    bottom_center_index = len(points)
    points.append([0.0, 0.0, 0.0])

    # 底面の三角面(外向き法線を下向きにする)
    for ai in range(angle_segments):
        next_ai = (ai + 1) % angle_segments
        faces.append([bottom_center_index, next_ai, ai])

    # 上面中心点
    top_center_index = len(points)
    points.append([0.0, 0.0, vase_height])

    top_start = z_segments * angle_segments

    # 上面の三角面(外向き法線を上向きにする)
    for ai in range(angle_segments):
        next_ai = (ai + 1) % angle_segments
        p0 = top_start + ai
        p1 = top_start + next_ai
        faces.append([top_center_index, p0, p1])

    return polyhedron(points=points, faces=faces)


def create_opening_cutter():
    """
    ドライフラワー用の差し込み穴を作るカッター。
    水を入れる前提ではないので、底まで貫通させない。
    """
    # 茎を差し込む深めの穴
    stem_hole = cylinder(
        h=stem_hole_depth + 2.0 * eps,
        r=stem_hole_radius,
        center=True,
        fn=fn_val
    )
    stem_hole = translate(
        stem_hole,
        [0.0, 0.0, vase_height - stem_hole_depth / 2.0 + eps]
    )

    # 口元だけ少し広げるテーパー穴
    mouth_taper = cylinder(
        h=mouth_depth + eps,
        r1=stem_hole_radius,
        r2=mouth_radius,
        center=False,
        fn=fn_val
    )
    mouth_taper = translate(
        mouth_taper,
        [0.0, 0.0, vase_height - mouth_depth]
    )

    return stem_hole + mouth_taper


# --- メイン処理 ---

# 1. 本体を生成
body = create_twisted_grooved_body()

# 2. カッターを生成
cutter = create_opening_cutter()

# 3. 一括くり抜き
result = body - cutter

# 4. テラコッタ風カラー
result = color(result, "sienna")

# 5. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-06 165920.png


ポイント

  • 溝6本groove_count = 6
  • ゆるめのねじれtwist_degrees = 75.0
  • ぽってり瓶型:高さごとに半径を変える body_radius_at_z() で表現
  • ドライフラワー用:中央穴は貫通させず、茎を差し込む深さだけ削っています
  • テラコッタ風:色は "sienna" を使用しています

水を入れる花瓶ではなく、ドライフラワー用の一輪挿しなので、内部は大きく中空にせず、上から茎を差し込む構造にしています。

03. エッシャー風・波形タイル

正方形の1辺を波打たせて、同じ曲線を反対の辺にコピーして、
すき間なく敷き詰まるタイルにして。
エッシャー風だけど柄はオリジナルで。

1. 完成コード(PythonSCAD)

# ==========================================
# エッシャー風・波形タイル
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import pi, sin, cos

# --- パラメータ設定 (単位: mm) ---
tile_size       = 40.0     # 元の正方形の1辺
tile_thickness  = 3.0      # タイル厚み
wave_amp        = 4.5      # 波の大きさ
wave_steps      = 40       # 波形の分割数
fn_val          = 48       # 円形部分のなめらかさ
eps             = 0.05     # チラつき防止用

engrave_depth   = 0.7      # 表面模様の彫り深さ

# 敷き詰め表示用
tiles_x         = 4
tiles_y         = 3

# --- 関数(モジュール)定義 ---

def wave_offset(x, size, amp):
    """
    波形のY方向オフセット。
    両端で必ず0になるように sin(2πu), sin(4πu) を組み合わせています。
    """
    u = (x + size / 2) / size
    return amp * (0.70 * sin(2 * pi * u) + 0.30 * sin(4 * pi * u))


def create_tile_profile(size, amp, steps):
    """
    2Dのタイル外形を作る。
    下辺を波形にし、上辺には同じ波形をそのままコピーする。
    
    下辺: y = -size/2 + wave(x)
    上辺: y =  size/2 + wave(x)
    
    つまり、上辺は下辺をY方向に size だけ平行移動した形。
    これにより上下方向にすき間なく敷き詰められます。
    """
    pts = []

    # 下辺:左から右へ
    for i in range(steps + 1):
        x = -size / 2 + size * i / steps
        y = -size / 2 + wave_offset(x, size, amp)
        pts.append([x, y])

    # 上辺:右から左へ
    for i in range(steps, -1, -1):
        x = -size / 2 + size * i / steps
        y = size / 2 + wave_offset(x, size, amp)
        pts.append([x, y])

    return polygon(points=pts)


def create_tile_body(size, amp, steps, thickness):
    """
    波形外形を押し出して、薄いタイル本体を作る。
    Z方向は底面を Z=0 に置く center=False の構造です。
    """
    profile = create_tile_profile(size, amp, steps)
    body = linear_extrude(profile, height=thickness)
    return body


def create_engrave_cutter(thickness, depth, fn):
    """
    表面に彫るオリジナル模様用カッター。
    「風のたね」が流れていくような抽象模様にしています。
    """
    cutter_h = depth + 2 * eps
    cutter_z = thickness - depth / 2 + eps

    # 中央の大きめの丸い彫り
    cutter = translate(
        cylinder(h=cutter_h, r=3.2, center=True, fn=fn),
        [-7.0, -1.5, cutter_z]
    )

    # 小さな流れ模様:点を波状に並べる
    dot_count = 8
    for i in range(dot_count):
        t = i / (dot_count - 1)
        x = -12.0 + 24.0 * t
        y = 5.0 * sin(2 * pi * t) + 2.0
        r = 1.6 - 0.7 * t

        dot = translate(
            cylinder(h=cutter_h, r=r, center=True, fn=fn),
            [x, y, cutter_z]
        )
        cutter = cutter + dot

    # 小さな三つ星風の点
    for i in range(3):
        angle = 2 * pi * i / 3
        x = 9.0 + 3.2 * cos(angle)
        y = -7.0 + 3.2 * sin(angle)

        star_dot = translate(
            cylinder(h=cutter_h, r=0.9, center=True, fn=fn),
            [x, y, cutter_z]
        )
        cutter = cutter + star_dot

    return cutter


def create_one_tile():
    """
    1枚分のタイルを作る。
    本体を作り、最後に模様カッターを一括で引き算します。
    """
    body = create_tile_body(tile_size, wave_amp, wave_steps, tile_thickness)
    cutter = create_engrave_cutter(tile_thickness, engrave_depth, fn_val)
    tile = body - cutter
    return tile


def add_shape(base, part):
    """
    + 演算子で安全に形状を追加するための補助関数。
    """
    if base is None:
        return part
    return base + part


# --- メイン処理 ---

# 1枚の基本タイル
one_tile = create_one_tile()

# 敷き詰め確認用に複数枚を並べる
scene = None

palette = [
    "lightskyblue",
    "palegreen",
    "khaki",
    "plum"
]

for ix in range(tiles_x):
    for iy in range(tiles_y):
        x_pos = (ix - (tiles_x - 1) / 2) * tile_size
        y_pos = (iy - (tiles_y - 1) / 2) * tile_size

        tile_color = palette[(ix + iy) % len(palette)]

        placed_tile = translate(
            color(one_tile, tile_color),
            [x_pos, y_pos, 0]
        )

        scene = add_shape(scene, placed_tile)

result = scene

# 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-06 170319.png


2. 形のポイント

  • 元は 40mm × 40mm の正方形です。
  • 下辺を波形にしています。
  • 上辺には、下辺とまったく同じ波形をコピーしています。
  • そのため、タイルを上下に並べると、
    下のタイルの上辺上のタイルの下辺がぴったり一致します。

つまり、

上辺 = 下辺をY方向に tile_size だけ移動した形

になっています。

左右の辺は直線なので、横方向にもすき間なく並べられます。


3. オリジナル柄について

表面には「風のたね」っぽい抽象模様を浅く彫っています。
円形のくぼみを波状に並べているので、エッシャー風の敷き詰め感を出しつつ、柄はオリジナルです。

3Dプリントする場合は、彫り深さ engrave_depth = 0.7 mm くらいなら見えやすく、失敗もしにくいです。

04. 噛み合うタイル(エッシャー風)

噛み合うタイル(エッシャー風)
正方形の1辺を波打たせて、その同じ曲線を反対側の辺にコピーする。
これだけで、ずらして並べると必ずカチッと噛み合うタイルになります。
中身はただの“変形した正方形”。

1. 完成コード(PythonSCAD)

# ==========================================
# 噛み合うタイル(エッシャー法)
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import pi, sin

# --- パラメータ設定 (単位: mm) ---
tile_size      = 40.0   # 正方形の基本サイズ
tile_thickness = 3.0    # タイルの厚み
wave_amp       = 5.0    # 波の振幅
wave_cycles    = 2      # 1辺に入る波の数
point_count    = 80     # 波線を作る点の数
fn_val         = 60     # 円・球用の分割数(今回は予備)
eps            = 0.1    # チラつき防止用

grid_x         = 3      # 横方向に並べる数
grid_y         = 3      # 縦方向に並べる数


# --- 関数(モジュール)定義 ---

def wave_y(x, size, amp, cycles):
    """
    正方形の辺を波打たせるための関数。
    x=-size/2 と x=+size/2 で波が0になるようにして、
    端点が元の正方形の角に戻るようにする。
    """
    t = (x + size / 2) / size
    return amp * sin(2 * pi * cycles * t)


def create_escher_tile_2d(size, amp, cycles, n):
    """
    噛み合う2Dタイル形状を作る。
    
    下辺:
        y = -size/2 + wave_y(x)
    
    上辺:
        y = +size/2 + wave_y(x)
    
    上辺は下辺をそのままY方向に size だけコピーした形。
    そのため、上下に並べると必ずカチッと噛み合う。
    """
    bottom_points = []
    top_points = []

    for i in range(n + 1):
        x = -size / 2 + size * i / n
        w = wave_y(x, size, amp, cycles)

        bottom_points.append([x, -size / 2 + w])
        top_points.append([x,  size / 2 + w])

    # 輪郭を反時計回りに作る
    # 下辺を左→右、上辺を右→左にたどる
    points = bottom_points + list(reversed(top_points))

    return polygon(points)


def create_escher_tile_3d(size, thickness, amp, cycles, n):
    """
    2Dの波形タイルをZ方向に押し出して3D化する。
    linear_extrude は底面を Z=0 に置く。
    """
    tile_2d = create_escher_tile_2d(size, amp, cycles, n)
    tile_3d = linear_extrude(tile_2d, height=thickness)
    return tile_3d


def create_tile_grid(tile_obj, size, gx, gy):
    """
    タイルを格子状に並べる。
    上下の辺が同じ波形なので、Y方向に size ずつ動かすと噛み合う。
    """
    result = None

    for ix in range(gx):
        for iy in range(gy):
            x_pos = (ix - (gx - 1) / 2) * size
            y_pos = (iy - (gy - 1) / 2) * size

            one_tile = translate(tile_obj, [x_pos, y_pos, 0])

            # 見やすいように市松模様っぽく色を変える
            if (ix + iy) % 2 == 0:
                one_tile = color(one_tile, "tomato")
            else:
                one_tile = color(one_tile, "gold")

            if result is None:
                result = one_tile
            else:
                result = result + one_tile

    return result


# --- メイン処理 ---

# 1. 変形した正方形タイルを作る
base_tile = create_escher_tile_3d(
    tile_size,
    tile_thickness,
    wave_amp,
    wave_cycles,
    point_count
)

# 2. 並べて、噛み合いを確認する
result = create_tile_grid(base_tile, tile_size, grid_x, grid_y)

# 3. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-06 170801.png


2. 考え方

このタイルは、元はただの正方形です。

ポイントは、

下の辺を波打たせる
↓
同じ波形を上の辺にコピーする
↓
上下に並べると、山と谷が完全に一致する

ということです。

コードでは下辺を、

y = -size / 2 + wave_y(x)

上辺を、

y = size / 2 + wave_y(x)

として作っています。

つまり、上辺は下辺をそのまま上に tile_size mm 移動しただけです。

だから、タイルをY方向に tile_size ずつ並べると、境界がぴったり一致します。


3. 改造ポイント

波を大きくしたい場合:

wave_amp = 8.0

波の数を増やしたい場合:

wave_cycles = 3

タイルを大きくしたい場合:

tile_size = 60.0

厚みを変えたい場合:

tile_thickness = 5.0

4. 学習ポイント

これはエッシャー風タイルの基本アイデアです。

重要なのは、

片側を変形したら、反対側にも同じ変形をコピーする

ことです。

今回は上下の辺だけを波にしましたが、同じ考え方で左右の辺も変形すると、もっと複雑な「完全に敷き詰められるタイル」が作れます。

05. Truchet タイル模様(四半円が流れるパターン)

Truchet(曲線がつながって流れる)
各タイルに四半円の弧を2本だけ入れて、向きをランダムに選ぶ。
たったこれだけで弧が勝手につながって、ループや輪っかや
小径が流れる模様になります。

1. 完成コード(PythonSCAD)

# ==========================================
# Truchet タイル模様(四半円が流れるパターン)
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
import random

# --- パラメータ設定 (単位: mm) ---
tile_size   = 12.0     # 1枚のタイルの一辺
cols        = 10       # X方向のタイル数
rows        = 10       # Y方向のタイル数

base_h      = 1.2      # 土台の厚み
curve_h     = 1.0      # 曲線の盛り上がり高さ
curve_w     = 2.2      # 曲線の太さ

fn_val      = 64       # 円弧のなめらかさ
eps         = 0.05     # 接合のチラつき防止
random_seed = 7        # 模様を固定する乱数シード(変えると模様も変わる)

base_color  = "ivory"
curve_color = "black"

# --- 関数(モジュール)定義 ---

def add_all(shape_list):
    """リスト内の形状を + 演算子で順番に結合する"""
    result = shape_list[0]
    for i in range(1, len(shape_list)):
        result = result + shape_list[i]
    return result


def create_quarter_ring_2d(radius, width, fn):
    """
    原点を中心にした「第1象限」の四半円リングを作る。
    つまり、X>=0, Y>=0 側だけを残した円弧。
    """
    outer_r = radius + width / 2
    inner_r = radius - width / 2

    outer_circle = circle(r=outer_r, fn=fn)
    inner_circle = circle(r=inner_r, fn=fn)

    ring = outer_circle - inner_circle

    # 第1象限だけを残すための2Dカッター
    # 2D形状なので translate は [X, Y] の2要素で指定
    quadrant_mask = translate(
        square([2 * outer_r + eps, 2 * outer_r + eps], center=True),
        [outer_r, outer_r]
    )

    return intersection(ring, quadrant_mask)


def create_truchet_tile_2d(tile_s, curve_width, orientation, fn):
    """
    1枚のTruchetタイルの2D曲線を作る。
    orientation = 0:
        左下コーナーと右上コーナーを中心にした弧
    orientation = 1:
        左上コーナーと右下コーナーを中心にした弧
    """
    arc_r = tile_s / 2
    q_arc = create_quarter_ring_2d(arc_r, curve_width, fn)

    if orientation == 0:
        # 左下:第1象限の弧
        arc_1 = translate(q_arc, [-tile_s / 2, -tile_s / 2])

        # 右上:第3象限の弧
        arc_2 = translate(
            rotate(q_arc, [0, 0, 180]),
            [tile_s / 2, tile_s / 2]
        )

    else:
        # 左上:第4象限の弧
        arc_1 = translate(
            rotate(q_arc, [0, 0, -90]),
            [-tile_s / 2, tile_s / 2]
        )

        # 右下:第2象限の弧
        arc_2 = translate(
            rotate(q_arc, [0, 0, 90]),
            [tile_s / 2, -tile_s / 2]
        )

    return arc_1 + arc_2


def create_base(cols_count, rows_count, tile_s, h):
    """全タイルを載せる1枚の土台を作る"""
    base_w = cols_count * tile_s
    base_l = rows_count * tile_s

    base_2d = square([base_w, base_l], center=True)
    return linear_extrude(base_2d, height=h)


def create_truchet_curves(cols_count, rows_count, tile_s, curve_width, h, fn):
    """ランダム向きのTruchet曲線を格子状に並べる"""
    random.seed(random_seed)

    curve_solids = []

    for ix in range(cols_count):
        for iy in range(rows_count):
            x = (ix - (cols_count - 1) / 2) * tile_s
            y = (iy - (rows_count - 1) / 2) * tile_s

            orientation = random.randint(0, 1)

            tile_curve_2d = create_truchet_tile_2d(
                tile_s,
                curve_width,
                orientation,
                fn
            )

            tile_curve_3d = linear_extrude(
                tile_curve_2d,
                height=h + eps
            )

            # 3D形状なので translate は [X, Y, Z] の3要素
            placed_curve = translate(
                tile_curve_3d,
                [x, y, base_h - eps]
            )

            curve_solids.append(placed_curve)

    return add_all(curve_solids)


# --- メイン処理 ---

# 1. 土台を作る
base = create_base(cols, rows, tile_size, base_h)

# 2. Truchet曲線を作る
curves = create_truchet_curves(
    cols,
    rows,
    tile_size,
    curve_w,
    curve_h,
    fn_val
)

# 3. 色をつける
colored_base = color(base, base_color)
colored_curves = color(curves, curve_color)

# 4. 結合して表示
result = colored_base + colored_curves

show(result)

スクリーンショット 2026-09-06 171429.png

2. 使い方のポイント

  • random_seed の値を変えると、模様のランダム配置が変わります
  • cols, rows を増やすと、より大きなTruchetパターンになります
  • curve_w を太くすると、よりポップな模様になります
  • fn_val を大きくすると円弧がなめらかになります

このモデルでは、各タイルに 四半円の弧を2本だけ 入れています。
向きは2種類しかありませんが、隣のタイルと自然につながるので、流れるようなループ模様が生まれます。

06. ペンローズ・タイル風メダリオン壁アート

ペンローズ(二度と同じ並びにならない)
黄金比のルールで図形を分割していくと、
“ペンローズ・タイル”という、永遠に繰り返さない不思議な模様ができます。
5回対称の星がびっしり。
規則はあるのに、決して周期的に繰り返さない。
メダリオン型の壁アート。

1. 完成コード(PythonSCAD)

# ==========================================
# ペンローズ・タイル風メダリオン壁アート
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import pi, sin, cos, sqrt

# --- パラメータ設定 (単位: mm) ---
medallion_radius = 55.0     # ペンローズ模様部分の半径
rim_width        = 6.0      # 外周フレームの幅
base_height      = 2.0      # 背板の厚み
tile_height      = 1.0      # タイルの盛り上がり
rim_height       = 2.0      # 外周フレームの高さ
subdivide_steps  = 5        # 分割回数(4:軽め / 5:細かい / 6:かなり重い)
gap_ratio        = 0.025    # タイル同士のすき間(黒い目地を見せる)
hanger_hole_r    = 2.2      # 壁掛け用穴の半径
fn_val           = 100      # 円のなめらかさ
eps              = 0.1      # チラつき防止用微小値

# 黄金比 φ
phi = (1.0 + sqrt(5.0)) / 2.0


# --- 基本ベクトル関数(2D) ---

def add_all(shapes):
    """リスト内の形状を + 演算子で結合する"""
    result = shapes[0]
    for s in shapes[1:]:
        result = result + s
    return result


def lerp(p1, p2, t):
    """p1からp2へ t の割合だけ進んだ点を返す"""
    return [
        p1[0] + (p2[0] - p1[0]) * t,
        p1[1] + (p2[1] - p1[1]) * t
    ]


def point_on_circle(r, angle):
    """半径r、角度angleの円周上の点"""
    return [
        r * cos(angle),
        r * sin(angle)
    ]


def shrink_triangle(points, ratio):
    """
    三角形を少し内側に縮める。
    黒い背板が目地として見えるようにするため。
    """
    cx = (points[0][0] + points[1][0] + points[2][0]) / 3.0
    cy = (points[0][1] + points[1][1] + points[2][1]) / 3.0

    new_points = []
    for p in points:
        new_points.append([
            cx + (p[0] - cx) * (1.0 - ratio),
            cy + (p[1] - cy) * (1.0 - ratio)
        ])
    return new_points


# --- ペンローズ分割関数 ---

def create_initial_triangles(radius):
    """
    10個の三角形を中心のまわりに並べる。
    ここから黄金比ルールで分割していく。
    """
    triangles = []

    for i in range(10):
        angle1 = (2 * i - 1) * pi / 10.0
        angle2 = (2 * i + 1) * pi / 10.0

        a = [0.0, 0.0]
        b = point_on_circle(radius, angle1)
        c = point_on_circle(radius, angle2)

        # 向きを交互に入れ替えることで5回対称の種を作る
        if i % 2 == 0:
            temp = b
            b = c
            c = temp

        triangles.append(["red", a, b, c])

    return triangles


def subdivide_penrose(triangles):
    """
    黄金比 φ を使って三角形を分割する。
    red / blue の2種類の三角形が、毎回決まったルールで増えていく。
    """
    new_triangles = []

    for tri in triangles:
        kind = tri[0]
        a = tri[1]
        b = tri[2]
        c = tri[3]

        if kind == "red":
            # red 三角形の分割
            p = lerp(a, b, 1.0 / phi)

            new_triangles.append(["red",  c, p, b])
            new_triangles.append(["blue", p, c, a])

        else:
            # blue 三角形の分割
            q = lerp(b, a, 1.0 / phi)
            r = lerp(b, c, 1.0 / phi)

            new_triangles.append(["blue", r, c, a])
            new_triangles.append(["blue", q, r, b])
            new_triangles.append(["red",  r, q, a])

    return new_triangles


def create_penrose_tiles(radius, steps, gap, h):
    """
    ペンローズ風タイル群を3D化する。
    red系とblue系で色を分ける。
    """
    triangles = create_initial_triangles(radius)

    for i in range(steps):
        triangles = subdivide_penrose(triangles)

    gold_tiles = []
    blue_tiles = []

    for tri in triangles:
        kind = tri[0]
        pts = [tri[1], tri[2], tri[3]]
        pts = shrink_triangle(pts, gap)

        tile_2d = polygon(points=pts)
        tile_3d = linear_extrude(tile_2d, height=h)
        tile_3d = translate(tile_3d, [0, 0, base_height])

        if kind == "red":
            gold_tiles.append(tile_3d)
        else:
            blue_tiles.append(tile_3d)

    gold_group = add_all(gold_tiles)
    blue_group = add_all(blue_tiles)

    gold_group = color(gold_group, "gold")
    blue_group = color(blue_group, "royalblue")

    return gold_group + blue_group


def create_backing_and_frame(radius, frame_w, base_h, frame_h, fn):
    """
    黒い背板と外周フレームを作る。
    タイルのすき間から黒が見えて、ステンドグラス風になる。
    """
    outer_r = radius + frame_w
    inner_r = radius + 1.0

    backing = cylinder(h=base_h, r=outer_r, center=False, fn=fn)
    backing = color(backing, "black")

    frame_2d = circle(r=outer_r, fn=fn) - circle(r=inner_r, fn=fn)
    frame = linear_extrude(frame_2d, height=frame_h)
    frame = translate(frame, [0, 0, base_h])
    frame = color(frame, "darkgoldenrod")

    return backing + frame


def create_cutter(radius, frame_w, total_h, hole_r, fn):
    """
    壁掛け用の穴カッター。
    Z方向に少し長くして、Zファイティングを防ぐ。
    """
    outer_r = radius + frame_w

    hole_y = outer_r - frame_w * 0.6
    hole = cylinder(h=total_h + 2 * eps, r=hole_r, center=True, fn=fn)
    hole = translate(hole, [0, hole_y, total_h / 2.0])

    return hole


# --- メイン処理 ---

# 1. 背板・フレーム
base_and_frame = create_backing_and_frame(
    medallion_radius,
    rim_width,
    base_height,
    rim_height,
    fn_val
)

# 2. ペンローズ風タイル
tiles = create_penrose_tiles(
    medallion_radius,
    subdivide_steps,
    gap_ratio,
    tile_height
)

# 3. 足す形状をまとめる
body = base_and_frame + tiles

# 4. カッターを作る
total_height = base_height + max(tile_height, rim_height)
cutter = create_cutter(
    medallion_radius,
    rim_width,
    total_height,
    hanger_hole_r,
    fn_val
)

# 5. 一括くり抜き
result = body - cutter

# 6. 表示
show(result)

スクリーンショット 2026-09-06 172553.png


2. 作品のポイント

これは「ペンローズ・タイル風」のメダリオン型壁アートです。

  • 黄金比 phi = (1 + √5) / 2 を使って三角形を分割
  • 10個の三角形からスタートして、5回対称の模様を作成
  • subdivide_steps = 5 により、かなり細かい非周期的パターンになります
  • タイルを少し縮めて、黒い背板が「目地」のように見えるようにしています
  • 外周には金色のフレームを追加
  • 上側に壁掛け用の穴を追加

3. 調整して遊べるパラメータ

軽くしたい場合:

subdivide_steps = 4

もっと細かくしたい場合:

subdivide_steps = 6

ただし、分割回数を増やすとタイル数がかなり増えるので、表示やレンダリングが重くなります。

色を変えたい場合:

gold_group = color(gold_group, "orange")
blue_group = color(blue_group, "teal")

などに変更できます。


4. 学習ポイント

この作品では、次の3つを学べます。

  1. 黄金比

    • ペンローズ・タイルの分割ルールに登場する重要な比率です
  2. 再帰的・反復的な図形生成

    • 小さなルールを何度も適用して、複雑な模様を作ります
  3. 2Dから3Dへの押し出し

    • polygon() で平面図形を作り、linear_extrude() で厚みを付けています

このように、「数学のルール」をそのまま3Dモデルにできるのが PythonSCAD の面白いところです。

07. タンブリングブロック風 立体レリーフ

タンブリングブロック(積み木が並ぶ立体)
小さな立方体を“角を上”にして、すき間なく敷き詰める。
すると面ごとに光の当たり方が変わって、
本物の積み木が並んだような立体レリーフ

1. 完成コード(PythonSCAD)

# ==========================================
# タンブリングブロック風 立体レリーフ
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import sqrt

# --- パラメータ設定 (単位: mm) ---
cube_side      = 8.0      # 見かけ上の小さな立方体1辺の長さ
columns        = 7        # 横方向の個数
rows           = 5        # 縦方向の個数

base_thickness = 2.0      # 土台の厚み
relief_height  = 1.2      # 菱形パネルの基本高さ
height_step    = 0.25     # 面ごとの高さ差(影を出すため)
margin         = 6.0      # 土台の余白

fn_val         = 48       # 円形では使わないが、拡張用
eps            = 0.1      # チラつき防止用

# --- 色設定 ---
color_top   = "gainsboro"   # 明るい面
color_right = "gray"        # 中間の面
color_left  = "dimgray"     # 暗い面
color_base  = "black"       # 土台


# --- 関数(モジュール)定義 ---

def create_base(w, d, h):
    """土台を生成する。XYは原点中心、Zは底面を0にそろえる"""
    base = cube([w, d, h], center=True)
    base = translate(base, [0, 0, h / 2])
    return color(base, color_base)


def create_rhombus_face(points, height, face_color, x_pos, y_pos, z_pos):
    """
    菱形の2D面を押し出して、薄い立体パネルにする。
    points は必ず [x, y] のリストで指定する。
    """
    face_2d = polygon(points=points)
    face_3d = linear_extrude(face_2d, height=height)
    face_3d = translate(face_3d, [x_pos, y_pos, z_pos])
    return color(face_3d, face_color)


def create_tumbling_block(x_pos, y_pos, side):
    """
    1つの「角を上にした立方体」に見える模様を作る。
    実体は3枚の菱形パネルで構成する。
    """
    half_w = sqrt(3) * side / 2

    # 立方体に見える正六角形を、3枚の菱形に分割する
    top_points = [
        [0, 0],
        [half_w, side / 2],
        [0, side],
        [-half_w, side / 2],
    ]

    right_points = [
        [0, 0],
        [half_w, -side / 2],
        [half_w, side / 2],
    ]

    left_points = [
        [0, 0],
        [0, -side],
        [-half_w, -side / 2],
        [-half_w, side / 2],
    ]

    # 面ごとに少し高さを変えると、実際の影も出やすい
    top_face = create_rhombus_face(
        top_points,
        relief_height + height_step,
        color_top,
        x_pos,
        y_pos,
        base_thickness
    )

    right_face = create_rhombus_face(
        right_points,
        relief_height,
        color_right,
        x_pos,
        y_pos,
        base_thickness
    )

    left_face = create_rhombus_face(
        left_points,
        relief_height - height_step,
        color_left,
        x_pos,
        y_pos,
        base_thickness
    )

    return top_face + right_face + left_face


def add_object(current_obj, new_obj):
    """+ 演算子で順番に形状を結合するための補助関数"""
    if current_obj is None:
        return new_obj
    return current_obj + new_obj


def create_tumbling_pattern(cols, row_count, side):
    """
    タンブリングブロックをすき間なく敷き詰める。
    正六角形タイルの配置:
      X方向ピッチ = sqrt(3) * side
      Y方向ピッチ = 1.5 * side
    """
    x_pitch = sqrt(3) * side
    y_pitch = 1.5 * side

    pattern = None

    for row in range(row_count):
        for col in range(cols):
            # 1段おきに半ピッチずらすことで、すき間なく並ぶ
            x_offset = (row % 2) * x_pitch / 2

            x_pos = (col - (cols - 1) / 2) * x_pitch + x_offset - x_pitch / 4
            y_pos = (row - (row_count - 1) / 2) * y_pitch

            block = create_tumbling_block(x_pos, y_pos, side)
            pattern = add_object(pattern, block)

    return pattern


# --- メイン処理 ---

# 1. 土台サイズの計算
hex_width = sqrt(3) * cube_side
pattern_width = (columns + 0.5) * hex_width + margin * 2
pattern_depth = (rows - 1) * 1.5 * cube_side + 2 * cube_side + margin * 2

# 2. 土台を作成
base = create_base(pattern_width, pattern_depth, base_thickness)

# 3. タンブリングブロック模様を作成
pattern = create_tumbling_pattern(columns, rows, cube_side)

# 4. 土台とレリーフを結合
result = base + pattern

# 5. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-06 173309.png


2. 形の考え方

このモデルは、実際に小さな立方体を積んでいるのではなく、3枚の菱形パネルで1つの立方体に見えるようにしています。

1つのブロックは次の3面でできています。

  • 明るい面:gainsboro
  • 中間の面:gray
  • 暗い面:dimgray

この3色を使うことで、光が左上から当たっているように見えます。

さらに、3つの面の高さを少し変えています。

relief_height + height_step
relief_height
relief_height - height_step

こうすると、単なる平面模様ではなく、少しだけ本物の影が出る立体レリーフになります。


3. 改造ポイント

模様を細かくしたい場合:

cube_side = 6.0

全体を大きくしたい場合:

columns = 10
rows = 8

レリーフ感を強くしたい場合:

relief_height = 2.0
height_step = 0.4

色を木材風にしたい場合は、例えば次のように変えると雰囲気が出ます。

color_top   = "burlywood"
color_right = "peru"
color_left  = "saddlebrown"

タンブリングブロックは、数学的には正六角形の敷き詰め菱形分割を組み合わせたパターンです。高校数学のベクトルや平面図形ともつながる、とてもよい題材です。

参考資料

📄 ライセンスと「オープンソース」の文化について

本記事のソースコードは、すべて MIT ライセンス で公開しています。

  • 高校生のみなさんへ 🚀
    プログラミングの世界には、「自分が作った便利な仕組みをみんなに共有し、お互いに助け合って技術を発展させる(オープンソース)」という素晴らしい文化があります。このコードも、作者の名前(クレジット)さえ残してもらえれば、改造して学校の課題に使ったり、自分のアプリに組み込んだりして自由に無料で使ってOKです!
    ぜひこのコードをベースに、自分だけの新しいプログラムを作って挑戦してみてください。
  • 免責事項 ⚠️
    本記事およびコードは個人の研究・検証に基づくものであり、所属する組織の公式見解ではありません。自由に使っていただけますが、利用に伴ういかなる損害についても執筆者は責任を負いかねますので、すべて「自己責任(無保証)」の範囲内で楽しく学んでくださいね。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?