技術ブログ

【Blender】SDFベースでモデリングを行うアドオンを作りました【mesh_from_sdf】

はじめに

BlenderでSDFベースのモデリングを行うアドオンを作成しました.3か月前にGithubに最後のコミットを行ってから暫くの間ソースの改修を行えていませんが,記憶が新しい内にSDFベースのモデリングという概念やアドオンで実現したかったこと,また,実装した機能や現在の課題について,宣伝がてらGithubだけでなくこの記事にもまとめようとおもいます.


ソースコードはこちら


TL;DR

(ここは最後に書く)


SDFベースのモデリングって何(前編)

SDF(符号付距離関数)を駆使して,物体の形状を定義(モデリング)していくモデリングのアプローチです.数式で物体の形を定義するということです.

例えば球体は,以下のように表現することができます.

    // 球体の距離関数
    float sdSphere( vec3 p, float s )
    {
        return length(p)-s;
    }

定義した関数sdSphereについて説明します.引数sは球体の半径で,引数pは半径s,位置(0,0,0)の球体に対して,球体の表面からの距離を測りたい座標です.関数sdSphereはある球体の表面と,ある座標との距離を符号付きで返します(引数pが球体の内側にあれば,戻り値の符号は-,外側にあれば,戻り値の符号は+です).p(1,0,0),引数s0.5を与えると,関数sdSphereは,半径0.5,位置(0,0,0)に位置する球体の表面と,座標(1,0,0)との距離を測ることとなり,0.5を返します.また,引数p(0,0,0)を与えると,関数sdSphere-0.5を返します.

このように,ある物体に対して,物体の表面からの距離を,物体の表面との内外判定を考慮して計算する関数をSDF(符号付距離関数)と呼びます.ここからは,SDFで物体と任意の座標との距離を内外判定付きで計算することが出来たとして,それをどうモデリングに活かせるのかについて説明します.

その前に,モデリングって何でしょうか?wiki等で調べれば厳密な定義を知ることが出来るかもしれませんが,難しそうなのでやっぱり触れません.その代わり,特にコンピュータグラフィックスにおいて,モデリングという作業のゴールについて自分の解釈を整理してみようと思います.


コンピュータグラフィックスにおけるモデリングという作業のゴール

自分の解釈では,コンピュータグラフィックスにおける物体のモデリングという作業のゴールは大きく2つ,1. 物体を画像や映像に出力すること2. 物体をメッシュとして出力することが挙げられると考えています.正確には全て1に集約されるのかもしれませんが,2は例として,物体を.obj等に変換し,UnityやBlenderのようなソフトで汎用的に使用できるようにすることであり,1は物体をメッシュとして扱うことに拘らず,とにかく何かしらのソフト上で物体を画像や映像に出力することができればOKという場面・目的になります.なので2つに分けてみました.前章で説明した,SDFをモデリングに活かすという考えは,SDFを活用して,上記の2つのゴールを達成するということと言い換えることができます.


SDFベースのモデリングって何(後編)

