feat: 离屏渲染
This commit is contained in:
@@ -1,154 +0,0 @@
|
||||
package net.airgame.compose.ui.client
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.InternalComposeUiApi
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.asComposeCanvas
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.scene.ComposeScene
|
||||
import androidx.compose.ui.scene.SingleLayerComposeScene
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.mojang.blaze3d.platform.TextureUtil
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import kotlinx.coroutines.launch
|
||||
import net.minecraft.client.MinecraftClient
|
||||
import net.minecraft.client.gui.DrawContext
|
||||
import net.minecraft.client.gui.screen.Screen
|
||||
import net.minecraft.client.texture.NativeImage
|
||||
import net.minecraft.text.Text
|
||||
import org.jetbrains.skia.Color
|
||||
import org.jetbrains.skia.EncodedImageFormat
|
||||
import org.jetbrains.skia.Surface
|
||||
import org.jetbrains.skiko.FrameDispatcher
|
||||
|
||||
@OptIn(InternalComposeUiApi::class)
|
||||
class ComposeScreen(
|
||||
val content: @Composable (ComposeScreen) -> Unit
|
||||
) : Screen(Text.literal("")) {
|
||||
private val frameDispatcher = FrameDispatcher(ComposeUIMod.singleThreadDispatcher) {
|
||||
renderFrame()
|
||||
}
|
||||
|
||||
private val windowWidth: Int get() = MinecraftClient.getInstance().window.width
|
||||
private val windowHeight: Int get() = MinecraftClient.getInstance().window.height
|
||||
|
||||
private lateinit var surface: Surface
|
||||
private lateinit var canvas :Canvas
|
||||
private lateinit var scene: ComposeScene
|
||||
|
||||
override fun init() {
|
||||
surface = Surface.makeRasterN32Premul(windowWidth, windowHeight)
|
||||
canvas = surface.canvas.asComposeCanvas()
|
||||
scene = SingleLayerComposeScene(
|
||||
coroutineContext = ComposeUIMod.singleThreadDispatcher,
|
||||
size = IntSize(windowWidth, windowHeight),
|
||||
density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat())
|
||||
) {
|
||||
frameDispatcher.scheduleFrame()
|
||||
}
|
||||
ComposeUIMod.coroutineScope.launch {
|
||||
scene.setContent {
|
||||
content(this@ComposeScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
if (surface.isClosed) {
|
||||
return
|
||||
}
|
||||
ComposeUIMod.coroutineScope.launch {
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Move,
|
||||
position = Offset(
|
||||
// mouseX.toFloat(), mouseY.toFloat()
|
||||
MinecraftClient.getInstance().mouse.x.toFloat(),
|
||||
MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
context.draw {
|
||||
val vertexConsumer = it.getBuffer(ComposeUIMod.composeLayer)
|
||||
val matrix4f = context.matrices.peek().positionMatrix
|
||||
vertexConsumer.vertex(matrix4f, 0f, 0f, 0f).texture(0f, 0f)
|
||||
vertexConsumer.vertex(matrix4f, 0f, height.toFloat(), 0f).texture(0f, 1f)
|
||||
vertexConsumer.vertex(matrix4f, width.toFloat(), height.toFloat(), 0f).texture(1f, 1f)
|
||||
vertexConsumer.vertex(matrix4f, width.toFloat(), 0f, 0f).texture(1f, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderFrame() {
|
||||
if (surface.isClosed) {
|
||||
return
|
||||
}
|
||||
val timer = DebugTimer()
|
||||
surface.canvas.clear(Color.TRANSPARENT)
|
||||
timer.log("clear")
|
||||
scene.render(canvas, System.nanoTime())
|
||||
timer.log("render")
|
||||
surface.makeImageSnapshot().use { image ->
|
||||
timer.log("makeImageSnapshot")
|
||||
image.encodeToData(EncodedImageFormat.PNG)?.use { data ->
|
||||
timer.log("encodeToData")
|
||||
val nativeImage = NativeImage.read(data.bytes)
|
||||
timer.log("NativeImage.read")
|
||||
RenderSystem.recordRenderCall {
|
||||
timer.log("recordRenderCall")
|
||||
TextureUtil.prepareImage(ComposeUIMod.textureGlID, 0, nativeImage.width, nativeImage.height)
|
||||
timer.log("prepareImage")
|
||||
nativeImage.upload(0, 0, 0, 0, 0, nativeImage.width, nativeImage.height, true)
|
||||
timer.log("upload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(client: MinecraftClient, width: Int, height: Int) {
|
||||
super.resize(client, width, height)
|
||||
val old = surface
|
||||
surface = Surface.makeRasterN32Premul(windowWidth, windowHeight)
|
||||
canvas = surface.canvas.asComposeCanvas()
|
||||
old.close()
|
||||
scene.size = IntSize(windowWidth, windowHeight)
|
||||
scene.density = Density(client.options.guiScale.value.toFloat())
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
ComposeUIMod.coroutineScope.launch {
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Press,
|
||||
button = PointerButton.Primary,
|
||||
position = Offset(
|
||||
// mouseX.toFloat(), mouseY.toFloat()
|
||||
MinecraftClient.getInstance().mouse.x.toFloat(),
|
||||
MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
ComposeUIMod.coroutineScope.launch {
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Release,
|
||||
button = PointerButton.Primary,
|
||||
position = Offset(
|
||||
// mouseX.toFloat(), mouseY.toFloat()
|
||||
MinecraftClient.getInstance().mouse.x.toFloat(),
|
||||
MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
MinecraftClient.getInstance().setScreen(null)
|
||||
scene.close()
|
||||
surface.close()
|
||||
}
|
||||
}
|
@@ -1,54 +1,23 @@
|
||||
package net.airgame.compose.ui.client
|
||||
|
||||
import com.mojang.blaze3d.platform.TextureUtil
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.airgame.compose.ui.client.screen.ComposeScreen
|
||||
import net.fabricmc.api.ClientModInitializer
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
|
||||
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
|
||||
import net.minecraft.client.option.KeyBinding
|
||||
import net.minecraft.client.render.RenderLayer
|
||||
import net.minecraft.client.render.RenderPhase
|
||||
import net.minecraft.client.render.VertexFormat
|
||||
import net.minecraft.client.render.VertexFormats
|
||||
import net.minecraft.client.util.InputUtil
|
||||
import org.jetbrains.skiko.MainUIDispatcher
|
||||
|
||||
class ComposeUIMod : ClientModInitializer {
|
||||
companion object {
|
||||
val singleThreadDispatcher =
|
||||
MainUIDispatcher.limitedParallelism(1) + CoroutineExceptionHandler { _, throwable ->
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
val coroutineScope = CoroutineScope(singleThreadDispatcher)
|
||||
val textureGlID: Int by lazy { TextureUtil.generateTextureId() }
|
||||
val composeLayer: RenderLayer by lazy {
|
||||
RenderLayer.of(
|
||||
"compose",
|
||||
VertexFormats.POSITION_TEXTURE,
|
||||
VertexFormat.DrawMode.QUADS,
|
||||
786432,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(RenderPhase.TextureBase({
|
||||
RenderSystem.setShaderTexture(0, textureGlID)
|
||||
}, {}))
|
||||
.program(RenderPhase.POSITION_TEXTURE_PROGRAM)
|
||||
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
|
||||
.depthTest(RenderPhase.LEQUAL_DEPTH_TEST)
|
||||
.build(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val bind = KeyBinding("测试", InputUtil.GLFW_KEY_G, "测试")
|
||||
private val bind = KeyBinding("测试", InputUtil.GLFW_KEY_G, "测试")
|
||||
|
||||
override fun onInitializeClient() {
|
||||
KeyBindingHelper.registerKeyBinding(bind)
|
||||
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client ->
|
||||
while (bind.wasPressed()) {
|
||||
val screen = ComposeScreen {
|
||||
TestUI()
|
||||
CenterUI {
|
||||
TestUI()
|
||||
}
|
||||
}
|
||||
client.setScreen(screen)
|
||||
}
|
||||
|
@@ -1,33 +1,194 @@
|
||||
package net.airgame.compose.ui.client
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun CenterUI(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun TestUI() {
|
||||
var show by remember { mutableStateOf(true) }
|
||||
val mails = mutableStateListOf(
|
||||
Mail(
|
||||
"新手启航大礼包来袭", "2025-01-14", """
|
||||
尊敬的玩家,您好!
|
||||
当您踏入《奇幻异界》这片神秘大陆的那一刻,一场精彩绝伦的奇幻之旅便已开启。为了助您在异界中快速成长,我们精心准备了这份新手启航大礼包,愿它成为您冒险之路上的助力。
|
||||
礼包内容如下:
|
||||
金币×1000:可用于在游戏内购买各类道具、装备,助您在前期迅速提升实力。
|
||||
初级经验药水×5:使用后可获得大量经验,加速角色升级,让您更快解锁新技能和玩法。
|
||||
新手护符×1:佩戴后可提升角色全属性5%,在您探索未知世界时,为您保驾护航。
|
||||
神秘传送石×3:可随机传送至游戏内的任意一个已解锁的城镇或地图,让您在冒险途中更加便捷地穿梭。
|
||||
新手任务指南×1:详细介绍了游戏内的新手任务流程及隐藏任务线索,助您轻松获取丰厚奖励。
|
||||
礼包已自动发放至您的游戏账户,您可在背包中查收。同时,我们还为新手玩家准备了专属的冒险手册,在游戏内点击“帮助”按钮即可查看,其中包含了丰富的游戏玩法介绍、操作技巧以及常见问题解答,助您快速熟悉游戏。
|
||||
在您冒险的过程中,若遇到任何问题或需要帮助,欢迎随时联系我们的客服团队,我们将竭诚为您服务。此外,别忘了关注我们的官方社区和论坛,那里有更多精彩内容和福利活动等着您!
|
||||
愿您在《奇幻异界》的冒险之路上,收获无尽的快乐与惊喜!
|
||||
《奇幻异界》运营团队
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"周年庆典盛典开启,海量福利来袭", "2025-01-14", """
|
||||
亲爱的勇士,您好!
|
||||
时光荏苒,不知不觉间,《荣耀之战》已经陪伴大家度过了一个精彩的年头。在过去的一年里,我们一同经历了无数惊心动魄的战斗,共同见证了无数荣耀时刻。在此,我们衷心感谢每一位勇士的支持与陪伴!
|
||||
为了庆祝这一特别的时刻,我们精心筹备了周年庆典盛典,海量福利活动现已全面开启!
|
||||
活动一:登录送豪礼
|
||||
活动期间,每日登录游戏即可领取丰厚奖励,累计登录7天,更有机会获得限定坐骑“荣耀战马”!
|
||||
第1天:金币×2000、体力药水×2
|
||||
第2天:钻石×50、初级强化石×10
|
||||
第3天:神秘宝箱×1、中级经验药水×1
|
||||
第4天:金币×3000、高级宝石×1
|
||||
第5天:钻石×100、稀有宠物碎片×5
|
||||
第6天:超级强化石×5、高级经验药水×2
|
||||
第7天:限定坐骑“荣耀战马”(7天体验)×1
|
||||
活动二:消费返利大狂欢
|
||||
活动期间,累计消费钻石达到一定额度,即可获得高额返利,最高返利可达200%!此外,还有稀有装备、珍贵道具等你来拿!
|
||||
消费钻石100 - 499:返利20%,额外赠送初级宝石×5
|
||||
消费钻石500 - 999:返利30%,额外赠送中级强化石×10
|
||||
消费钻石1000 - 2999:返利50%,额外赠送稀有装备宝箱×1
|
||||
消费钻石3000及以上:返利200%,额外赠送传说级武器×1
|
||||
活动三:周年庆典专属副本
|
||||
全新周年庆典专属副本“荣耀之塔”现已开放!副本内机关重重,怪物强大,但宝藏也异常丰厚。成功通关副本,可获得海量金币、钻石、稀有材料以及周年庆典专属称号“荣耀先锋”!
|
||||
活动四:限时抽奖,大奖等你拿
|
||||
活动期间,每日完成指定任务即可获得抽奖机会,奖池内包含超级坐骑“梦幻飞龙”、绝版时装“荣耀战神”、海量钻石等超值大奖,中奖率高达100%!
|
||||
以上活动详情可在游戏内“活动中心”查看,更多惊喜福利等你来发现!
|
||||
在庆典期间,我们的客服团队将全天候在线,为您解决任何游戏问题。同时,我们也诚挚邀请您参与我们的玩家见面会,届时将有机会与开发团队面对面交流,分享您的游戏心得与建议。
|
||||
再次感谢您对《荣耀之战》的支持与热爱,让我们携手共进,开启新的荣耀篇章!
|
||||
《荣耀之战》运营团队
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"公会招募:加入“荣耀之光”,共创辉煌", "2025-01-14", """
|
||||
我是《星际争霸》“荣耀之光”公会的会长。在星际战场上,我们一直关注着您的表现。您出色的指挥能力、卓越的战斗技巧以及对游戏的热情,让我们坚信您是我们公会的不二人选。
|
||||
“荣耀之光”公会成立于游戏开服之初,至今已有数年的历史。我们公会成员来自五湖四海,大家因为共同的爱好和目标而聚集在一起。在这里,我们不仅有激烈的星际战斗,还有温馨的社交氛围。公会成员之间相互帮助、共同成长,一起为了公会的荣耀而努力奋斗。
|
||||
加入“荣耀之光”公会,您将获得以下专属福利:
|
||||
每日公会任务奖励:完成公会任务可获得大量金币、钻石以及公会贡献值。公会贡献值可用于在公会商店兑换稀有装备、珍贵材料以及专属道具。
|
||||
公会仓库资源共享:公会仓库内存放着大量的装备、材料以及道具,成员可免费借用或兑换。当您在游戏中遇到装备或材料短缺的问题时,公会仓库将为您提供强大的支持。
|
||||
专属公会副本:我们拥有独特的公会副本,副本内的怪物难度适中,但宝藏丰厚。公会成员可组队挑战副本,获取专属装备和材料,提升自身实力。
|
||||
公会战荣誉奖励:每周举办的公会战是展示公会实力的舞台。在公会战中,成员们将并肩作战,为公会的荣誉而战。根据公会战的排名,成员可获得丰厚的奖励,包括稀有装备、大量钻石以及专属称号。
|
||||
线下聚会活动:为了增进公会成员之间的友谊,我们定期举办线下聚会活动。在聚会上,大家可以面对面交流游戏心得、分享生活趣事,还能一起参与各种有趣的互动游戏,增进彼此之间的感情。
|
||||
我们相信,您的加入将为“荣耀之光”公会注入新的活力,让我们一起携手共创辉煌!如果您对加入公会感兴趣,请在游戏内回复此邮件,或直接联系公会成员,我们将竭诚为您服务。
|
||||
“荣耀之光”公会会长
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"神秘信件", "2025-01-14", """
|
||||
吾乃隐居于幽暗森林深处的古老精灵族长。数日前,吾族遗失了一件至关重要的神器——生命之源。据吾族先知预言,唯有心怀正义且实力超群之人,方能找回神器,拯救吾族于危难之中。吾族族人已暗中观察你多时,你便是那命中注定之人!请前往幽暗森林之心,寻找线索,开启一段奇幻冒险。吾族将永远铭记你的恩情!
|
||||
古老精灵族长
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"国王的密令", "2025-01-14", """
|
||||
朕闻得边境之地,魔族余孽蠢蠢欲动,欲图谋不轨。朕命你即刻前往边境,查探魔族动向,必要时可调遣附近驻军,将其剿灭。此乃关乎王国安危之重任,朕盼你凯旋而归,朕定有重赏!
|
||||
国王陛下
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"稀有装备出售", "2025-01-14", """
|
||||
各位玩家,您好!
|
||||
本人因游戏风格转变,现有一件稀有装备——“雷霆之怒”双手锤,欲出售。该装备属性卓越,攻击力+120,暴击率+5%,附带独特技能:雷霆一击,可对目标造成额外50%伤害,并有几率使其麻痹2秒。装备等级要求30级,售价金币×2000,诚心出售,有意者请私信联系,非诚勿扰!
|
||||
""".trimIndent()
|
||||
),
|
||||
Mail(
|
||||
"稀有装备出售", "2025-01-14", """
|
||||
亲爱的玩家们:
|
||||
本人急需大量高级魔力水晶,用于打造强力魔法装备。现高价收购,每颗魔力水晶金币×50。本人信誉良好,交易迅速,可直接在游戏内交易,也可线下交易(需双方协商)。若有大量存货者,可适当加价。望各位玩家不吝赐教,助我一臂之力!
|
||||
""".trimIndent()
|
||||
),
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
modifier = Modifier.size(800.dp, 600.dp).background(Color(0x80808080)),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
show = !show
|
||||
println("clicked")
|
||||
}) {
|
||||
Text("点我切换")
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.Gray
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(5.dp),
|
||||
fontSize = 24.sp,
|
||||
text = "邮箱列表"
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(show) {
|
||||
Text("Hello Compose Minecraft!")
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f).padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(mails) {
|
||||
MainContent(it)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(Color.Gray),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
Button(onClick = {}) {
|
||||
Text("全部领取")
|
||||
}
|
||||
Button(onClick = {}) {
|
||||
Text("一键清空")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainContent(mail: Mail) {
|
||||
var isExpand by remember { mutableStateOf(false) }
|
||||
Card(elevation = 5.dp) {
|
||||
Column(
|
||||
modifier = Modifier.padding(5.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = mail.title,
|
||||
color = MaterialTheme.colors.primary,
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
text = mail.sendTime,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.fillMaxWidth().height(1.dp).background(color = Color.Black))
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
elevation = 1.dp,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.clickable { isExpand = !isExpand }.animateContentSize(),
|
||||
style = MaterialTheme.typography.body1,
|
||||
text = mail.content,
|
||||
maxLines = if (isExpand) Int.MAX_VALUE else 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Mail(
|
||||
val title: String,
|
||||
val sendTime: String,
|
||||
val content: String
|
||||
)
|
@@ -0,0 +1,223 @@
|
||||
package net.airgame.compose.ui.client.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.InternalComposeUiApi
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.asComposeCanvas
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.scene.ComposeScene
|
||||
import androidx.compose.ui.scene.MultiLayerComposeScene
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.mojang.blaze3d.platform.GlStateManager
|
||||
import com.mojang.blaze3d.platform.TextureUtil
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Runnable
|
||||
import net.minecraft.client.MinecraftClient
|
||||
import net.minecraft.client.gui.DrawContext
|
||||
import net.minecraft.client.gui.screen.Screen
|
||||
import net.minecraft.client.render.RenderLayer
|
||||
import net.minecraft.client.render.RenderPhase
|
||||
import net.minecraft.client.render.VertexFormat
|
||||
import net.minecraft.client.render.VertexFormats
|
||||
import net.minecraft.text.Text
|
||||
import org.jetbrains.skia.*
|
||||
import org.jetbrains.skiko.FrameDispatcher
|
||||
import org.lwjgl.glfw.GLFW.*
|
||||
import org.lwjgl.opengl.GL30.*
|
||||
import org.lwjgl.system.MemoryUtil.NULL
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@OptIn(InternalComposeUiApi::class)
|
||||
class ComposeScreen(
|
||||
val composeContent: @Composable (ComposeScreen) -> Unit
|
||||
) : Screen(Text.literal("FrameRenderComposeScreen")) {
|
||||
companion object {
|
||||
private val frameBufferID by lazy { GlStateManager.glGenFramebuffers() }
|
||||
private val textureID by lazy { TextureUtil.generateTextureId() }
|
||||
private val composeLayer: RenderLayer by lazy {
|
||||
RenderLayer.of(
|
||||
"cpu_render_compose",
|
||||
VertexFormats.POSITION_TEXTURE,
|
||||
VertexFormat.DrawMode.QUADS,
|
||||
786432,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(RenderPhase.TextureBase({
|
||||
RenderSystem.setShaderTexture(0, textureID)
|
||||
}, {}))
|
||||
.program(RenderPhase.POSITION_TEXTURE_PROGRAM)
|
||||
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
|
||||
.depthTest(RenderPhase.LEQUAL_DEPTH_TEST)
|
||||
.build(false)
|
||||
)
|
||||
}
|
||||
private val windowID by lazy {
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
|
||||
val oldWindow = glfwGetCurrentContext()
|
||||
glfwCreateWindow(1, 1, "", NULL, oldWindow)
|
||||
}
|
||||
}
|
||||
|
||||
private val windowWidth: Int get() = MinecraftClient.getInstance().window.width
|
||||
private val windowHeight: Int get() = MinecraftClient.getInstance().window.height
|
||||
|
||||
private lateinit var directContext: DirectContext
|
||||
private lateinit var surface: Surface
|
||||
private lateinit var canvas: Canvas
|
||||
private lateinit var scene: ComposeScene
|
||||
|
||||
private val tasks = mutableListOf<Runnable>()
|
||||
private val tasksCopy = mutableListOf<Runnable>()
|
||||
|
||||
private val coroutineDispatcher = object : CoroutineDispatcher() {
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
synchronized(tasks) {
|
||||
tasks.add(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val frameDispatcher = FrameDispatcher(coroutineDispatcher) {
|
||||
RenderSystem.recordRenderCall {
|
||||
if (surface.isClosed) {
|
||||
return@recordRenderCall
|
||||
}
|
||||
val oldWindowID = glfwGetCurrentContext()
|
||||
glfwMakeContextCurrent(windowID)
|
||||
|
||||
surface.canvas.clear(Color.TRANSPARENT)
|
||||
scene.render(canvas, System.nanoTime())
|
||||
directContext.flush()
|
||||
|
||||
glfwMakeContextCurrent(oldWindowID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
val oldFrameBuffer = glGetInteger(GL_FRAMEBUFFER_BINDING)
|
||||
val oldTexture = glGetInteger(GL_TEXTURE_BINDING_2D)
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID)
|
||||
glBindTexture(GL_TEXTURE_2D, textureID)
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, GL_RGBA, windowWidth, windowHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL
|
||||
)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0)
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, oldFrameBuffer)
|
||||
glBindTexture(GL_TEXTURE_2D, oldTexture)
|
||||
|
||||
directContext = DirectContext.makeGL()
|
||||
val renderTarget = BackendRenderTarget.makeGL(
|
||||
windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8
|
||||
)
|
||||
surface = Surface.makeFromBackendRenderTarget(
|
||||
directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB
|
||||
)!!
|
||||
canvas = surface.canvas.asComposeCanvas()
|
||||
scene = MultiLayerComposeScene(
|
||||
density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2),
|
||||
size = IntSize(windowWidth, windowHeight),
|
||||
coroutineContext = coroutineDispatcher,
|
||||
invalidate = { frameDispatcher.scheduleFrame() }
|
||||
)
|
||||
scene.setContent {
|
||||
composeContent(this@ComposeScreen)
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
|
||||
synchronized(tasks) {
|
||||
tasksCopy.clear()
|
||||
tasksCopy.addAll(tasks)
|
||||
tasks.clear()
|
||||
}
|
||||
for (runnable in tasksCopy) {
|
||||
runnable.run()
|
||||
}
|
||||
context.draw {
|
||||
val vertexConsumer = it.getBuffer(composeLayer)
|
||||
val matrix4f = context.matrices.peek().positionMatrix
|
||||
vertexConsumer.vertex(matrix4f, 0f, 0f, 0f).texture(0f, 0f)
|
||||
vertexConsumer.vertex(matrix4f, 0f, height.toFloat(), 0f).texture(0f, 1f)
|
||||
vertexConsumer.vertex(matrix4f, width.toFloat(), height.toFloat(), 0f).texture(1f, 1f)
|
||||
vertexConsumer.vertex(matrix4f, width.toFloat(), 0f, 0f).texture(1f, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(client: MinecraftClient, width: Int, height: Int) {
|
||||
super.resize(client, width, height)
|
||||
val old = surface
|
||||
surface = Surface.makeRasterN32Premul(windowWidth, windowHeight)
|
||||
canvas = surface.canvas.asComposeCanvas()
|
||||
old.close()
|
||||
scene.density = Density(client.options.guiScale.value.toFloat() / 2)
|
||||
scene.size = IntSize(windowWidth, windowHeight)
|
||||
}
|
||||
|
||||
override fun mouseMoved(mouseX: Double, mouseY: Double) {
|
||||
val x = MinecraftClient.getInstance().mouse.x.toFloat()
|
||||
val y = MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
coroutineDispatcher.dispatch(coroutineDispatcher) {
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Move,
|
||||
position = Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
coroutineDispatcher.dispatch(coroutineDispatcher) {
|
||||
val x = MinecraftClient.getInstance().mouse.x.toFloat()
|
||||
val y = MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Press,
|
||||
button = PointerButton.Primary,
|
||||
position = Offset(x, y)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
|
||||
coroutineDispatcher.dispatch(coroutineDispatcher) {
|
||||
val x = MinecraftClient.getInstance().mouse.x.toFloat()
|
||||
val y = MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Release,
|
||||
button = PointerButton.Primary,
|
||||
position = Offset(x, y)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mouseScrolled(
|
||||
mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double
|
||||
): Boolean {
|
||||
val x = MinecraftClient.getInstance().mouse.x.toFloat()
|
||||
val y = MinecraftClient.getInstance().mouse.y.toFloat()
|
||||
println("mouseScrolled: $x $y $horizontalAmount $verticalAmount")
|
||||
coroutineDispatcher.dispatch(coroutineDispatcher) {
|
||||
scene.sendPointerEvent(
|
||||
PointerEventType.Scroll,
|
||||
button = PointerButton.Tertiary,
|
||||
position = Offset(x, y),
|
||||
scrollDelta = Offset(horizontalAmount.toFloat(), -verticalAmount.toFloat())
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
MinecraftClient.getInstance().setScreen(null)
|
||||
surface.close()
|
||||
scene.close()
|
||||
directContext.close()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user