All Downloads are FREE. Search and download functionalities are using the official Maven repository.

indigo.platform.renderer.webgl2.LayerRenderer.scala Maven / Gradle / Ivy

The newest version!
package indigo.platform.renderer.webgl2

import indigo.facades.WebGL2RenderingContext
import indigo.platform.assets.AtlasId
import indigo.platform.assets.DynamicText
import indigo.platform.renderer.shared.FrameBufferComponents
import indigo.platform.renderer.shared.FrameBufferFunctions
import indigo.platform.renderer.shared.TextureLookupResult
import indigo.platform.renderer.shared.WebGLHelper
import indigo.shared.datatypes.RGBA
import indigo.shared.datatypes.mutable.CheapMatrix4
import indigo.shared.display.DisplayCloneBatch
import indigo.shared.display.DisplayCloneTiles
import indigo.shared.display.DisplayEntity
import indigo.shared.display.DisplayGroup
import indigo.shared.display.DisplayMutants
import indigo.shared.display.DisplayObject
import indigo.shared.display.DisplayText
import indigo.shared.display.DisplayTextLetters
import indigo.shared.scenegraph.CloneBatchData
import indigo.shared.scenegraph.CloneId
import indigo.shared.scenegraph.CloneTileData
import indigo.shared.shader.ShaderId
import org.scalajs.dom
import org.scalajs.dom.WebGLBuffer
import org.scalajs.dom.WebGLProgram
import org.scalajs.dom.WebGLRenderingContext
import org.scalajs.dom.WebGLRenderingContext._
import org.scalajs.dom.WebGLTexture

import scala.annotation.tailrec
import scala.collection.immutable
import scala.scalajs.js
import scala.scalajs.js.typedarray.Float32Array

