本ガイドの結論を先に言う。①〜④はPILだけで「AIに一発で見抜かれる素人臭」を消せる。到達点は「商業誌のプロ写植と完全同等」ではなく「素材として金を取れる説得力ライン」=80点台。残りの“筆致の生っぽさ”はPILの限界で、第6章のフリー素材合成で補う。
PILで作ったSFXが安く見える根本原因はフォントでも色でもない。「1回のdraw.text()で全部終わらせている」からだ。プロのレタリングは1文字に最低7層(影→外縁→中縁→本体グラデ→にじみ→ベベル→テクスチャ)を重ねている。素人は1層。この層数の差が、AIが0.2秒で見抜く「平面臭」の正体である。
具体的な処方箋は4つ。これを全部やると、同じフォント・同じ文字列でも見違える。
漫画用描き文字(SFX/オノマトペ)を透過PNG素材として売る市場は、BOOTH・DLsite・dddFont等で確立している。dddFontは「漫画的表現ができる素材をドドドっとストック」を掲げる専門ストアで、描き文字素材が単体商品として流通していることを示す[1]。Clip Studio ASSETSでも効果音・描き文字のブラシ/素材が大量に配布・販売されている[2]。
| 区分 | 規模感/相場 | 本テーマとの関係 |
|---|---|---|
| 漫画フォント(SFX向け) | 無料〜有料。プロ用英字SFXフォントBlambotは無料〜$40[3] | 素材の「素」になる。崩しは自分で足す |
| 描き文字PNG素材(BOOTH等) | 1セット300〜1,500円が中心帯 | 本ガイドのPIL量産品の販売先 |
| クリスタ用ブラシ/素材 | 無料配布多・有料は100〜数百円 | 競合。AI制作者にはハードル(クリスタ前提) |
市場は巨大ではないが、「AIエロ漫画量産勢が大量にいて、その全員が写植で詰まっている」という需要構造がある。自分の制作で使うコードを素材化して横流しできるのが旨味だ。
プロのSFXが安く見えないのは、文字に複数の物理的・視覚的矛盾を同時に抱えさせているから。商業レタリングの定石(線の太さで音の重さ、字形で音質を表す[4]、白フチで背景から浮かせる[5])を、素人NG/プロ正解の対比で分解する。
根本思想は「1回の描画で済ませない」。すべて別レイヤーで作り、合成順序とマスクで制御する。下から上の標準7層:
| # | レイヤー | PIL実装 | 効く理由(視覚/物理) |
|---|---|---|---|
| 1 | ドロップシャドウ | 黒文字マスク→GaussianBlur→offset→不透明度65% | 文字を紙から浮かせ、奥行きを与える |
| 2 | 外黒縁 | stroke_width大 or 多重オフセット描画で膨張 | どんな背景でも視認性100%を担保[5] |
| 3 | 中白縁 | 本体1.3〜1.5倍マスクに白、不透明度80〜90% | 黒と本体の間にコントラストの段差を作る |
| 4 | 本体グラデ | L mask + Image.composite(縦グラデ) | 平坦なベタ塗りを脱し体温/湿りを表現 |
| 5 | にじみ | 輪郭を1pxぼかし→不透明度15〜25%で本体下 | インクが紙に染みる物理を再現 |
| 6 | ベベル明部 | 左上1pxずらした明マスクを加算 | インクの盛り上がり=立体錯覚 |
| 7 | テクスチャ | ImageChops.multiply/overlayでノイズ/紙目[7] | デジタル特有のノッペリを破壊 |
ImageDraw.text単発だと必ず平坦になる。マスク経由でグラデを切り抜くのが定石。Image.composite(image1, image2, mask)はLモードマスクの画素値0〜255で2画像をブレンドする[8]。
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops
def text_mask(size, font, text, xy):
m = Image.new("L", size, 0)
ImageDraw.Draw(m).text(xy, text, font=font, fill=255)
return m
def vertical_gradient(size, top, bottom):
# 上=明るいピンク, 下=暗い紫 への縦グラデ
w, h = size
grad = Image.new("RGB", (1, h))
for y in range(h):
t = y / max(h - 1, 1)
grad.putpixel((0, y), tuple(int(top[i]*(1-t)+bottom[i]*t) for i in range(3)))
return grad.resize((w, h))
W, H = 900, 360
font = ImageFont.truetype("GenEiChikugoMin.ttf", 220) # 第5章のフォント
mask = text_mask((W,H), font, "ビクンッ", (40, 30))
grad = vertical_gradient((W,H), top=(255,150,190), bottom=(180,70,140)).convert("RGBA")
body = Image.composite(grad, Image.new("RGBA",(W,H),(0,0,0,0)), mask)
エロでは上をやや明るく下を暗くすると「湿った熱」が出る。逆向きグラデは冷たく見えるので避ける。
重ね順は絶対に外黒→中白→本体。逆だと白縁が浮く。Pillowのstroke_widthはきれいな膨張に使えるが[9]、3層以上は別マスクを膨らませて重ねる。Pillow単体の膨張はMaxFilterか多重オフセット描画で代用する。
def expand_mask(mask, px):
# 8方向オフセット描画で擬似膨張(MaxFilterより輪郭が滑らか)
out = mask.copy()
for dx in range(-px, px+1):
for dy in range(-px, px+1):
if dx*dx + dy*dy <= px*px:
out = ImageChops.lighter(out, ImageChops.offset(mask, dx, dy))
return out
outer = expand_mask(mask, 14) # 外黒
mid = expand_mask(mask, 7) # 中白
layer = Image.new("RGBA",(W,H),(0,0,0,0))
black = Image.new("RGBA",(W,H),(25,8,16,255)) # 真っ黒でなく赤み黒
white = Image.new("RGBA",(W,H),(255,255,255,235)) # 中白は不透明度235
layer = Image.composite(black, layer, outer)
layer = Image.composite(white, layer, mid)
layer.alpha_composite(body) # 本体グラデを最前面
GaussianBlurはradius=標準偏差[10]。影は完全な黒を避け、わずかに赤み・低不透明度に。
shadow = expand_mask(mask, 3).filter(ImageFilter.GaussianBlur(2.2))
shadow = shadow.point(lambda v: int(v*0.68)) # 不透明度68%
sh_layer = Image.new("RGBA",(W,H),(0,0,0,0))
sh_layer.paste((20,5,12,255), mask=shadow)
sh_layer = ImageChops.offset(sh_layer, 3, 4) # 右下3,4px
canvas = Image.new("RGBA",(W,H),(0,0,0,0))
canvas.alpha_composite(sh_layer) # 影が最下層
canvas.alpha_composite(layer)
# 本体マスクを左上にずらした差分=明部、右下=暗部
hi = ImageChops.subtract(mask, ImageChops.offset(mask, 2, 2)) # 左上ハイライト
sh = ImageChops.subtract(mask, ImageChops.offset(mask, -2,-2)) # 右下シェード
hi_l = Image.new("RGBA",(W,H),(0,0,0,0)); hi_l.paste((255,255,255,120), mask=hi)
sh_l = Image.new("RGBA",(W,H),(0,0,0,0)); sh_l.paste((120,40,80,110), mask=sh)
canvas.alpha_composite(sh_l); canvas.alpha_composite(hi_l)
ノイズや紙目を乗算すると一発でデジタル臭が消える。Image.effect_noiseでガウシアンノイズを生成し[11]、ImageChops.multiply/overlayで重ねる[7]。乗算は文字マスクで切り抜いてから掛けること(背景まで暗くしない)。
noise = Image.effect_noise((W,H), 22).convert("RGB") # 微細ノイズ
noise = noise.point(lambda v: 180 + (v-128)//3) # コントラスト弱め
tex_rgb = ImageChops.multiply(canvas.convert("RGB"), noise) # 乗算
canvas = Image.composite(tex_rgb.convert("RGBA"), canvas, mask) # 文字部のみ適用
Pillowのtext()は1文字単位の歪みを直接サポートしない[12]。1文字ずつ別imageに描いてrotate→貼るのが定石。小文字は縮小、語尾は横に潰す。
import random
def lay_chars(text, font_path, base=200):
out = Image.new("RGBA",(len(text)*base, base*2),(0,0,0,0)); x=base*0.3
for ch in text:
sz = base*(0.7 if ch in "っッぁぃぅぇぉー…" else random.uniform(0.95,1.25))
f = ImageFont.truetype(font_path, int(sz))
tmp = Image.new("RGBA",(int(sz*2),int(sz*2)),(0,0,0,0))
ImageDraw.Draw(tmp).text((sz*0.5,sz*0.4), ch, font=f, fill=(255,255,255,255))
rot = tmp.rotate(random.uniform(-15,15), resample=Image.BICUBIC, expand=True)
out.alpha_composite(rot,(int(x),int(base+random.uniform(-base*0.15,base*0.15))))
x += sz*random.uniform(0.72,0.9) # 詰めて息詰まり感
return out.crop(out.getbbox())
擬似入り抜き=同じ文字を縦に微小オフセットで2〜3回重ね描き(縦画だけ太く見える)。アフィン/パース変形(Image.transform)で語尾を台形に潰すとさらに崩せる[13]。
「フォントが安っぽい」の8割は本文用ゴシックをSFXに流用していること。SFXは専用の太く崩れた書体を使い、そこに第4章の加工を足す。崩しがあるフォントを下地にすると、PIL加工の効きが倍増する。
| フォント | 用途 | 商用 | R18 | 配布元 |
|---|---|---|---|---|
| 源界明朝 | 絶頂/狂気/イキ狂い系SFX。源ノ明朝を破壊した崩し | 可(SIL OFL 1.1) | 可 | flopdesign[14] |
| オとマのペ(Otomanopee) | 汎用オノマトペ。漫画描き文字をイメージ。ひらカナ漢字一部 | 可 | 可 | Google Fonts/goodfreefonts[15] |
| 効果音フォント | カタカナSFX。モーション線つき動的書体 | 可 | 要確認 | goodfreefonts[15] |
| カワサキマッドドッグ | 怒り/激しい爆発系。超極太カタカナ | 可 | 要確認 | goodfreefonts[15] |
| プぷプ | 少女漫画/可愛い系SFX。先細りデザイン | 可 | 要確認 | goodfreefonts[15] |
| たぬき油性マジック | 手書き風の汎用喘ぎ/落書き感SFX | 可 | 可 | たぬき侍/まとめ[16] |
安っぽさの最大原因は重力と表面張力の無視。正しい構造:上が細く下が膨らむ涙型/最上部に鋭い小ハイライト/下部に薄い反射/外縁がわずかに暗い。
def draw_drop(size=120):
img = Image.new("RGBA",(size,int(size*1.6)),(0,0,0,0))
d = ImageDraw.Draw(img)
cx = size//2
# 1) 涙型: 上の小円 + 下の大楕円を縦に重ねる
d.ellipse([cx-size*0.18,0,cx+size*0.18,size*0.5], fill=(210,120,170,210)) # 細い上部
d.ellipse([cx-size*0.42,size*0.4,cx+size*0.42,size*1.55], fill=(210,120,170,210)) # 膨らむ下部
# 2) 外縁を暗く(表面張力)
edge = img.split()[3].filter(ImageFilter.FIND_EDGES)
dk = Image.new("RGBA",img.size,(0,0,0,0)); dk.paste((120,40,90,140),mask=edge)
img.alpha_composite(dk)
# 3) 上部に鋭い小ハイライト
d.ellipse([cx-size*0.10,size*0.12,cx+size*0.02,size*0.34], fill=(255,255,255,235))
# 4) 下部に薄い反射
d.ellipse([cx-size*0.18,size*1.15,cx+size*0.18,size*1.42], fill=(255,255,255,70))
return img
#FF00FF等の単色ドピンクでベタ塗り。安っぽさの極み。均一な線幅=入り抜きが無い=「ベクター描画」とバレる。PILで入り抜きを疑似再現する。
def speech_bubble(draw, box, lw=6):
# 同一楕円を太→細で多重描画して入り抜きを疑似再現
for i,(w,a) in enumerate([(lw+3,90),(lw+1,150),(lw,255)]):
col=(30,15,25,a)
draw.ellipse(box, outline=col, width=w)
# 角(=楕円の左右端)を少し太く残すと手描き感
| シナリオ | 前提(推定) | 月次 |
|---|---|---|
| 悲観 | 1セット500円 × 月3本 | 約1,500円 |
| 中央 | 1セット700円 × 月15本 + 自作漫画への内製活用 | 約10,500円+制作時短 |
| 楽観 | 定番セット化し常時複数SKU、月50本 | 約35,000円 |
いずれも筆者推定。素材単体の売上は小さい。本命は「自分のエロ漫画の写植品質が上がって本編が売れる」こと=素材販売はおまけ・露出導線と捉えるのが現実的。
正直に言う。PILでここまでやっても「プロの筆致の生っぽさ」には届かない。プロは筆毛の流れと紙へのインク染みを同時制御している。PILで到達できるのは「視覚的説得力(80点台)」まで。残りはこう補う:
| 限界 | 補い方 |
|---|---|
| 筆の擦れ・かすれの自然さ | かすれブラシ画像(フリーのグランジPNG)をmultiplyで乗算合成。Clip Studio ASSETSの無料かすれ素材を画像として読み込む[2] |
| 本物の手描きSFX書体 | プロ用英字SFXフォント(Blambot無料枠)を英字SFXに併用[3]。日本語は崩しフォント(第5章)+PIL加工 |
| 曲線の自由度(PILの線が硬い) | aggdrawやpycairoでアンチエイリアス曲線を描く(既存DR参照)。しっぽ/集中線が滑らかに |
| 紙テクスチャの質 | フリーの和紙/トーン紙テクスチャPNGをoverlayで重ねる(自前ノイズより自然) |
rotateは必ずresample=Image.BICUBIC+expand=True。| 期間 | やること | 到達点 |
|---|---|---|
| Day1-5 | 第5章フォント3種DL+ライセンス確認。7層レイヤリングの最小実装(影+多重縁+グラデ) | 平面臭が消える(25→55点) |
| Day6-12 | ベベル・にじみ・テクスチャ追加。字形崩し関数(回転/サイズ/ジッター) | 立体感と手作り感(→70点) |
| Day13-20 | しずく/ハート/フキダシ多重線を物理実装。音質別プリセット5種 | 装飾が稚拙でなくなる(→80点) |
| Day21-26 | フリーかすれ/紙目素材合成でPIL限界を補完。4AIに再採点 | 素材として売れる説得力(→83点) |
| Day27-30 | サンプルグリッド+実使用例+Before/After作成、BOOTH/DLsite出品 | 初版リリース |
技術23:7層レイヤリング・全コード骨子が実際に動く設計で公式ドキュメント裏取り済。-2はPILの曲線限界を完全には埋めきれず外部補完依存。マーケ22:収益試算は推定明記だが市場規模の一次統計が薄い(描き文字素材は公開統計が乏しいため)。法務23:全フォントを実在配布元+ライセンス名で明記、R18は要確認を明示。競合23:dddFont/Blambot/Clip Studio ASSETSとプロレタリング論を比較軸に。-2は具体的競合素材SKUの価格実数まで踏み込めず。