はじめに,SDFを利用して,前章で整理したモデリングにおける目的の1つ目(物体を画像や映像に出力する)を実現するまでの流れを辿っていきましょう.HLSLやGLSL等,コンピュータグラフィックスにおける代表的なシェーダー言語においては,レイマーチングという手法をもってこれを実現できます.例えば,GLSLで以下のようなレイマーチングシェーダーを書いてみます(shadertoyで実行することを想定).

    // The MIT License
    // Copyright © 2020 Inigo Quilez
    // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    // Exact euclidean distance to a box frame

    // List of other 3D SDFs:
    //    https://www.shadertoy.com/playlist/43cXRl
    // and
    //    https://iquilezles.org/articles/distfunctions

    //===========================================================================

    // @ 2025 TLabAltoh
    // Inigo Quilez氏の公開したシェーダを基盤に作成したサンプルです.ソースコードの権利については,上記を参照してください

    //===========================================================================

    float sdSphere( vec3 p, float s )
    {
        return length(p)-s;
    }

    //===========================================================================

    float map( in vec3 pos )
    {
        return sdSphere(pos, 0.5 );
    }

    // https://iquilezles.org/articles/normalsSDF
    vec3 calcNormal( in vec3 pos )
    {
        vec2 e = vec2(1.0,-1.0)*0.5773;
        const float eps = 0.0005;
        return normalize( e.xyy*map( pos + e.xyy*eps ) + 
                        e.yyx*map( pos + e.yyx*eps ) + 
                        e.yxy*map( pos + e.yxy*eps ) + 
                        e.xxx*map( pos + e.xxx*eps ) );
    }

    float raycast( in vec3 ro, in vec3 rd, float tmax )
    {
        float t = 0.0;
        for( int i=0; i<256; i++ )
        {
            vec3 pos = ro + t*rd;
            float h = map(pos);
            if( h<0.0001 || t>tmax ) break;
            t += h;
        }
        return (t<tmax)?t:-1.0;
    }

    vec3 lighting( vec3 nor, vec3 mate )
    {
        vec3 lig = vec3(0.0);

        float amb = 0.5 + 0.5*nor.y;
        lig += mate*vec3(0.2,0.3,0.4)*amb;

        float dif = clamp( dot(nor,vec3(0.57703)), 0.0, 1.0 );
        lig += mate*vec3(0.85,0.75,0.65)*dif;
        
        return lig;
    }

    mat4x4 setCameraToWorld( in vec3 ro, in vec3 ta, float cr )
    {
        vec3 cw = normalize(ro-ta);
        vec3 cp = vec3(0.0, cos(cr),sin(cr));
        vec3 cu = normalize(cross(cp,cw));
        vec3 cv =          (cross(cw,cu));
        return mat4x4( cu.x, cu.y, cu.z, 0.0, // note transpose notation because of GLSL row major
                    cv.x, cv.y, cv.z, 0.0,
                    cw.x, cw.y, cw.z, 0.0,
                    ro.x, ro.y, ro.z, 1.0 );
    }

    mat4x4 setWorldToCamera(in vec3 ro, in vec3 ta, float cr )
    {
        vec3 cw = normalize(ro-ta);
        vec3 cp = vec3(0.0, cos(cr),sin(cr));
        vec3 cu = normalize(cross(cp,cw));
        vec3 cv =          (cross(cw,cu));
        return mat4x4( cu.x, cv.x, cw.x, 0.0,
                    cu.y, cv.y, cw.y, 0.0,
                    cu.z, cv.z, cw.z, 0.0,
                    -dot(cu,ro), -dot(cv,ro), -dot(cw,ro), 1.0 );
    }

    #if HW_PERFORMANCE==0
    #define AA 1
    #else
    #define AA 3
    #endif

    void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {
        // camera movement	
        float an = 0.15*(iTime-10.0);
        vec3 ro = 1.2*vec3( 1.0*cos(an), 0.0, 1.0*sin(an) );
        vec3 ta = vec3( 0.0, -0.0, 0.0 );
        // camera matrix
        #if 1
        mat4x4 cameraToWorld = setCameraToWorld( ro, ta, 0.0 );
        mat4x4 worldToCamera = inverse(cameraToWorld);
        #else
        mat4x4 worldToCamera = setWorldToCamera( ro, ta, 0.0 );
        mat4x4 cameraToWorld = inverse(worldToCamera);
        #endif

        // camera projection
        const float fle = 1.5;
                                
        // probe
        vec2  m = (2.0*iMouse.xy-iResolution.xy)/iResolution.y;
        vec3  mrd = normalize(cameraToWorld*vec4(m,-fle,0.0)).xyz;
        float t2 = (-0.4-ro.y)/mrd.y;

        // render    
        vec3 tot = vec3(0.0);
        #if AA>1
        for( int m=0; m<AA; m++ )
        for( int n=0; n<AA; n++ )
        {
            // pixel coordinates
            vec2 o = vec2(float(m),float(n)) / float(AA) - 0.5;
            vec2 p = (2.0*(fragCoord+o)-iResolution.xy)/iResolution.y;
            #else    
            vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
            #endif
            float px = 2.0/iResolution.y;

            // create view ray
            vec3 rd = normalize(cameraToWorld*vec4(p,-fle,0.0)).xyz;

            vec3 col = vec3(0.0);
            float tmin = 1e20;
            
            // SDF shape
            {
                float t = raycast(ro,rd,3.0);
                if( t>0.0 )
                {
                    tmin = t;
                    vec3 pos = ro + t*rd;
                    vec3 nor = calcNormal(pos);
                    col = lighting( nor, vec3(1.0) );
                }
            }

            // gamma        
            col = sqrt( col );
            tot += col;
        #if AA>1
        }
        tot /= float(AA*AA);
        #endif

        fragColor = vec4( tot, 1.0 );
    }

上記シェーダーをshadertoyで実行すると,以下の結果を得ることが出来ます



ちゃんと球体が描画できていますね!メッシュデータ(頂点等)を使用せずにSDFを使用して球体を描画することができました.次は,目的2の,SDFで定義した物体をメッシュに変換するまでの流れを辿っていきましょう.SDFからメッシュへの変換は,マーチングキューブという手法をもって実現することができます.マーチングキューブそのものについても詳細な説明は省きますが,以下の動画がとても分かりやすくて参考になると思うのでぜひ見てみてください(Sebastian Lague氏の作成したチュートリアル動画です).


上記を参考に,SDFをメッシュに変換する実装を検討します.実際にBlender上で動作検証までしてみました.作成したコードは以下です.検証コードでは,Torus(ドーナッツみたいな形のこと)を物体のSDFとして定義しています.



パネルからUIで上記を実行する為のコード等,一部の実装を省略していますが,上記のコードをBlenderのテキストエディタタブから実行すると,以下の結果を得ることができます.
Blender上で,SDFで定義していたトーラスをメッシュに変換することができました!ここまでで,前章で整理したコンピュータグラフィクスにおけるモデリングの目的1,2をSDFを活用することで達成することが確認できました.SDFベースのモデリングという概念は,ここまでで検証したSDFを活用する物体の描画,メッシュ化というプロセスに則ってコンピュータグラフィクスにおけるモデリングを行うことにあたります.また,今回作成したアドオンは,この,SDFベースのモデリングをBlender上で実現することを目的としたものになります.以下に作成したアドオンの機能や現状の課題・制約をまとめていきます.


SDFベースのモデリングを行うアドオン

(これから書く)


さいごに

(これから書く)