class LayerRenderer(
    gl2: WebGL2RenderingContext,
    textureLocations: js.Array[TextureLookupResult],
    maxBatchSize: Int,
    projectionUBOBuffer: => WebGLBuffer,
    frameDataUBOBuffer: => WebGLBuffer,
    cloneReferenceUBOBuffer: => WebGLBuffer,
    lightDataUBOBuffer: => WebGLBuffer,
    dynamicText: DynamicText,
    textTexture: WebGLTexture
) {

  private val customDataUBOBuffers: js.Dictionary[WebGLBuffer] =
    js.Dictionary.empty[WebGLBuffer]

  // Instance Array Buffers
  private val translateScaleInstanceArray: WebGLBuffer       = gl2.createBuffer()
  private val refFlipInstanceArray: WebGLBuffer              = gl2.createBuffer()
  private val sizeAndFrameScaleInstanceArray: WebGLBuffer    = gl2.createBuffer()
  private val channelOffsets01InstanceArray: WebGLBuffer     = gl2.createBuffer()
  private val channelOffsets23InstanceArray: WebGLBuffer     = gl2.createBuffer()
  private val textureSizeAtlasSizeInstanceArray: WebGLBuffer = gl2.createBuffer()
  private val rotationInstanceArray: WebGLBuffer             = gl2.createBuffer()

  def setupInstanceArray(buffer: WebGLBuffer, location: Int, size: Int): Unit = {
    gl2.bindBuffer(ARRAY_BUFFER, buffer)
    gl2.enableVertexAttribArray(location)
    gl2.vertexAttribPointer(location, size, FLOAT, false, size * Float32Array.BYTES_PER_ELEMENT, 0)
    gl2.vertexAttribDivisor(location, 1)
  }

  // Instance Data Arrays
  private val translateScaleData: js.Array[Float]       = js.Array[Float](4f * maxBatchSize)
  private val refFlipData: js.Array[Float]              = js.Array[Float](4f * maxBatchSize)
  private val sizeAndFrameScaleData: js.Array[Float]    = js.Array[Float](4f * maxBatchSize)
  private val channelOffsets01Data: js.Array[Float]     = js.Array[Float](4f * maxBatchSize)
  private val channelOffsets23Data: js.Array[Float]     = js.Array[Float](4f * maxBatchSize)
  private val textureSizeAtlasSizeData: js.Array[Float] = js.Array[Float](4f * maxBatchSize)
  private val rotationData: js.Array[Float]             = js.Array[Float](1f * maxBatchSize)

  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var lastRenderMode: Int = 0

  inline private def bindData(buffer: WebGLBuffer, data: js.Array[Float]): Unit = {
    gl2.bindBuffer(ARRAY_BUFFER, buffer)
    gl2.bufferData(ARRAY_BUFFER, new Float32Array(data), STATIC_DRAW)
  }

  private def updateData(d: DisplayObject, i: Int): Unit = {
    translateScaleData((i * 4) + 0) = d.x
    translateScaleData((i * 4) + 1) = d.y
    translateScaleData((i * 4) + 2) = d.scaleX
    translateScaleData((i * 4) + 3) = d.scaleY

    rotationData(i) = d.rotation.toFloat

    refFlipData((i * 4) + 0) = d.refX
    refFlipData((i * 4) + 1) = d.refY
    refFlipData((i * 4) + 2) = d.flipX
    refFlipData((i * 4) + 3) = d.flipY

    sizeAndFrameScaleData((i * 4) + 0) = d.width
    sizeAndFrameScaleData((i * 4) + 1) = d.height
    sizeAndFrameScaleData((i * 4) + 2) = d.frameScaleX
    sizeAndFrameScaleData((i * 4) + 3) = d.frameScaleY

    channelOffsets01Data((i * 4) + 0) = d.channelOffset0X
    channelOffsets01Data((i * 4) + 1) = d.channelOffset0Y
    channelOffsets01Data((i * 4) + 2) = d.channelOffset1X
    channelOffsets01Data((i * 4) + 3) = d.channelOffset1Y

    channelOffsets23Data((i * 4) + 0) = d.channelOffset2X
    channelOffsets23Data((i * 4) + 1) = d.channelOffset2Y
    channelOffsets23Data((i * 4) + 2) = d.channelOffset3X
    channelOffsets23Data((i * 4) + 3) = d.channelOffset3Y

    textureSizeAtlasSizeData((i * 4) + 0) = d.textureWidth
    textureSizeAtlasSizeData((i * 4) + 1) = d.textureHeight
    textureSizeAtlasSizeData((i * 4) + 2) = d.atlasWidth
    textureSizeAtlasSizeData((i * 4) + 3) = d.atlasHeight
  }

  inline private def updateCloneData(
      i: Int,
      clone: CloneBatchData
  ): Unit = {
    translateScaleData((i * 4) + 0) = clone.x.toFloat
    translateScaleData((i * 4) + 1) = clone.y.toFloat
    translateScaleData((i * 4) + 2) = clone.scaleX.toFloat
    translateScaleData((i * 4) + 3) = clone.scaleY.toFloat

    rotationData(i) = clone.rotation.toFloat
  }

  inline private def updateCloneTileData(
      i: Int,
      x: Float,
      y: Float,
      rotation: Float,
      scaleX: Float,
      scaleY: Float,
      width: Float,
      height: Float,
      frameScaleX: Float,
      frameScaleY: Float,
      channelOffset0X: Float,
      channelOffset0Y: Float,
      channelOffset1X: Float,
      channelOffset1Y: Float,
      channelOffset2X: Float,
      channelOffset2Y: Float,
      channelOffset3X: Float,
      channelOffset3Y: Float
  ): Unit = {
    translateScaleData((i * 4) + 0) = x
    translateScaleData((i * 4) + 1) = y
    translateScaleData((i * 4) + 2) = scaleX
    translateScaleData((i * 4) + 3) = scaleY

    sizeAndFrameScaleData((i * 4) + 0) = width
    sizeAndFrameScaleData((i * 4) + 1) = height
    sizeAndFrameScaleData((i * 4) + 2) = frameScaleX
    sizeAndFrameScaleData((i * 4) + 3) = frameScaleY

    channelOffsets01Data((i * 4) + 0) = channelOffset0X
    channelOffsets01Data((i * 4) + 1) = channelOffset0Y
    channelOffsets01Data((i * 4) + 2) = channelOffset1X
    channelOffsets01Data((i * 4) + 3) = channelOffset1Y

    channelOffsets23Data((i * 4) + 0) = channelOffset2X
    channelOffsets23Data((i * 4) + 1) = channelOffset2Y
    channelOffsets23Data((i * 4) + 2) = channelOffset3X
    channelOffsets23Data((i * 4) + 3) = channelOffset3Y

    rotationData(i) = rotation
  }

  private def updateTextData(d: DisplayText, i: Int): Unit = {
    translateScaleData((i * 4) + 0) = d.x
    translateScaleData((i * 4) + 1) = d.y
    translateScaleData((i * 4) + 2) = d.scaleX
    translateScaleData((i * 4) + 3) = d.scaleY

    refFlipData((i * 4) + 0) = d.refX
    refFlipData((i * 4) + 1) = d.refY
    refFlipData((i * 4) + 2) = d.flipX
    refFlipData((i * 4) + 3) = d.flipY

    rotationData(i) = d.rotation.toFloat

    sizeAndFrameScaleData((i * 4) + 0) = d.width.toFloat
    sizeAndFrameScaleData((i * 4) + 1) = d.height.toFloat
    sizeAndFrameScaleData((i * 4) + 2) = 1
    sizeAndFrameScaleData((i * 4) + 3) = 1

    channelOffsets01Data((i * 4) + 0) = 0
    channelOffsets01Data((i * 4) + 1) = 0
    channelOffsets01Data((i * 4) + 2) = 0
    channelOffsets01Data((i * 4) + 3) = 0

    channelOffsets23Data((i * 4) + 0) = 0
    channelOffsets23Data((i * 4) + 1) = 0
    channelOffsets23Data((i * 4) + 2) = 0
    channelOffsets23Data((i * 4) + 3) = 0

    textureSizeAtlasSizeData((i * 4) + 0) = 0
    textureSizeAtlasSizeData((i * 4) + 1) = 0
    textureSizeAtlasSizeData((i * 4) + 2) = 0
    textureSizeAtlasSizeData((i * 4) + 3) = 0
  }

  private val refData: js.Array[Float] =
    js.Array(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
      0.0f, 0.0f)

  def init(): LayerRenderer =
    // pre-populate array
    WebGLHelper.attachUBOData(gl2, refData, cloneReferenceUBOBuffer)

    this

  def requiresContextChange(
      d: DisplayObject,
      atlasName: Option[AtlasId],
      currentShader: ShaderId,
      currentUniformHash: js.Array[String]
  ): Boolean = {
    val uniformHash: js.Array[String] = d.shaderUniformData.map(_.uniformHash)

    d.shaderId != currentShader ||
    (uniformHash.nonEmpty && (uniformHash.length != currentUniformHash.length ||
      !uniformHash.sameElements(currentUniformHash))) ||
    d.atlasName != atlasName ||
    lastRenderMode != 0
  }

  @SuppressWarnings(Array("scalafix:DisableSyntax.var", "scalafix:DisableSyntax.null"))
  private var currentProgram: WebGLProgram                           = null
  private given CanEqual[Option[WebGLProgram], Option[WebGLProgram]] = CanEqual.derived

  @SuppressWarnings(Array("scalafix:DisableSyntax.null"))
  private def setBaseTransform(baseTransform: CheapMatrix4): Unit =
    if currentProgram == null then ()
    else
      gl2.uniformMatrix4fv(
        location = gl2.getUniformLocation(currentProgram, "u_baseTransform"),
        transpose = false,
        value = Float32Array(baseTransform.toJSArray)
      )

  @SuppressWarnings(Array("scalafix:DisableSyntax.null"))
  private def setMode(mode: Int): Unit =
    if currentProgram == null then ()
    else
      gl2.uniform1i(
        gl2.getUniformLocation(currentProgram, "u_mode"),
        mode
      )

  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
  def doContextChange(
      d: DisplayObject,
      atlasName: Option[AtlasId],
      currentShader: ShaderId,
      currentUniformHash: js.Array[String],
      customShaders: js.Dictionary[WebGLProgram],
      baseTransform: CheapMatrix4,
      renderMode: Int
  ): Unit = {
    val shaderHasChanged: Boolean = d.shaderId != currentShader

    // Switch and reference shader
    val activeShader: WebGLProgram =
      if shaderHasChanged then
        try {
          currentProgram = customShaders(d.shaderId.toString)
          setupShader(currentProgram)
          currentProgram
        } catch {
          case _: Throwable =>
            throw new Exception(
              s"Missing entity shader '${d.shaderId}'. Have you remembered to add the shader to the boot sequence or disabled auto-loading of default shaders?"
            )
        }
      else currentProgram

    // Base transform
    if shaderHasChanged then setBaseTransform(baseTransform)
    if shaderHasChanged || lastRenderMode != renderMode then
      lastRenderMode = renderMode
      setMode(renderMode)

    // UBO data
    if d.shaderUniformData.nonEmpty then
      d.shaderUniformData.zipWithIndex.foreach { case (ud, i) =>
        currentUniformHash.lift(i) match
          case Some(hash) if !shaderHasChanged && hash == ud.uniformHash =>
            // This data has already been set on the shader.
            ()

          case _ =>
            val buff = customDataUBOBuffers.getOrElseUpdate(ud.blockName, gl2.createBuffer())

            WebGLHelper.attachUBOData(gl2, ud.data, buff)
            WebGLHelper.bindUBO(
              gl2,
              activeShader,
              RendererWebGL2Constants.customDataBlockOffsetPointer + i,
              buff,
              gl2.getUniformBlockIndex(activeShader, ud.blockName)
            )
      }

    // Atlas
    if d.atlasName != atlasName then
      d.atlasName.flatMap { nextAtlas =>
        textureLocations.find(t => t.name == nextAtlas)
      } match
        case None =>
          ()

        case Some(textureLookup) =>
          gl2.bindTexture(TEXTURE_2D, textureLookup.texture)

    ()
  }

  def setupShader(program: WebGLProgram): Unit = {

    gl2.useProgram(program)

    WebGLHelper.bindUBO(
      gl2,
      program,
      RendererWebGL2Constants.projectionBlockPointer,
      projectionUBOBuffer,
      gl2.getUniformBlockIndex(program, "IndigoProjectionData")
    )
    WebGLHelper.bindUBO(
      gl2,
      program,
      RendererWebGL2Constants.frameDataBlockPointer,
      frameDataUBOBuffer,
      gl2.getUniformBlockIndex(program, "IndigoFrameData")
    )
    WebGLHelper.bindUBO(
      gl2,
      program,
      RendererWebGL2Constants.cloneReferenceDataBlockPointer,
      cloneReferenceUBOBuffer,
      gl2.getUniformBlockIndex(program, "IndigoCloneReferenceData")
    )
    WebGLHelper.bindUBO(
      gl2,
      program,
      RendererWebGL2Constants.lightDataBlockPointer,
      lightDataUBOBuffer,
      gl2.getUniformBlockIndex(program, "IndigoDynamicLightingData")
    )

    // Instance attributes
    // vec4 a_matRotateScale
    setupInstanceArray(translateScaleInstanceArray, 1, 4) //
    // vec4 a_matTranslateAlpha
    setupInstanceArray(refFlipInstanceArray, 2, 4) //
    // vec4 a_sizeAndFrameScale
    setupInstanceArray(sizeAndFrameScaleInstanceArray, 3, 4) //
    // vec4 a_channelOffsets01
    setupInstanceArray(channelOffsets01InstanceArray, 4, 4) //
    // vec4 a_channelOffsets23
    setupInstanceArray(channelOffsets23InstanceArray, 5, 4) //
    // vec4 a_textureSize + atlasSize
    setupInstanceArray(textureSizeAtlasSizeInstanceArray, 6, 4) //
    // float a_rotation
    setupInstanceArray(rotationInstanceArray, 7, 1) //
  }

  def enableCloneBatchMode(): Unit =
    gl2.disableVertexAttribArray(2)
    gl2.disableVertexAttribArray(3)
    gl2.disableVertexAttribArray(4)
    gl2.disableVertexAttribArray(5)
    gl2.disableVertexAttribArray(6)

  def enableCloneTileMode(): Unit =
    gl2.disableVertexAttribArray(2)
    gl2.enableVertexAttribArray(3)
    gl2.enableVertexAttribArray(4)
    gl2.enableVertexAttribArray(5)
    gl2.disableVertexAttribArray(6)

  def disableCloneMode(): Unit =
    gl2.enableVertexAttribArray(2)
    gl2.enableVertexAttribArray(3)
    gl2.enableVertexAttribArray(4)
    gl2.enableVertexAttribArray(5)
    gl2.enableVertexAttribArray(6)

  def drawBuffer(instanceCount: Int): Unit =
    if instanceCount > 0 then
      disableCloneMode()

      bindData(translateScaleInstanceArray, translateScaleData)
      bindData(refFlipInstanceArray, refFlipData)
      bindData(sizeAndFrameScaleInstanceArray, sizeAndFrameScaleData)
      bindData(channelOffsets01InstanceArray, channelOffsets01Data)
      bindData(channelOffsets23InstanceArray, channelOffsets23Data)
      bindData(textureSizeAtlasSizeInstanceArray, textureSizeAtlasSizeData)
      bindData(rotationInstanceArray, rotationData)

      gl2.drawArraysInstanced(TRIANGLE_STRIP, 0, 4, instanceCount)

  def drawCloneBuffer(instanceCount: Int): Unit =
    if instanceCount > 0 then
      enableCloneBatchMode()

      bindData(translateScaleInstanceArray, translateScaleData)
      bindData(rotationInstanceArray, rotationData)

      gl2.drawArraysInstanced(TRIANGLE_STRIP, 0, 4, instanceCount)

  def drawCloneTileBuffer(instanceCount: Int): Unit =
    if instanceCount > 0 then
      enableCloneTileMode()

      bindData(translateScaleInstanceArray, translateScaleData)
      bindData(sizeAndFrameScaleInstanceArray, sizeAndFrameScaleData)
      bindData(channelOffsets01InstanceArray, channelOffsets01Data)
      bindData(channelOffsets23InstanceArray, channelOffsets23Data)
      bindData(rotationInstanceArray, rotationData)

      gl2.drawArraysInstanced(TRIANGLE_STRIP, 0, 4, instanceCount)

  def prepareCloneProgramBuffer(): Unit =
    disableCloneMode()
    bindData(translateScaleInstanceArray, translateScaleData)
    bindData(refFlipInstanceArray, refFlipData)
    bindData(sizeAndFrameScaleInstanceArray, sizeAndFrameScaleData)
    bindData(channelOffsets01InstanceArray, channelOffsets01Data)
    bindData(channelOffsets23InstanceArray, channelOffsets23Data)
    bindData(textureSizeAtlasSizeInstanceArray, textureSizeAtlasSizeData)
    bindData(rotationInstanceArray, rotationData)

  def drawSingleCloneProgram(): Unit =
    gl2.drawArraysInstanced(TRIANGLE_STRIP, 0, 4, 1)

  def drawLayer(
      cloneBlankDisplayObjects: => js.Dictionary[DisplayObject],
      displayEntities: => js.Array[DisplayEntity],
      frameBufferComponents: FrameBufferComponents,
      clearColor: RGBA,
      customShaders: => js.Dictionary[WebGLProgram]
  ): Unit = {

    FrameBufferFunctions.switchToFramebuffer(gl2, frameBufferComponents.frameBuffer, clearColor, true)
    gl2.drawBuffers(frameBufferComponents.colorAttachments)

    gl2.activeTexture(TEXTURE0);

    renderEntities(cloneBlankDisplayObjects, displayEntities, customShaders, CheapMatrix4.identity)
  }

  @SuppressWarnings(
    Array(
      "scalafix:DisableSyntax.null",
      "scalafix:DisableSyntax.while",
      "scalafix:DisableSyntax.var",
      "scalafix:DisableSyntax.throw"
    )
  )
  private def renderEntities(
      cloneBlankDisplayObjects: => js.Dictionary[DisplayObject],
      displayEntities: => js.Array[DisplayEntity],
      customShaders: => js.Dictionary[WebGLProgram],
      baseTransform: CheapMatrix4
  ): Unit = {

    val count: Int                          = displayEntities.length
    var i: Int                              = 0
    var batchCount: Int                     = 0
    var atlasName: Option[AtlasId]          = None
    var currentShader: ShaderId             = ShaderId("")
    var currentShaderHash: js.Array[String] = new js.Array()
    var currentBaseTransformHash: Int       = 0

    // Clones
    var currentCloneId: CloneId        = CloneId("")
    var currentCloneRef: DisplayObject = null

    //
    val sortedEntities: js.Array[DisplayEntity] =
      displayEntities.sortWith((d1, d2) => d1.z > d2.z)

    while (i <= count)
      if i == count then
        drawBuffer(batchCount)
        i += 1
      else if batchCount == maxBatchSize then
        drawBuffer(batchCount)
        batchCount = 0
      else
        sortedEntities(i) match {
          case d: DisplayTextLetters if d.letters.isEmpty =>
            i += 1

          case d: DisplayTextLetters =>
            drawBuffer(batchCount)
            batchCount = 0
            atlasName = None
            currentShader = ShaderId("")
            currentShaderHash = new js.Array()
            renderEntities(cloneBlankDisplayObjects, d.letters, customShaders, baseTransform)
            i += 1

          case d: DisplayGroup if d.entities.isEmpty =>
            i += 1

          case d: DisplayGroup =>
            drawBuffer(batchCount)
            batchCount = 0
            atlasName = None
            currentShader = ShaderId("")
            currentShaderHash = new js.Array()
            renderEntities(cloneBlankDisplayObjects, d.entities, customShaders, d.transform * baseTransform)
            setBaseTransform(baseTransform)
            i += 1

          case d: DisplayObject if requiresContextChange(d, atlasName, currentShader, currentShaderHash) =>
            drawBuffer(batchCount)
            doContextChange(
              d,
              atlasName,
              currentShader,
              currentShaderHash,
              customShaders,
              baseTransform,
              0
            )
            batchCount = 0
            atlasName = d.atlasName
            currentShader = d.shaderId
            currentShaderHash = d.shaderUniformData.map(_.uniformHash)

          case d: DisplayObject =>
            updateData(d, batchCount)
            batchCount = batchCount + 1
            i += 1

          case c: DisplayCloneBatch =>
            drawBuffer(batchCount)

            var cloneBlankExists = false
            var refreshCloneUBO  = false

            if c.id.toString != currentCloneId.toString then
              cloneBlankDisplayObjects.get(c.id.toString) match
                case None => ()
                case Some(d) =>
                  currentCloneId = c.id
                  currentCloneRef = d
                  cloneBlankExists = true
                  refreshCloneUBO = true
            else cloneBlankExists = true

            if cloneBlankExists then
              doContextChange(
                currentCloneRef,
                atlasName,
                currentShader,
                currentShaderHash,
                customShaders,
                baseTransform,
                1
              )

              if refreshCloneUBO || currentShader != currentCloneRef.shaderId then uploadRefUBO(currentCloneRef)

              val numberProcessed: Int =
                processCloneBatch(c)

              drawCloneBuffer(numberProcessed)

              batchCount = 0
              atlasName = currentCloneRef.atlasName
              currentShader = currentCloneRef.shaderId
              currentShaderHash = currentCloneRef.shaderUniformData.map(_.uniformHash)

            i += 1

          case c: DisplayCloneTiles =>
            drawBuffer(batchCount)

            var cloneBlankExists = false
            var refreshCloneUBO  = false

            if c.id.toString != currentCloneId.toString then
              cloneBlankDisplayObjects.get(c.id.toString) match
                case None => ()
                case Some(d) =>
                  currentCloneId = c.id
                  currentCloneRef = d
                  cloneBlankExists = true
                  refreshCloneUBO = true
            else cloneBlankExists = true

            if cloneBlankExists then
              doContextChange(
                currentCloneRef,
                atlasName,
                currentShader,
                currentShaderHash,
                customShaders,
                baseTransform,
                2
              )

              if refreshCloneUBO || currentShader != currentCloneRef.shaderId then uploadRefUBO(currentCloneRef)

              val numberProcessed: Int =
                processCloneTiles(c, currentCloneRef)

              drawCloneTileBuffer(numberProcessed)

              batchCount = 0
              atlasName = currentCloneRef.atlasName
              currentShader = currentCloneRef.shaderId
              currentShaderHash = currentCloneRef.shaderUniformData.map(_.uniformHash)

            i += 1

          case c: DisplayMutants =>
            drawBuffer(batchCount)

            var cloneBlankExists = false

            if c.id.toString != currentCloneId.toString then
              cloneBlankDisplayObjects.get(c.id.toString) match
                case None => ()
                case Some(d) =>
                  currentCloneId = c.id
                  currentCloneRef = d
                  cloneBlankExists = true
            else cloneBlankExists = true

            if cloneBlankExists &&
              requiresContextChange(currentCloneRef, atlasName, currentShader, currentShaderHash)
            then
              doContextChange(
                currentCloneRef,
                atlasName,
                currentShader,
                currentShaderHash,
                customShaders,
                baseTransform,
                0
              )

              processMutants(c, currentCloneRef, currentProgram)

              batchCount = 0
              atlasName = currentCloneRef.atlasName
              currentShader = currentCloneRef.shaderId
              currentShaderHash = currentCloneRef.shaderUniformData.map(_.uniformHash)

            i += 1

          case t: DisplayText =>
            drawBuffer(batchCount)

            // Change context
            val shaderId = indigo.shared.shader.StandardShaders.Bitmap.id
            val activeShader: WebGLProgram =
              if (currentShader != shaderId) {
                customShaders.get(shaderId.toString) match {
                  case Some(s) =>
                    currentProgram = s
                    setupShader(s)
                    s

                  case None =>
                    throw new Exception(
                      s"(TextBox) Missing entity shader '$shaderId'. Have you remembered to add the shader to the boot sequence or disabled auto-loading of default shaders?"
                    )
                }
              } else currentProgram
            //

            // Base transform
            if shaderId != currentShader then
              setBaseTransform(baseTransform)
              lastRenderMode = 0
              setMode(0)
              currentShader = shaderId
            else if lastRenderMode != 0 then
              lastRenderMode = 0
              setMode(0)

            // UBO data
            val buff = customDataUBOBuffers.getOrElseUpdate("[indigo_internal_buffer_textbox]", gl2.createBuffer())
            WebGLHelper.attachUBOData(gl2, js.Array[Float](0), buff)
            WebGLHelper.bindUBO(
              gl2,
              activeShader,
              RendererWebGL2Constants.customDataBlockOffsetPointer,
              buff,
              gl2.getUniformBlockIndex(activeShader, "IndigoBitmapData")
            )
            //

            gl2.bindTexture(TEXTURE_2D, textTexture)
            gl2.texImage2D(
              TEXTURE_2D,
              0,
              WebGLRenderingContext.RGBA,
              WebGLRenderingContext.RGBA,
              UNSIGNED_BYTE,
              dynamicText.makeTextImageData(t.text, t.style, t.width, t.height)
            )

            updateTextData(t, 0)
            batchCount = 1
            atlasName = None
            currentShaderHash = new js.Array()
            i += 1
        }
  }

  @SuppressWarnings(Array("scalafix:DisableSyntax.var"))
  private var currentRefUBOHash: Int = -1
  private def uploadRefUBO(refDisplayObject: DisplayObject): Unit =
    val code = refDisplayObject.hashCode
    if currentRefUBOHash == code then ()
    else
      val refData: js.Array[Float] =
        js.Array(
          refDisplayObject.refX,
          refDisplayObject.refY,
          refDisplayObject.flipX,
          refDisplayObject.flipY,
          refDisplayObject.width,
          refDisplayObject.height,
          refDisplayObject.frameScaleX,
          refDisplayObject.frameScaleY,
          refDisplayObject.channelOffset0X,
          refDisplayObject.channelOffset0Y,
          refDisplayObject.channelOffset1X,
          refDisplayObject.channelOffset1Y,
          refDisplayObject.channelOffset2X,
          refDisplayObject.channelOffset2Y,
          refDisplayObject.channelOffset3X,
          refDisplayObject.channelOffset3Y,
          refDisplayObject.textureWidth,
          refDisplayObject.textureHeight,
          refDisplayObject.atlasWidth,
          refDisplayObject.atlasHeight
        )
      WebGLHelper.attachUBOData(gl2, refData, cloneReferenceUBOBuffer)
      currentRefUBOHash = code

  @SuppressWarnings(Array("scalafix:DisableSyntax.var", "scalafix:DisableSyntax.while"))
  private def processCloneBatch(c: DisplayCloneBatch): Int = {
    val count: Int = c.cloneData.length
    var i: Int     = 0

    while (i < count) {
      updateCloneData(
        i,
        c.cloneData(i)
      )

      i += 1
    }

    count
  }

  @SuppressWarnings(Array("scalafix:DisableSyntax.var", "scalafix:DisableSyntax.while"))
  private def processCloneTiles(
      c: DisplayCloneTiles,
      refDisplayObject: DisplayObject
  ): Int = {
    val count: Int = c.cloneData.length
    var i: Int     = 0

    val atlasWidth  = refDisplayObject.atlasWidth
    val atlasHeight = refDisplayObject.atlasHeight
    val textureX    = refDisplayObject.textureX
    val textureY    = refDisplayObject.textureY
    val c1X         = refDisplayObject.channelOffset1X - refDisplayObject.channelOffset0X
    val c1Y         = refDisplayObject.channelOffset1Y - refDisplayObject.channelOffset0Y
    val c2X         = refDisplayObject.channelOffset2X - refDisplayObject.channelOffset0X
    val c2Y         = refDisplayObject.channelOffset2Y - refDisplayObject.channelOffset0Y
    val c3X         = refDisplayObject.channelOffset3X - refDisplayObject.channelOffset0X
    val c3Y         = refDisplayObject.channelOffset3Y - refDisplayObject.channelOffset0Y

    while (i < count) {
      val clone           = c.cloneData(i)
      val cropWidth       = clone.cropWidth
      val cropHeight      = clone.cropHeight
      val frameScaleX     = cropWidth / atlasWidth
      val frameScaleY     = cropHeight / atlasHeight
      val channelOffset0X = frameScaleX * ((clone.cropX + textureX) / cropWidth)
      val channelOffset0Y = frameScaleY * ((clone.cropY + textureY) / cropHeight)

      updateCloneTileData(
        i = i,
        x = clone.x.toFloat,
        y = clone.y.toFloat,
        rotation = clone.rotation.toFloat,
        scaleX = clone.scaleX.toFloat,
        scaleY = clone.scaleY.toFloat,
        width = cropWidth.toFloat,
        height = cropHeight.toFloat,
        frameScaleX = frameScaleX,
        frameScaleY = frameScaleY,
        channelOffset0X = channelOffset0X,
        channelOffset0Y = channelOffset0Y,
        channelOffset1X = channelOffset0X + c1X,
        channelOffset1Y = channelOffset0Y + c1Y,
        channelOffset2X = channelOffset0X + c2X,
        channelOffset2Y = channelOffset0Y + c2Y,
        channelOffset3X = channelOffset0X + c3X,
        channelOffset3Y = channelOffset0Y + c3Y
      )

      i += 1
    }

    count
  }

  @SuppressWarnings(Array("scalafix:DisableSyntax.var", "scalafix:DisableSyntax.while"))
  private def processMutants(
      c: DisplayMutants,
      refDisplayObject: DisplayObject,
      activeShader: WebGLProgram
  ): Unit =
    if (c.cloneData.length > 0) {
      updateData(refDisplayObject, 0)
      prepareCloneProgramBuffer()

      val count: Int                              = c.cloneData.length
      var i: Int                                  = 0
      var currentUniformHash: js.Array[String]    = new js.Array()
      val blockIndexLookup: js.Dictionary[Double] = js.Dictionary()

      while (i < count) {
        val shaderUniformData = c.cloneData(i)

        // UBO data
        if shaderUniformData.nonEmpty then
          shaderUniformData.zipWithIndex.foreach { case (ud, i) =>
            currentUniformHash.lift(i) match
              case Some(hash) if hash == ud.uniformHash =>
                // This data has already been set on the shader.
                ()

              case _ =>
                val blockName = ud.blockName

                val buff = customDataUBOBuffers.getOrElseUpdate(blockName, gl2.createBuffer())

                val blockIndex: Double =
                  blockIndexLookup.get(blockName) match
                    case Some(idx) => idx
                    case None =>
                      val idx = gl2.getUniformBlockIndex(activeShader, blockName)
                      blockIndexLookup.update(blockName, idx)
                      idx

                WebGLHelper.attachUBOData(gl2, ud.data, buff)
                WebGLHelper.bindUBO(
                  gl2,
                  activeShader,
                  RendererWebGL2Constants.customDataBlockOffsetPointer + i,
                  buff,
                  blockIndex
                )
          }
          currentUniformHash = shaderUniformData.map(_.uniformHash)

        drawSingleCloneProgram()

        i += 1
      }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy