線形状に沿ってチューブ状のポリゴンメッシュを作成 (スクリプト)

Standard/Professionalのグレードの場合は、プラグインとしての「掃引-円」機能を使うことで、
線形状を中心としたチューブ形状の作成ができます。
Basic版ではこの機能は使えないため、スクリプトで補助します。


import numpy
import math

scene = xshade.scene()

#---------------------------------------.
# ゼロチェック.
#---------------------------------------.
def isZero (v):
  minV = 1e-5
  if v > -minV and v < minV:
    return True
  return False

#---------------------------------------.
# ベジェ上の位置を計算.
#---------------------------------------.
def getBezierPoint (v1Pos, v1Out, v2In, v2Pos, fPos):
  fMin = 1e-6

  rPos = [0.0, 0.0, 0.0]
  cPos = []
  cPos.append([v1Pos[0], v1Pos[1], v1Pos[2]])
  cPos.append([v1Out[0], v1Out[1], v1Out[2]])
  cPos.append([v2In[0],  v2In[1],  v2In[2]])
  cPos.append([v2Pos[0], v2Pos[1], v2Pos[2]])

  fPos2 = float(fPos)
  fPos2 = max(0.0, fPos2)
  fPos2 = min(1.0, fPos2)
  
  if isZero(fPos2):
    rPos = cPos[0]
    return rPos

  if isZero(fPos2 - 1.0):
    rPos = cPos[3]
    return rPos

  # ベジェ曲線の計算.
  t   = fPos2
  t2  = 1.0 - t
  t2d = t2 * t2
  td  = t * t
  b1  = t2d * t2
  b2  = 3.0 * t * t2d
  b3  = 3.0 * td * t2
  b4  = t * td

  for i in range(3):
    rPos[i] = b1 * cPos[0][i] + b2 * cPos[1][i] + b3 * cPos[2][i] + b4 * cPos[3][i]

  return rPos

#---------------------------------------.
# 線形状を直線の集まりに分解.
# @param[in] shape       対象形状.
# @param[in] lineDivCou  ラインの全体の分割数.
# @return ポイントの配列.
#---------------------------------------.
def getLinePoints (shape, lineDivCou):
  vCou = shape.total_number_of_control_points
  vList = []
  if shape.type != 4 or vCou < 2:  # 線形状でない場合.
    return vList
  
  divCou = lineDivCou / vCou
  if divCou < 4:
    divCou = 4
  divD = 1.0 / float(divCou)

  # ベジェをポイントで保持.
  for i in range(vCou):
    if shape.closed == False and (i + 1 >= vCou):
      break
    p1 = shape.control_point(i)
    p2 = shape.control_point((i + 1) % vCou)

    dPos = 0.0
    for j in range(divCou + 1):
      p = getBezierPoint(p1.position, p1.out_handle, p2.in_handle, p2.position, dPos)
      if (i == 0) or (i != 0 and j > 0):
        vList.append(p)
      dPos += divD

  return vList

#---------------------------------------.
# ポイントのみで構成された配列情報から、等間隔になるように再計算.
# @param[in] vList   ポイントの配列.
# @param[in] divCou  新しいラインの分割数.
# @return ポイントの配列.
#---------------------------------------.
def recalcLinePoints (vList, divCou):
  # numpyの形式に配列に格納し直す.
  vListLen = len(vList)
  if vListLen < 2:
      return []

  posA = []
  for i in range(vListLen):
      posA.append(numpy.array([vList[i][0], vList[i][1], vList[i][2]]))

  # ラインの長さを計算.
  allLen = 0.0
  lenList = []
  for i in range(vListLen - 1):
    vLen = numpy.linalg.norm(posA[i + 1] - posA[i])
    lenList.append(vLen)
    allLen += vLen

  dLen = allLen / (divCou - 1.0)
  newPosA = []
  newPosA.append([posA[0][0], posA[0][1], posA[0][2]])
  dPos = 0.0
  for i in range(vListLen - 1):
    len1 = lenList[i]

    if dPos + len1 < dLen:
      dPos += len1
      continue

    dPos2 = 0.0
    while dPos2 < len1:
      dd = (dPos2 + (dLen - dPos)) / len1
      p = (posA[i + 1] - posA[i]) * dd + posA[i]
      newPosA.append([p[0], p[1], p[2]])
      if len(newPosA) >= divCou - 1:
        break
      dd2 = dLen - dPos
      if dPos2 + dd2 + dLen > len1:
        dPos = len1 - (dPos2 + dd2)
        break
      dPos2 += dd2
      dPos = 0.0

    if len(newPosA) >= divCou - 1:
      break

  newPosA.append([posA[-1][0], posA[-1][1], posA[-1][2]])
  return newPosA

#---------------------------------------.
# ベクトルの単位ベクトルを計算.
#---------------------------------------.
def calcVecNormal (vD):
  vD2 = vD
  lenV = numpy.linalg.norm(vD2)
  if lenV != 0.0:
    vD2 /= lenV
  return vD2

#---------------------------------------.
# 指定の進行方向から行列を作成.
# @param[in] vDir  進行方向のベクトル.
# @return 進行方向をZとしたときの4x4行列を返す.
#---------------------------------------.
def calcDirToMatrix (vDir):
  vDir0 = calcVecNormal(vDir)

  m = numpy.matrix(numpy.identity(4))
  vX = numpy.array([1.0, 0.0, 0.0])
  vY = numpy.array([0.0, 1.0, 0.0])
  
  dirY = vY
  angleV = numpy.dot(vDir0, vY)
  if math.fabs(angleV) > 0.999:
    dirY = vX
  dirX = numpy.cross(vDir0, dirY)
  dirX = calcVecNormal(dirX)
  dirY = numpy.cross(dirX, vDir0)
  dirY = calcVecNormal(dirY)

  m[0, 0] = dirX[0]
  m[0, 1] = dirX[1]
  m[0, 2] = dirX[2]
  m[1, 0] = dirY[0]
  m[1, 1] = dirY[1]
  m[1, 2] = dirY[2]
  m[2, 0] = vDir0[0]
  m[2, 1] = vDir0[1]
  m[2, 2] = vDir0[2]

  return m

# -------------------------------------------------------.
shape = scene.active_shape()

if shape.type != 4 or shape.total_number_of_control_points < 2:
  xshade.show_message_box('ポイント数が2以上の線形状を選択してください。', False)

