ARのSNS作ってみる オブジェクト作成編

# 初めに
　本記事は以下の記事の続きになります。まだ読まれていない方は読まれてから本記事を読むと分かり易いと思います。

https://qiita.com/dd7223dd/items/d9ecf9b33ff3c2f6ca00

　前回は投稿したい内容を入力する画面と、タップしたらオブジェクトが描写できるところまで解説しました。本記事では以下２点について解説していきます。
 * 入力された内容が記載されたオブジェクトを描写できる
 * 入力された内容（文字列）の長さによってオブジェクトを伸び縮みできる

# 入力された内容が記載されたオブジェクトを描写する
　入力される内容は日本語の文字列です。ひらがな、カタカナ、漢字が存在するため、文字をオブジェクト化して描写するというのは難しいです。したがって文字列を画像化してオブジェクトにテクスチャとして張り付けるという方法を取ります。コードは以下です。
```kotlin
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Canvas
import android.graphics.Paint.Align
import android.graphics.Paint
import android.graphics.Typeface

class MessageTextBitmap (messageText: String) {

    val messageTextBitmap: Bitmap = textToBitmap(messageText, 30f, Color.WHITE)

    private fun textToBitmap(text: String, textSize: Float, textColor: Int): Bitmap {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        paint.textSize  = textSize
        paint.color     = textColor
        paint.textAlign = Align.LEFT
        paint.typeface  = Typeface.DEFAULT_BOLD

        val testList: List<String> =  text.split("\n")

        val longestText = testList.reduce { longestText, text -> if (text.length > longestText.length) text else longestText }
        val width       = (paint.measureText(longestText) + 50f).toInt()
        val height      = ((-paint.ascent() + paint.descent() + 5f) * testList.size + 50f).toInt()

        val image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(image)

        canvas.drawColor(Color.CYAN)

        for (i in testList.indices) {
            val x = (width - paint.measureText(longestText)) / 2f
            val y =
                if (i == 0) {
                    -paint.ascent() + (50f / 2f)
                } else {
                    -paint.ascent() + (50f / 2f) + ((-paint.ascent() + paint.descent() + 5f) * (i))
                }

            canvas.drawText(testList[i], x, y, paint)
        }

        return image
    }
}
```

　テキストを受け取ったら文字数、行数に応じて画像のサイズを動的に設定します。実行すると以下のような画像ができます。

![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/5d40c395-c151-5b91-829b-a258a55a8f6d.png)

　次にテクスチャを張り付けるベースとなるオブジェクトを作成します。右がオブジェクトの形で左がテクスチャ座標です。テクスチャ座標は0～1の相対距離で計算されます。オブジェクトの表面のテクスチャ座標はテクスチャの全体を張り付けられるようにして、オブジェクトの裏面のテクスチャ座標はすべて0,0座標に置いてテクスチャの端の色が適用されるだけにしています。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/f2c2e58f-12e9-0388-c5d1-32dccd363ac8.png)


　このオブジェクトに先ほどの画像をテクスチャとして貼り付けると入力された内容が記載されたオブジェクトを描写できます。
 
![Screenshot_20241020-150957.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/432e7a46-c29c-379e-5a55-828afe68bb4a.png)

　しかし今のままではオブジェクトの大きさが固定なので入力された文字数が長かったり、改行があった場合、画像がオブジェクトのサイズにスケールされてしまいます。

 ![Screenshot_20241020-151102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/725b033c-684c-d82f-8f80-5bf80031dc53.png)

# 入力された内容（文字列）の長さによってオブジェクトを伸び縮みする
　次に画像サイズに合わせてオブジェクトのサイズも動的に変更します。まずは全角の一文字が丁度よく収まるサイズのオブジェクトを作成します。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/b4a7a1b7-d714-dd8b-4154-44e3721c47e9.png)

　次にこのオブジェクトを入力された文字列の画像サイズに合わせて動的に大きさを変えていきます。objファイルはテキストベースで情報を持っています。なのでテキストファイルとして読み込み、書き換えて再度保存するという方法を取ります。
　利用したソフトによりますがobjファイルは頂点座標の情報を「v {x} {z} {y}」の形で持っています。この{x}と{y}を変えることでオブジェクトをX軸Y軸に伸び縮みさせることができます。objファイルは頂点座標の他にもテクスチャ座標の情報なども持っていますが、テクスチャ座標は上述した通り相対距離で持っているため、オブジェクトを伸び縮みさせるだけなら何も変えなくて大丈夫です。
　objファイルを書き換えるコードは以下の通りです。

 ```Kotlin
import android.content.Context
import android.content.res.AssetManager
import java.io.File
import java.io.InputStream
import kotlin.text.split
import kotlin.text.startsWith
import kotlin.text.toFloat

class MessageTextObject (private val messageTextBitmap: MessageTextBitmap, private val assetManager: AssetManager, private val context: Context) {

    private val baseImageWidth: Double  = 50.0
    private val baseImageHeight: Double = 60.0

    fun convertObjFile() {
        val imageWidth  = messageTextBitmap.messageTextBitmap.width
        val imageHeight = messageTextBitmap.messageTextBitmap.height

        val ratioWidth: Double  = imageWidth.toDouble() / baseImageWidth
        val ratioHeight: Double = imageHeight.toDouble() / baseImageHeight

        val objFile = assetManager.open( "models/message.obj")
        val newObjFileLine = multiplyVertices(objFile, ratioWidth, ratioHeight)

        val newObjFile = File(context.filesDir, "message.obj")
        newObjFile.writeText(newObjFileLine)
    }

    private fun multiplyVertices(objFile: InputStream, xMultiplier: Double = 1.0, yMultiplier: Double = 1.0, zMultiplier: Double = 1.0): String {
        val lines = objFile.bufferedReader().readLines()
        val newLines = mutableListOf<String>()

        for (line in lines) {
            if (line.startsWith("v ")) {
                val parts = line.split(" ")
                if (parts.size >= 4) {
                    val x = parts[1].toFloat() * xMultiplier
                    val z = parts[2].toFloat() * zMultiplier
                    val y = parts[3].toFloat() * yMultiplier
                    newLines.add("v $x $z $y")
                } else {
                    newLines.add(line)
                }
            } else {
                newLines.add(line)
            }
        }

        return newLines.joinToString("\n")
    }
}
 ```

　全角一文字の時の画像サイズが50×60なので、それをベースサイズとして、入力された文字列の画像がベースサイズから縦横それぞれ何倍の大きさになっているかを求めます。求められた倍率をobjファイルの頂点座標のX、Yにそれぞれ乗算します。これによってオブジェクトが動的に大きさを合わせてくれます。

 ![Screenshot_20241020-144810.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/820020bf-cba9-2507-3e16-487f71840734.png)

![Screenshot_20241020-145000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267761/7e15ed38-e1e6-43dd-9170-c5a99d8c0001.png)

# まとめ
　今回はここまでにします。次回はオブジェクトの絶対位置取得や永続化まで終わらせて一旦必須機能はすべて実装しようかなと思います。細かなレイアウトやユーザ登録機能、オブジェクトの形や色もユーザが任意で変えられるような機能も追加したいので、完成はもう少しかかりそうです。