else:
  # ダイアログボックスの作成と表示.
  dlg = xshade.create_dialog_with_uuid('1cb3c17f-6df6-4451-ab9e-473034179357')
  divU_id = dlg.append_int('分割数 U (円周まわり)')
  divV_id = dlg.append_int('分割数 V (線形状の進行方向)')
  startRadius_id = dlg.append_float('開始半径', 'mm')
  endRadius_id   = dlg.append_float('終了半径', 'mm')
  dlg.set_value(divU_id, 12)
  dlg.set_value(divV_id, 10)
  dlg.set_value(startRadius_id, 100.0)
  dlg.set_value(endRadius_id, 100.0)
  dlg.set_default_value(divU_id, 12)
  dlg.set_default_value(divV_id, 10)
  dlg.set_default_value(startRadius_id, 100.0)
  dlg.set_default_value(endRadius_id, 100.0)

  dlg.append_default_button()

  if dlg.ask("線形状からポリゴンメッシュのチューブを作成"):
    divVCou = max(2, dlg.get_value(divV_id) + 1)
    divUCou = max(3, dlg.get_value(divU_id) + 1)
    startRadiusV = max(0.001, dlg.get_value(startRadius_id))
    endRadiusV   = max(0.001, dlg.get_value(endRadius_id))

    # 線形状をポイントで分割.
    divCou = min(40, divVCou * 4)
    vList = getLinePoints(shape, divCou)
    vList2 = recalcLinePoints(vList, divVCou)
    if shape.closed:
      vList2.pop()

    # ---------------------------------------------------.
    # ポリゴンメッシュのチューブを作成.
    # ---------------------------------------------------.
    scene.begin_creating()
    mesh = scene.begin_polygon_mesh(None)

    # numpyのポイントの形で再格納.
    vListCou = len(vList2)
    for i in range(vListCou):
      p = vList2[i]
      vList2[i] = numpy.array([p[0], p[1], p[2]], dtype = 'float64')

    # +Zを中心とした半径radiusVのポイントを計算.
    circleV = []
    dd = (math.pi * 2.0) / ((float)(divUCou))
    dPos = 0.0
    for i in range(divUCou):
      circleV.append(numpy.array([math.cos(dPos), math.sin(dPos), 0.0], dtype = 'float64'))
      dPos += dd

    # ポリゴンメッシュのポイントを配置.
    m = numpy.matrix(numpy.identity(4))
    vDir0 = numpy.array([0.0, 0.0, 1.0])

    radiusV = startRadiusV
    radiusD = (endRadiusV - startRadiusV) / (float)(vListCou - 1)
    for i in range(vListCou):
      if shape.closed == False and i + 1 >= vListCou:
        p1 = vList2[i]
      else:
        p1 = vList2[i]
        p2 = vList2[(i + 1) % vListCou]
        vDir = calcVecNormal(p2 - p1)

      if i == 0:
        m = calcDirToMatrix(p2 - p1)
        vDir0 = vDir
      else:
        pV0 = numpy.dot(numpy.array([vDir0[0], vDir0[1], vDir0[2], 1.0]), m.I)
        pV0 = numpy.array([pV0[0,0], pV0[0,1], pV0[0,2]])
        pV1 = numpy.dot(numpy.array([vDir[0], vDir[1], vDir[2], 1.0]), m.I)
        pV1 = numpy.array([pV1[0,0], pV1[0,1], pV1[0,2]])

        m0 = calcDirToMatrix(pV0)
        m1 = calcDirToMatrix(pV1)
        m = (m1.I * m0).I * m
      
      for j in range(divUCou):
        p = circleV[j]
        p = numpy.dot(numpy.array([p[0] * radiusV, p[1] * radiusV, p[2] * radiusV, 1.0]), m)
        p = [p[0,0] + p1[0], p[0,1] + p1[1], p[0,2] + p1[2]]
        scene.append_polygon_mesh_vertex(p)

      vDir0 = vDir
      radiusV += radiusD

    # ポリゴンメッシュの面を配置.
    iPos = 0
    vCou = vListCou - 1
    if shape.closed:
      vCou = vListCou
    for i in range(vCou):
      for j in range(divUCou):
        i0 = iPos + j
        i1 = iPos + ((j + 1) % divUCou)
        if shape.closed and i + 1 >= vListCou:
          i2 = ((j + 1) % divUCou)
          i3 = j
        else:
          i2 = iPos + divUCou + ((j + 1) % divUCou)
          i3 = iPos + divUCou + j

        scene.append_polygon_mesh_face([i3, i2, i1, i0])
      iPos += divUCou

    # 稜線を計算.
    scene.append_polygon_mesh_edges()

    # UVの割り当て.
    uvIndex = mesh.append_uv_layer()
    uD = 1.0 / (float)(divUCou)
    vD = 1.0 / (float)(divVCou - 1)
    faceI = 0
    vPos = 0.0
    for i in range(vCou):
      uPos = 0.0
      for j in range(divUCou):
        f = mesh.face(faceI)
        f.set_face_uv(uvIndex, 3, [1.0 - uPos, 1.0 - vPos])
        f.set_face_uv(uvIndex, 2, [1.0 - (uPos + uD), 1.0 - vPos])
        f.set_face_uv(uvIndex, 1, [1.0 - (uPos + uD), 1.0 - (vPos + vD)])
        f.set_face_uv(uvIndex, 0, [1.0 - uPos, 1.0 - (vPos + vD)])
        faceI = faceI + 1
        uPos += uD
      vPos += vD

    scene.end_polygon_mesh()
    scene.end_creating()

線形状を選択し上記スクリプトを実行すると、以下のダイアログボックスが表示されます。

「分割数 U」は生成するチューブ形状の円周回りの分割数です。
「分割数 V」は生成するチューブ形状の進行方向での分割数です。
「開始半径」は生成するチューブ形状の開始位置での半径です。
「終了半径」は生成するチューブ形状の終了位置での半径です。
OKボタンを押すと、線形状を中心としたポリゴンメッシュのチューブ形状が生成されます。

UV1のUV層に対して、U方向にチューブ形状の円周回り、V方向にチューブ形状の進行方向でUVが割り当てられます。

Translate »