diff --git a/app/src/main/java/com/tucavr/FeatureFlags.kt b/app/src/main/java/com/tucavr/FeatureFlags.kt index 30a2db7..d9f3a73 100644 --- a/app/src/main/java/com/tucavr/FeatureFlags.kt +++ b/app/src/main/java/com/tucavr/FeatureFlags.kt @@ -60,6 +60,32 @@ object FeatureFlags { // vr_player_app_vulkan.cpp::shouldDrawAmbientHalo. Desligado por padrão: // nunca validado em headset real até agora. AMBIENT_MODE("ambient_mode", defaultEnabled = false), + + // RF-ENV-007: Modo Noturno (aquecimento ~3000K para redução de luz azul) + NIGHT_MODE("night_mode", defaultEnabled = false), + } + + /** Chave usada para persistir o brilho do ambiente 3D/Skybox (0.0f a 1.0f). Padrão: 1.0f (100%). */ + const val KEY_ENVIRONMENT_BRIGHTNESS = "environment_brightness" + + /** Chave usada para persistir a intensidade do Screen Glow (0=Off, 1=Subtle, 2=Strong). Padrão: 2. */ + const val KEY_SCREEN_GLOW_INTENSITY = "screen_glow_intensity" + + /** Chave usada para persistir a temperatura de cor em Kelvin (2700K a 6500K). Padrão: 6500.0f. */ + const val KEY_COLOR_TEMPERATURE = "color_temperature" + + const val SCREEN_GLOW_OFF = 0 + const val SCREEN_GLOW_SUBTLE = 1 + const val SCREEN_GLOW_STRONG = 2 + + const val SCREEN_GLOW_INTENSITY_OFF = 0.0f + const val SCREEN_GLOW_INTENSITY_SUBTLE = 0.45f + const val SCREEN_GLOW_INTENSITY_STRONG = 0.85f + + fun screenGlowModeToFloat(mode: Int): Float = when (mode) { + SCREEN_GLOW_SUBTLE -> SCREEN_GLOW_INTENSITY_SUBTLE + SCREEN_GLOW_STRONG -> SCREEN_GLOW_INTENSITY_STRONG + else -> SCREEN_GLOW_INTENSITY_OFF } /** Chave usada para persistir o modo de áudio espacial como Int (0/1/2). */ @@ -256,4 +282,62 @@ object FeatureFlags { .putInt(KEY_PACKED_ALPHA_CHOKE, choke.coerceIn(0, 100)) .apply() } + + /** Lê o brilho do ambiente virtual (0.0f a 1.0f). Padrão: 1.0f (100%). */ + fun getEnvironmentBrightness(context: Context): Float = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getFloat(KEY_ENVIRONMENT_BRIGHTNESS, 1.0f) + + /** Persiste o brilho do ambiente virtual. */ + fun setEnvironmentBrightness(context: Context, brightness: Float) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putFloat(KEY_ENVIRONMENT_BRIGHTNESS, brightness.coerceIn(0.0f, 1.0f)) + .apply() + } + + /** Lê o modo de intensidade do Screen Glow (0=Off, 1=Subtle, 2=Strong). Padrão: 2. */ + fun getScreenGlowIntensityMode(context: Context): Int { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + if (prefs.contains(KEY_SCREEN_GLOW_INTENSITY)) { + return prefs.getInt(KEY_SCREEN_GLOW_INTENSITY, SCREEN_GLOW_STRONG) + } + return if (isEnabled(context, Flag.AMBIENT_MODE)) SCREEN_GLOW_STRONG else SCREEN_GLOW_OFF + } + + /** Persiste o modo de intensidade do Screen Glow e sincroniza com a flag AMBIENT_MODE. */ + fun setScreenGlowIntensityMode(context: Context, mode: Int) { + val clamped = mode.coerceIn(SCREEN_GLOW_OFF, SCREEN_GLOW_STRONG) + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(KEY_SCREEN_GLOW_INTENSITY, clamped) + .putBoolean(Flag.AMBIENT_MODE.key, clamped != SCREEN_GLOW_OFF) + .apply() + } + + /** Lê a intensidade float do Screen Glow (0.0f, 0.45f ou 0.85f). */ + fun getScreenGlowIntensityFloat(context: Context): Float = + screenGlowModeToFloat(getScreenGlowIntensityMode(context)) + + /** Lê a temperatura de cor em Kelvin (2700K a 6500K). Padrão: 6500K. */ + fun getColorTemperature(context: Context): Float = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getFloat(KEY_COLOR_TEMPERATURE, 6500.0f) + + /** Persiste a temperatura de cor em Kelvin. */ + fun setColorTemperature(context: Context, kelvin: Float) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putFloat(KEY_COLOR_TEMPERATURE, kelvin.coerceIn(2700.0f, 6500.0f)) + .apply() + } + + /** Lê se o Modo Noturno está ativo. Padrão: false. */ + fun isNightModeEnabled(context: Context): Boolean = + isEnabled(context, Flag.NIGHT_MODE) + + /** Persiste o estado do Modo Noturno. */ + fun setNightModeEnabled(context: Context, enabled: Boolean) { + setEnabled(context, Flag.NIGHT_MODE, enabled) + } } diff --git a/app/src/main/java/com/tucavr/VRActivity.kt b/app/src/main/java/com/tucavr/VRActivity.kt index 466c2d7..72f6357 100644 --- a/app/src/main/java/com/tucavr/VRActivity.kt +++ b/app/src/main/java/com/tucavr/VRActivity.kt @@ -443,6 +443,12 @@ class VRActivity : NativeActivity() { // docs/reports/MODO-AMBIENTE.md: empurra o estado persistido do halo de luz ambiente nativeSetAmbientMode(FeatureFlags.isEnabled(this, FeatureFlags.Flag.AMBIENT_MODE)) + // RF-ENV-007: Ajuste de Iluminação e Cor do Ambiente (Fase 0.5 §4) + nativeSetEnvironmentBrightness(FeatureFlags.getEnvironmentBrightness(this)) + nativeSetScreenGlowIntensity(FeatureFlags.getScreenGlowIntensityFloat(this)) + nativeSetColorTemperature(FeatureFlags.getColorTemperature(this)) + nativeSetNightMode(FeatureFlags.isNightModeEnabled(this)) + // Fase 0.3 Seção 2: empurra o estado persistido do toggle de Passthrough. // A capacidade real (extensão XR_FB_passthrough presente) só é conhecida // depois que o C++ cria o XrInstance; o painel de controles consulta @@ -1848,6 +1854,11 @@ class VRActivity : NativeActivity() { // frame. Vulkan-only, GLES aceita a chamada mas não aplica. Só tem efeito visual // com ambiente Void ativo, fora de modos esfera e sem Passthrough ligado. external fun nativeSetAmbientMode(enabled: Boolean) + // RF-ENV-007: Ajuste de Iluminação e Cor do Ambiente (Fase 0.5 §4) + external fun nativeSetEnvironmentBrightness(brightness: Float) + external fun nativeSetScreenGlowIntensity(intensity: Float) + external fun nativeSetColorTemperature(kelvin: Float) + external fun nativeSetNightMode(enabled: Boolean) // Fase 0.3 Seção 2: Passthrough / Mixed Reality (Vulkan-only, XR_FB_passthrough). external fun nativeSetPassthroughEnabled(enabled: Boolean) external fun nativeIsPassthroughSupported(): Boolean diff --git a/app/src/main/java/com/tucavr/screens/SettingsScreen.kt b/app/src/main/java/com/tucavr/screens/SettingsScreen.kt index 238df00..6818e84 100644 --- a/app/src/main/java/com/tucavr/screens/SettingsScreen.kt +++ b/app/src/main/java/com/tucavr/screens/SettingsScreen.kt @@ -1,10 +1,13 @@ package com.tucavr.screens import android.content.Context +import android.content.res.ColorStateList import android.view.ViewGroup import android.widget.CheckBox import android.widget.LinearLayout import android.widget.ScrollView +import android.widget.SeekBar +import android.widget.TextView import com.tucavr.FeatureFlags import com.tucavr.R import com.tucavr.UpscalingModeStore @@ -15,6 +18,7 @@ import com.tucavr.designsystem.VoidFilterChip import com.tucavr.designsystem.VoidPanelChrome import com.tucavr.designsystem.VoidText import com.tucavr.designsystem.VoidTheme +import kotlin.math.roundToInt /** * Tela de Configurações — Seções "Vídeo" (Upscaling MQSR/SGSR1 e Foveated Rendering), @@ -61,12 +65,21 @@ class SettingsScreen( content.addView(buildUpscalingRow()) content.addView(buildFoveationRow()) + // Seção Iluminação e Ambiente (RF-ENV-007) + content.addView( + VoidText.title(context, context.getString(R.string.settings_section_lighting), sizeSp = 18f).apply { + setPadding(0, VoidTheme.dpToPx(context, 8f), 0, VoidTheme.dpToPx(context, 8f)) + } + ) + content.addView(buildEnvironmentBrightnessRow()) + content.addView(buildScreenGlowRow()) + content.addView(buildColorTemperatureRow()) content.addView( buildFlagRow( - labelRes = R.string.settings_ambient_mode_label, - descriptionRes = R.string.settings_ambient_mode_description, - flag = FeatureFlags.Flag.AMBIENT_MODE, - onChanged = { enabled -> activity.nativeSetAmbientMode(enabled) } + labelRes = R.string.settings_night_mode_label, + descriptionRes = R.string.settings_night_mode_description, + flag = FeatureFlags.Flag.NIGHT_MODE, + onChanged = { enabled -> activity.nativeSetNightMode(enabled) } ) ) @@ -416,4 +429,186 @@ class SettingsScreen( addView(chipContainer) } + + private fun buildEnvironmentBrightnessRow(): LinearLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { bottomMargin = VoidTheme.dpToPx(context, 16f) } + + val currentBrightness = FeatureFlags.getEnvironmentBrightness(context) + val initialPercent = (currentBrightness * 100f).roundToInt().coerceIn(0, 100) + + val headerRow = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + val titleView = VoidText.title(context, context.getString(R.string.settings_environment_brightness_label), sizeSp = 16f).apply { + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) + } + val valueView = VoidText.body( + context, + context.getString(R.string.settings_environment_brightness_percent, initialPercent), + sizeSp = 14f, + secondary = true + ) + headerRow.addView(titleView) + headerRow.addView(valueView) + addView(headerRow) + + addView( + VoidText.body(context, context.getString(R.string.settings_environment_brightness_description), sizeSp = 14f, secondary = true).apply { + setPadding(0, VoidTheme.dpToPx(context, 2f), 0, VoidTheme.dpToPx(context, 4f)) + } + ) + + val seekBar = SeekBar(context).apply { + max = 100 + progress = initialPercent + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + VoidTheme.dpToPx(context, 44f) + ) + progressTintList = ColorStateList.valueOf(VoidTheme.colorAccent) + thumbTintList = ColorStateList.valueOf(VoidTheme.colorAccent) + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(sb: SeekBar?, prog: Int, fromUser: Boolean) { + val brightness = prog / 100f + valueView.text = context.getString(R.string.settings_environment_brightness_percent, prog) + if (fromUser) { + FeatureFlags.setEnvironmentBrightness(context, brightness) + activity.nativeSetEnvironmentBrightness(brightness) + } + } + override fun onStartTrackingTouch(sb: SeekBar?) {} + override fun onStopTrackingTouch(sb: SeekBar?) {} + }) + } + addView(seekBar) + } + + private fun buildScreenGlowRow(): LinearLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { bottomMargin = VoidTheme.dpToPx(context, 16f) } + + addView( + VoidText.title(context, context.getString(R.string.settings_screen_glow_label), sizeSp = 16f).apply { + setPadding(0, 0, 0, VoidTheme.dpToPx(context, 4f)) + } + ) + + addView( + VoidText.body(context, context.getString(R.string.settings_screen_glow_description), sizeSp = 14f, secondary = true).apply { + setPadding(0, 0, 0, VoidTheme.dpToPx(context, 8f)) + } + ) + + val chipContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + val modes = listOf( + FeatureFlags.SCREEN_GLOW_OFF to R.string.settings_screen_glow_off, + FeatureFlags.SCREEN_GLOW_SUBTLE to R.string.settings_screen_glow_subtle, + FeatureFlags.SCREEN_GLOW_STRONG to R.string.settings_screen_glow_strong + ) + + val chips = mutableListOf() + val currentMode = FeatureFlags.getScreenGlowIntensityMode(context) + + modes.forEach { (mode, labelRes) -> + val chip = VoidFilterChip( + context = context, + text = context.getString(labelRes), + isSelectedChip = (mode == currentMode) + ).apply { + layoutParams = LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f + ).apply { + val margin = VoidTheme.dpToPx(context, 4f) + setMargins(margin, 0, margin, 0) + } + setOnClickListener { + chips.forEach { it.setSelectedState(false) } + setSelectedState(true) + FeatureFlags.setScreenGlowIntensityMode(context, mode) + val intensity = FeatureFlags.screenGlowModeToFloat(mode) + activity.nativeSetScreenGlowIntensity(intensity) + activity.nativeSetAmbientMode(mode != FeatureFlags.SCREEN_GLOW_OFF) + } + } + chips.add(chip) + chipContainer.addView(chip) + } + + addView(chipContainer) + } + + private fun buildColorTemperatureRow(): LinearLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { bottomMargin = VoidTheme.dpToPx(context, 16f) } + + val currentKelvin = FeatureFlags.getColorTemperature(context) + val initialProgress = ((currentKelvin - 2700f) / 100f).roundToInt().coerceIn(0, 38) + + val headerRow = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + val titleView = VoidText.title(context, context.getString(R.string.settings_color_temperature_label), sizeSp = 16f).apply { + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) + } + val valueView = VoidText.body( + context, + context.getString(R.string.settings_color_temperature_kelvin, currentKelvin.toInt()), + sizeSp = 14f, + secondary = true + ) + headerRow.addView(titleView) + headerRow.addView(valueView) + addView(headerRow) + + addView( + VoidText.body(context, context.getString(R.string.settings_color_temperature_description), sizeSp = 14f, secondary = true).apply { + setPadding(0, VoidTheme.dpToPx(context, 2f), 0, VoidTheme.dpToPx(context, 4f)) + } + ) + + val seekBar = SeekBar(context).apply { + max = 38 + progress = initialProgress + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + VoidTheme.dpToPx(context, 44f) + ) + progressTintList = ColorStateList.valueOf(VoidTheme.colorAccent) + thumbTintList = ColorStateList.valueOf(VoidTheme.colorAccent) + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(sb: SeekBar?, prog: Int, fromUser: Boolean) { + val kelvin = 2700f + prog * 100f + valueView.text = context.getString(R.string.settings_color_temperature_kelvin, kelvin.toInt()) + if (fromUser) { + FeatureFlags.setColorTemperature(context, kelvin) + activity.nativeSetColorTemperature(kelvin) + } + } + override fun onStartTrackingTouch(sb: SeekBar?) {} + override fun onStopTrackingTouch(sb: SeekBar?) {} + }) + } + addView(seekBar) + } } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ee4d1e8..f605e2e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -53,6 +53,22 @@ Medio Alto Automático (adaptativo) + Modo Ambiente + Añade un halo de luz suave detrás de la pantalla, con el color del propio video. Solo visible en el entorno Void. Solo Vulkan. + Iluminación y Entorno + Brillo del Entorno + Controla la intensidad de la iluminación de la sala virtual y skybox (0%% a 100%%). + %1$d%% + Luz Reactiva de Pantalla (Screen Glow) + Refleja el color dinámico del video en las paredes de la sala virtual y el espacio circundante. + Desactivado + Sutil + Fuerte + Temperatura de Color + Ajusta la calidez del color de la pantalla de video de cálido a neutro (2700K a 6500K). + %1$dK + Modo Nocturno + Reduce la emisión de luz azul (~3000K) para comodidad visual en entornos oscuros. Límite térmico alcanzado: reduciendo calidad de renderizado para enfriar el dispositivo. El video 8K con alta tasa de cuadros puede experimentar caídas de cuadros en hardware móvil. Pausar al salir diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ecf5d6b..1b61f23 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -55,6 +55,20 @@ Automatico (Adaptativo) Modo Ambiente Adiciona um halo de luz suave atras da tela, com a cor do proprio video. So visivel no ambiente Void. So no Vulkan. + Iluminação e Ambiente + Brilho do Ambiente + Controla a intensidade da iluminação da sala virtual e skybox (0%% a 100%%). + %1$d%% + Luz Reativa da Tela (Screen Glow) + Reflete a cor dinâmica do vídeo nas paredes da sala virtual e espaço ao redor. + Desligado + Sutil + Forte + Temperatura de Cor + Ajusta o calor da cor da tela de vídeo de quente a neutro (2700K a 6500K). + %1$dK + Modo Noturno + Reduz a emissão de luz azul (~3000K) para conforto visual em ambientes escuros. Limite termico atingido: reduzindo qualidade para resfriar o dispositivo. Video 8K com alta taxa de quadros pode apresentar quedas em hardware movel. Pausar ao sair diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18bea32..2da0403 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,20 @@ Auto (Adaptive) Ambient Mode Adds a soft glow behind the screen, colored from the video frame. Only visible in the Void environment. Vulkan only. + Environment & Lighting + Environment Brightness + Controls virtual room lighting and skybox brightness (0%% to 100%%). + %1$d%% + Reactive Screen Glow + Reflects dynamic video color onto virtual room walls and surrounding space. + Off + Subtle + Strong + Color Temperature + Adjusts the video screen color warmth from warm incandescent to neutral daylight. + %1$dK + Night Mode + Reduces blue light emission (~3000K) for comfortable viewing in dark environments. Thermal limit reached: reducing rendering quality to cool down device. High framerate 8K video may experience frame drops on mobile hardware. Pause on exit diff --git a/app/src/test/java/com/tucavr/EnvironmentLightingTest.kt b/app/src/test/java/com/tucavr/EnvironmentLightingTest.kt new file mode 100644 index 0000000..e241605 --- /dev/null +++ b/app/src/test/java/com/tucavr/EnvironmentLightingTest.kt @@ -0,0 +1,63 @@ +package com.tucavr + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Testes unitários para configurações e mapeamentos de iluminação e cor do ambiente (RF-ENV-007). + * Valida limites de clamping, conversões de modo de screen glow e flags de modo noturno. + */ +class EnvironmentLightingTest { + + @Test + fun testScreenGlowModeMapping() { + assertEquals(0.0f, FeatureFlags.screenGlowModeToFloat(FeatureFlags.SCREEN_GLOW_OFF), 0.001f) + assertEquals(0.45f, FeatureFlags.screenGlowModeToFloat(FeatureFlags.SCREEN_GLOW_SUBTLE), 0.001f) + assertEquals(0.85f, FeatureFlags.screenGlowModeToFloat(FeatureFlags.SCREEN_GLOW_STRONG), 0.001f) + + // Valores fora do intervalo devem mapear para 0.0f (desligado) + assertEquals(0.0f, FeatureFlags.screenGlowModeToFloat(-1), 0.001f) + assertEquals(0.0f, FeatureFlags.screenGlowModeToFloat(99), 0.001f) + } + + @Test + fun testScreenGlowConstantsMatchNativeContract() { + assertEquals(0, FeatureFlags.SCREEN_GLOW_OFF) + assertEquals(1, FeatureFlags.SCREEN_GLOW_SUBTLE) + assertEquals(2, FeatureFlags.SCREEN_GLOW_STRONG) + + assertEquals(0.0f, FeatureFlags.SCREEN_GLOW_INTENSITY_OFF, 0.001f) + assertEquals(0.45f, FeatureFlags.SCREEN_GLOW_INTENSITY_SUBTLE, 0.001f) + assertEquals(0.85f, FeatureFlags.SCREEN_GLOW_INTENSITY_STRONG, 0.001f) + } + + @Test + fun testEnvironmentBrightnessClampingContract() { + val testClamping = { value: Float -> value.coerceIn(0.0f, 1.0f) } + + assertEquals(0.0f, testClamping(-0.5f), 0.001f) + assertEquals(0.0f, testClamping(0.0f), 0.001f) + assertEquals(0.5f, testClamping(0.5f), 0.001f) + assertEquals(1.0f, testClamping(1.0f), 0.001f) + assertEquals(1.0f, testClamping(1.5f), 0.001f) + } + + @Test + fun testColorTemperatureClampingContract() { + val testClamping = { kelvin: Float -> kelvin.coerceIn(2700.0f, 6500.0f) } + + assertEquals(2700.0f, testClamping(1500.0f), 0.001f) + assertEquals(2700.0f, testClamping(2700.0f), 0.001f) + assertEquals(4500.0f, testClamping(4500.0f), 0.001f) + assertEquals(6500.0f, testClamping(6500.0f), 0.001f) + assertEquals(6500.0f, testClamping(9000.0f), 0.001f) + } + + @Test + fun testNightModeFlagContract() { + assertEquals("night_mode", FeatureFlags.Flag.NIGHT_MODE.key) + assertFalse(FeatureFlags.Flag.NIGHT_MODE.defaultEnabled) + } +} diff --git a/app/src/test/java/com/tucavr/FeatureFlagsTest.kt b/app/src/test/java/com/tucavr/FeatureFlagsTest.kt index 7885547..7dbb454 100644 --- a/app/src/test/java/com/tucavr/FeatureFlagsTest.kt +++ b/app/src/test/java/com/tucavr/FeatureFlagsTest.kt @@ -57,5 +57,8 @@ class FeatureFlagsTest { // Modo Ambiente (halo de luz) nunca validado em headset real, deve iniciar desabilitado org.junit.Assert.assertFalse("AMBIENT_MODE deve iniciar desabilitado por padrão", FeatureFlags.Flag.AMBIENT_MODE.defaultEnabled) + + // RF-ENV-007: Modo Noturno deve iniciar desabilitado por padrão + org.junit.Assert.assertFalse("NIGHT_MODE deve iniciar desabilitado por padrão", FeatureFlags.Flag.NIGHT_MODE.defaultEnabled) } } diff --git a/docs/phases/PHASE-0.5-PREMIUM.md b/docs/phases/PHASE-0.5-PREMIUM.md index 0435eea..2060de9 100644 --- a/docs/phases/PHASE-0.5-PREMIUM.md +++ b/docs/phases/PHASE-0.5-PREMIUM.md @@ -809,7 +809,7 @@ Screen Glow Effect: ### Tarefas -- [ ] **T4.1** — Implementar **Screen Glow** (C++/GLSL): +- [x] **T4.1** — Implementar **Screen Glow** (C++/GLSL): - Amostrar cor média do frame de vídeo atual (via mipmap ou downscale FBO) - Usar essa cor como fonte de luz difusa que ilumina o ambiente ao redor da tela - Suavizar temporalmente para evitar cintilação em cenas de corte rápido @@ -873,7 +873,7 @@ Screen Glow Effect: }; ``` -- [ ] **T4.2** — Atualizar **shaders do ambiente** para receber screen glow (GLSL): +- [x] **T4.2** — Atualizar **shaders do ambiente** para receber screen glow (GLSL): ```glsl // Uniforms adicionados ao shader PBR do ambiente uniform vec3 uScreenGlowColor; // Cor média da tela @@ -901,12 +901,12 @@ Screen Glow Effect: } ``` -- [ ] **T4.3** — Implementar **controle de brilho do ambiente** (Kotlin + C++): +- [x] **T4.3** — Implementar **controle de brilho do ambiente** (Kotlin + C++): - Slider na UI de configurações (0% = void total, 100% = iluminação original do lightmap) - Persistir em DataStore, enviar ao C++ via JNI - Aplicar via uniform `uEnvironmentBrightness` -- [ ] **T4.4** — Implementar **temperatura de cor / Night Mode** (GLSL): +- [x] **T4.4** — Implementar **temperatura de cor / Night Mode** (GLSL): ```glsl // Aplicar ao shader de vídeo (na tela virtual) uniform float uColorTemperature; // 2700 (quente) a 6500 (neutro) @@ -948,7 +948,7 @@ Screen Glow Effect: } ``` -- [ ] **T4.5** — Implementar **UI de ajustes de iluminação** (Kotlin): +- [x] **T4.5** — Implementar **UI de ajustes de iluminação** (Kotlin): - Slider "Brilho do ambiente" (0-100%) - Slider "Temperatura de cor" (2700K Quente ↔ 6500K Neutro) - Toggle "Screen Glow" (Desligado / Sutil / Forte) @@ -1062,12 +1062,12 @@ Screen Glow Effect: - [ ] Transição entre ambientes com fade suave (< 3s de loading) ### Iluminação -- [ ] Screen glow reflete cor média do vídeo nas paredes do ambiente -- [ ] Suavização temporal: sem cintilação em cenas de corte rápido -- [ ] Slider de brilho do ambiente funcional (0% = void, 100% = original) -- [ ] Slider de temperatura de cor funcional (quente ↔ neutro) -- [ ] Night mode reduz componente azul da tela de vídeo -- [ ] Todas as preferências persistidas entre sessões +- [x] Screen glow reflete cor média do vídeo nas paredes do ambiente +- [x] Suavização temporal: sem cintilação em cenas de corte rápido +- [x] Slider de brilho do ambiente funcional (0% = void, 100% = original) +- [x] Slider de temperatura de cor funcional (quente ↔ neutro) +- [x] Night mode reduz componente azul da tela de vídeo +- [x] Todas as preferências persistidas entre sessões ### Geral - [ ] Nenhuma regressão nos testes das fases 0.1–0.4 diff --git a/native/shaders/vulkan/environment.frag b/native/shaders/vulkan/environment.frag index cb6966b..52f8223 100644 --- a/native/shaders/vulkan/environment.frag +++ b/native/shaders/vulkan/environment.frag @@ -1,7 +1,17 @@ #version 450 -layout(location = 0) in vec3 vNormal; -layout(location = 1) in vec4 vColor; +layout(set = 0, binding = 0) uniform sampler2D screenAmbientTexture; + +layout(push_constant) uniform PushConstants { + mat4 mvp; + vec4 tintColor; + vec4 glowParams; // x: screenGlowIntensity (0.0-1.0), y: environmentBrightness (0.0-1.0), z/w: reserved + vec4 screenPos; // xyz: screenWorldPosition +} pc; + +layout(location = 0) in vec3 vWorldPosition; +layout(location = 1) in vec3 vNormal; +layout(location = 2) in vec4 vColor; layout(location = 0) out vec4 outColor; @@ -9,7 +19,20 @@ void main() { vec3 lightDir = normalize(vec3(0.2, 1.0, 0.4)); float diff = max(dot(normalize(vNormal), lightDir), 0.0); float ambient = 0.50; - vec3 lighting = vec3(ambient + diff * 0.50); + // Modulação pelo brilho configurado do ambiente (pc.glowParams.y) + vec3 baseLighting = vec3(ambient + diff * 0.50) * pc.glowParams.y; + + // Screen Glow: luz difusa emitida pela tela virtual sobre a malha da sala + vec3 glow = vec3(0.0); + if (pc.glowParams.x > 0.001) { + vec3 toScreen = pc.screenPos.xyz - vWorldPosition; + float dist = length(toScreen); + float attenuation = 1.0 / (1.0 + dist * dist * 0.25); + float nDotL = max(dot(normalize(vNormal), normalize(toScreen)), 0.0); + vec3 screenColor = texture(screenAmbientTexture, vec2(0.5, 0.5)).rgb; + glow = screenColor * (pc.glowParams.x * attenuation * nDotL); + } - outColor = vec4(vColor.rgb * lighting, vColor.a); + vec3 finalLighting = baseLighting + glow; + outColor = vec4(vColor.rgb * finalLighting, vColor.a); } diff --git a/native/shaders/vulkan/environment.vert b/native/shaders/vulkan/environment.vert index 7aa1f6a..8be5193 100644 --- a/native/shaders/vulkan/environment.vert +++ b/native/shaders/vulkan/environment.vert @@ -7,13 +7,17 @@ layout(location = 2) in vec4 inColor; layout(push_constant) uniform PushConstants { mat4 mvp; vec4 tintColor; + vec4 glowParams; // x: screenGlowIntensity (0.0-1.0), y: environmentBrightness (0.0-1.0), z/w: reserved + vec4 screenPos; // xyz: screenWorldPosition } pc; -layout(location = 0) out vec3 vNormal; -layout(location = 1) out vec4 vColor; +layout(location = 0) out vec3 vWorldPosition; +layout(location = 1) out vec3 vNormal; +layout(location = 2) out vec4 vColor; void main() { gl_Position = pc.mvp * vec4(inPosition, 1.0); + vWorldPosition = inPosition; vNormal = inNormal; vColor = inColor * pc.tintColor; } diff --git a/native/shaders/vulkan/stereo.frag b/native/shaders/vulkan/stereo.frag index ecf290f..d9e5e43 100644 --- a/native/shaders/vulkan/stereo.frag +++ b/native/shaders/vulkan/stereo.frag @@ -19,6 +19,7 @@ layout(location = 9) flat in int vChromaKeyEnabled; layout(location = 10) flat in uint vChromaColorRgb; layout(location = 11) flat in float vChromaSimilarity; layout(location = 12) flat in float vChromaSmoothness; +layout(location = 13) flat in float vColorTemperature; layout(location = 0) out vec4 outColor; @@ -50,6 +51,33 @@ vec3 TonemapHdrToSdr(vec3 hdrColor) { return pow(clamp(tonemapped, 0.0, 1.0), vec3(1.0 / 2.2)); } +// Planckian locus approximation (Kelvin 2700K a 6500K / Night Mode ~3000K) +vec3 ApplyColorTemperature(vec3 color, float tempK) { + float t = tempK / 100.0; + vec3 tempColor; + if (t <= 66.0) { + tempColor.r = 1.0; + } else { + tempColor.r = clamp(1.292 * pow(t - 60.0, -0.1332), 0.0, 1.0); + } + + if (t <= 66.0) { + tempColor.g = clamp(0.3901 * log(max(t, 1.0)) - 0.6318, 0.0, 1.0); + } else { + tempColor.g = clamp(1.130 * pow(t - 60.0, -0.0755), 0.0, 1.0); + } + + if (t >= 66.0) { + tempColor.b = 1.0; + } else if (t <= 19.0) { + tempColor.b = 0.0; + } else { + tempColor.b = clamp(0.5432 * log(max(t - 10.0, 1.0)) - 1.1962, 0.0, 1.0); + } + + return color * tempColor; +} + vec3 ApplySGSR1(vec2 uv, float sharpness, vec2 texelSize) { vec2 dx = vec2(texelSize.x, 0.0); vec2 dy = vec2(0.0, texelSize.y); @@ -181,6 +209,13 @@ void main() { color = TonemapHdrToSdr(color); } + if (vColorTemperature < 0.0) { + // Night Mode: reduz luz azul (~3000K) + color = ApplyColorTemperature(color, 3000.0); + } else if (abs(vColorTemperature - 6500.0) > 1.0 && vColorTemperature > 1000.0) { + color = ApplyColorTemperature(color, vColorTemperature); + } + // Calculo de Alpha: composicao do hemisferio, Chroma Key ou Packed Alpha (DeoVR/HereSphere) float finalAlpha = hemisphereAlpha; if (vChromaKeyEnabled == 1) { diff --git a/native/shaders/vulkan/stereo.vert b/native/shaders/vulkan/stereo.vert index e4a87d8..b86ab8e 100644 --- a/native/shaders/vulkan/stereo.vert +++ b/native/shaders/vulkan/stereo.vert @@ -28,6 +28,7 @@ layout(push_constant) uniform PushConstants { uint chromaColorRgb; // 0xRRGGBB (ex: 0x00FF00) float chromaSimilarity; // corte de distancia cromatica (ex: 0.35) float chromaSmoothness; // suavidade da transicao de borda (ex: 0.10) + float colorTemperature; // Kelvin (2700 a 6500) ou negativo (< 0.0) para Night Mode } pc; layout(location = 0) out vec2 vTexCoord; @@ -43,6 +44,7 @@ layout(location = 9) flat out int vChromaKeyEnabled; layout(location = 10) flat out uint vChromaColorRgb; layout(location = 11) flat out float vChromaSimilarity; layout(location = 12) flat out float vChromaSmoothness; +layout(location = 13) flat out float vColorTemperature; void main() { gl_Position = pc.mvp * vec4(inPosition, 1.0); @@ -59,4 +61,5 @@ void main() { vChromaColorRgb = pc.chromaColorRgb; vChromaSimilarity = pc.chromaSimilarity; vChromaSmoothness = pc.chromaSmoothness; + vColorTemperature = pc.colorTemperature; } diff --git a/native/shaders/vulkan/stereo_cubemap.frag b/native/shaders/vulkan/stereo_cubemap.frag index 3f7c521..5916ab8 100644 --- a/native/shaders/vulkan/stereo_cubemap.frag +++ b/native/shaders/vulkan/stereo_cubemap.frag @@ -18,6 +18,7 @@ layout(location = 7) flat in int vProjectionType; layout(location = 8) in vec3 vWorldDirection; layout(location = 9) flat in int vIsHdr; layout(location = 10) flat in vec2 vTexelSize; +layout(location = 11) flat in float vColorTemperature; layout(location = 0) out vec4 outColor; @@ -43,6 +44,33 @@ vec3 TonemapHdrToSdr(vec3 hdrColor) { return pow(clamp(tonemapped, 0.0, 1.0), vec3(1.0 / 2.2)); } +// Planckian locus approximation (Kelvin 2700K a 6500K / Night Mode ~3000K) +vec3 ApplyColorTemperature(vec3 color, float tempK) { + float t = tempK / 100.0; + vec3 tempColor; + if (t <= 66.0) { + tempColor.r = 1.0; + } else { + tempColor.r = clamp(1.292 * pow(t - 60.0, -0.1332), 0.0, 1.0); + } + + if (t <= 66.0) { + tempColor.g = clamp(0.3901 * log(max(t, 1.0)) - 0.6318, 0.0, 1.0); + } else { + tempColor.g = clamp(1.130 * pow(t - 60.0, -0.0755), 0.0, 1.0); + } + + if (t >= 66.0) { + tempColor.b = 1.0; + } else if (t <= 19.0) { + tempColor.b = 0.0; + } else { + tempColor.b = clamp(0.5432 * log(max(t - 10.0, 1.0)) - 1.1962, 0.0, 1.0); + } + + return color * tempColor; +} + // Kernel adaptativo SGSR1 para upscaling e nitidez de vídeo vec3 ApplySGSR1(vec2 uv, float sharpness, vec2 texelSize) { vec2 dx = vec2(texelSize.x, 0.0); @@ -255,5 +283,11 @@ void main() { if (vIsHdr != 0) { color = TonemapHdrToSdr(color); } + if (vColorTemperature < 0.0) { + // Night Mode: reduz luz azul (~3000K) + color = ApplyColorTemperature(color, 3000.0); + } else if (abs(vColorTemperature - 6500.0) > 1.0 && vColorTemperature > 1000.0) { + color = ApplyColorTemperature(color, vColorTemperature); + } outColor = vec4(color, 1.0); } diff --git a/native/shaders/vulkan/stereo_cubemap.vert b/native/shaders/vulkan/stereo_cubemap.vert index 7b8b14a..bc9b3c6 100644 --- a/native/shaders/vulkan/stereo_cubemap.vert +++ b/native/shaders/vulkan/stereo_cubemap.vert @@ -20,6 +20,11 @@ layout(push_constant) uniform PushConstants { int isHdr; // T-HDR: 1 = fonte PQ/HLG, aplicar tonemap no fragment shader float texelWidth; // 1.0 / videoWidth float texelHeight; // 1.0 / videoHeight + int chromaKeyEnabled; // alinhamento com StereoPushConstants + uint chromaColorRgb; + float chromaSimilarity; + float chromaSmoothness; + float colorTemperature; // Kelvin (2700 a 6500) ou negativo (< 0.0) para Night Mode } pc; layout(location = 0) out vec2 vTexCoord; @@ -33,6 +38,7 @@ layout(location = 7) flat out int vProjectionType; layout(location = 8) out vec3 vWorldDirection; layout(location = 9) flat out int vIsHdr; layout(location = 10) flat out vec2 vTexelSize; +layout(location = 11) flat out float vColorTemperature; void main() { gl_Position = pc.mvp * vec4(inPosition, 1.0); @@ -47,4 +53,5 @@ void main() { vWorldDirection = inPosition; vIsHdr = pc.isHdr; vTexelSize = vec2(pc.texelWidth, pc.texelHeight); + vColorTemperature = pc.colorTemperature; } diff --git a/native/shaders/vulkan/video.frag b/native/shaders/vulkan/video.frag index 657af4f..744bb46 100644 --- a/native/shaders/vulkan/video.frag +++ b/native/shaders/vulkan/video.frag @@ -15,6 +15,7 @@ layout(location = 5) flat in int vChromaKeyEnabled; layout(location = 6) flat in uint vChromaColorRgb; layout(location = 7) flat in float vChromaSimilarity; layout(location = 8) flat in float vChromaSmoothness; +layout(location = 9) flat in float vColorTemperature; layout(location = 0) out vec4 outColor; @@ -55,6 +56,33 @@ vec3 TonemapHdrToSdr(vec3 hdrColor) { return pow(clamp(tonemapped, 0.0, 1.0), vec3(1.0 / 2.2)); } +// Planckian locus approximation (Kelvin 2700K a 6500K / Night Mode ~3000K) +vec3 ApplyColorTemperature(vec3 color, float tempK) { + float t = tempK / 100.0; + vec3 tempColor; + if (t <= 66.0) { + tempColor.r = 1.0; + } else { + tempColor.r = clamp(1.292 * pow(t - 60.0, -0.1332), 0.0, 1.0); + } + + if (t <= 66.0) { + tempColor.g = clamp(0.3901 * log(max(t, 1.0)) - 0.6318, 0.0, 1.0); + } else { + tempColor.g = clamp(1.130 * pow(t - 60.0, -0.0755), 0.0, 1.0); + } + + if (t >= 66.0) { + tempColor.b = 1.0; + } else if (t <= 19.0) { + tempColor.b = 0.0; + } else { + tempColor.b = clamp(0.5432 * log(max(t - 10.0, 1.0)) - 1.1962, 0.0, 1.0); + } + + return color * tempColor; +} + // Kernel SGSR1 (Snapdragon Game Super Resolution v1 / 12-tap edge-aware) vec3 ApplySGSR1(vec2 uv, float sharpness, vec2 texelSize) { vec2 dx = vec2(texelSize.x, 0.0); @@ -125,6 +153,13 @@ void main() { color = TonemapHdrToSdr(color); } + if (vColorTemperature < 0.0) { + // Night Mode: reduz luz azul (~3000K) + color = ApplyColorTemperature(color, 3000.0); + } else if (abs(vColorTemperature - 6500.0) > 1.0 && vColorTemperature > 1000.0) { + color = ApplyColorTemperature(color, vColorTemperature); + } + float finalAlpha = 1.0; if (vChromaKeyEnabled == 1) { vec3 keyColor = vec3( diff --git a/native/shaders/vulkan/video.vert b/native/shaders/vulkan/video.vert index 1eb0ca3..f28ba78 100644 --- a/native/shaders/vulkan/video.vert +++ b/native/shaders/vulkan/video.vert @@ -8,15 +8,16 @@ layout(location = 1) in vec2 inTexCoord; layout(push_constant) uniform PushConstants { mat4 mvp; - float sharpness; // 0.0 = amostragem bilinear direta, > 0.0 = forca do kernel SGSR1 - int upscalingMode; // 0 = Off, 1 = Quality, 2 = Performance, 3 = Auto - int isHdr; // T-HDR: 1 = fonte PQ/HLG, aplicar tonemap no fragment shader - float texelWidth; // 1.0 / videoWidth - float texelHeight; // 1.0 / videoHeight + float sharpness; // 0.0 = amostragem bilinear direta, > 0.0 = forca do kernel SGSR1 + int upscalingMode; // 0 = Off, 1 = Quality, 2 = Performance, 3 = Auto + int isHdr; // T-HDR: 1 = fonte PQ/HLG, aplicar tonemap no fragment shader + float texelWidth; // 1.0 / videoWidth + float texelHeight; // 1.0 / videoHeight int chromaKeyEnabled; // 0 = desligado, 1 = ativo uint chromaColorRgb; // 0xRRGGBB (ex: 0x00FF00) float chromaSimilarity; // corte de distancia cromatica (ex: 0.35) float chromaSmoothness; // suavidade da transicao de borda (ex: 0.10) + float colorTemperature; // Kelvin (2700 a 6500) ou negativo (< 0.0) para Night Mode } pc; layout(location = 0) out vec2 vTexCoord; @@ -28,6 +29,7 @@ layout(location = 5) flat out int vChromaKeyEnabled; layout(location = 6) flat out uint vChromaColorRgb; layout(location = 7) flat out float vChromaSimilarity; layout(location = 8) flat out float vChromaSmoothness; +layout(location = 9) flat out float vColorTemperature; void main() { gl_Position = pc.mvp * vec4(inPosition, 1.0); @@ -40,4 +42,5 @@ void main() { vChromaColorRgb = pc.chromaColorRgb; vChromaSimilarity = pc.chromaSimilarity; vChromaSmoothness = pc.chromaSmoothness; + vColorTemperature = pc.colorTemperature; } diff --git a/native/src/vr_player_ambient.h b/native/src/vr_player_ambient.h index 5678ad4..19916d4 100644 --- a/native/src/vr_player_ambient.h +++ b/native/src/vr_player_ambient.h @@ -35,6 +35,8 @@ constexpr float kAmbientEdgeWidth = 0.22f; // Intensidade maxima do halo (multiplicador de alpha) — ponto de partida // pra calibracao em headset (F5 do relatorio). constexpr float kAmbientMaxIntensity = 0.85f; +constexpr float kAmbientIntensitySubtle = 0.45f; +constexpr float kAmbientIntensityStrong = 0.85f; // Suavizacao temporal (2.3 do relatorio, "obrigatoria, nao polimento") — // tempo para o halo convergir pra uma nova cor apos uma mudanca abrupta @@ -59,3 +61,9 @@ constexpr uint32_t kAmbientDecimationFrames = 3; constexpr float kAmbientDownsampleKernelScale = 2.0f; } // namespace vrplayer + +// Getters de iluminação e cor do ambiente (RF-ENV-007) +float get_environment_brightness(); +float get_screen_glow_intensity(); +float get_color_temperature(); +bool get_night_mode_enabled(); diff --git a/native/src/vr_player_app_vulkan.cpp b/native/src/vr_player_app_vulkan.cpp index 87242c1..aedee2e 100644 --- a/native/src/vr_player_app_vulkan.cpp +++ b/native/src/vr_player_app_vulkan.cpp @@ -1229,6 +1229,7 @@ struct VideoPushConstants { uint32_t chromaColorRgb; float chromaSimilarity; float chromaSmoothness; + float colorTemperature; }; // Estagio 4: push constant para UI (MVP + alpha) @@ -1259,6 +1260,7 @@ struct StereoPushConstants { uint32_t chromaColorRgb; float chromaSimilarity; float chromaSmoothness; + float colorTemperature; }; struct BeamPushConstants { @@ -4901,6 +4903,8 @@ struct EnvironmentVertex { struct EnvironmentPushConstants { Mat4 mvp; XrVector4f tintColor; + XrVector4f glowParams; // x: screenGlowIntensity (0.0-1.0), y: environmentBrightness (0.0-1.0), z/w: reserved + XrVector4f screenPos; // xyz: screenWorldPosition }; static void CreateEnvironmentPipeline(AppState& state) { @@ -4976,11 +4980,13 @@ static void CreateEnvironmentPipeline(AppState& state) { dynamicState.pDynamicStates = dynamicStates; VkPushConstantRange pushConstantRange{}; - pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pushConstantRange.offset = 0; pushConstantRange.size = sizeof(EnvironmentPushConstants); VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &state.uiDescriptorSetLayout; layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &pushConstantRange; @@ -5262,9 +5268,10 @@ static void DrawSkyboxIfLoaded( viewNoTrans.m[13] = 0.0f; viewNoTrans.m[14] = 0.0f; + const float skyboxBrightness = get_environment_brightness(); SkyboxPushConstants skyboxPc{}; skyboxPc.mvp = Mat4Multiply(Mat4Multiply(proj, viewNoTrans), skyboxModel); - skyboxPc.tintColor = {1.0f, 1.0f, 1.0f, 1.0f}; + skyboxPc.tintColor = {skyboxBrightness, skyboxBrightness, skyboxBrightness, 1.0f}; vkCmdPushConstants( cmd, state.skyboxPipelineLayout, @@ -5495,7 +5502,7 @@ static bool LoadEnvironmentMesh(AppState& state, const std::string& envId) { } static void DrawEnvironmentIfLoaded( - AppState& state, VkCommandBuffer cmd, const Mat4& proj, const Mat4& view) { + AppState& state, VkCommandBuffer cmd, const Mat4& proj, const Mat4& view, XrVector3f headCenter) { if (!state.envMeshLoaded || state.envPipeline == VK_NULL_HANDLE || state.envVertexBuffer == VK_NULL_HANDLE || state.passthroughActive) { return; @@ -5506,17 +5513,34 @@ static void DrawEnvironmentIfLoaded( vkCmdBindVertexBuffers(cmd, 0, 1, &state.envVertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, state.envIndexBuffer, 0, VK_INDEX_TYPE_UINT32); + VkDescriptorSet envAmbientDs = (state.ambientDescriptorSet[state.ambientDisplayIndex] != VK_NULL_HANDLE) + ? state.ambientDescriptorSet[state.ambientDisplayIndex] + : state.uiDescriptorSet; + if (envAmbientDs != VK_NULL_HANDLE) { + vkCmdBindDescriptorSets( + cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, state.envPipelineLayout, + 0, 1, &envAmbientDs, 0, nullptr); + } + + const float envBrightness = get_environment_brightness(); EnvironmentPushConstants envPc{}; Mat4 envModel = Mat4Multiply( Mat4Translation(state.sceneTranslationOffset.x, state.sceneTranslationOffset.y, state.sceneTranslationOffset.z), Mat4RotationY(state.sceneYawOffset) ); envPc.mvp = Mat4Multiply(Mat4Multiply(proj, view), envModel); - envPc.tintColor = {1.0f, 1.0f, 1.0f, 1.0f}; + envPc.tintColor = {envBrightness, envBrightness, envBrightness, 1.0f}; + + // Screen Glow difuso nos ambientes 3D (Fase 0.5 §4 T4.1 / T4.2) + SceneTransforms scene = ComputeSceneTransforms(state, headCenter); + const bool is3dRoom = (state.currentEnvironmentId == "cinema" || state.currentEnvironmentId == "living_room"); + const float glowIntensity = (is3dRoom && state.thermalLevel < 2) ? state.ambientIntensity : 0.0f; + envPc.glowParams = {glowIntensity, envBrightness, 0.0f, 0.0f}; + envPc.screenPos = {scene.screenPos.x, scene.screenPos.y, scene.screenPos.z, 1.0f}; vkCmdPushConstants( cmd, state.envPipelineLayout, - VK_SHADER_STAGE_VERTEX_BIT, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(envPc), &envPc); vkCmdDrawIndexed(cmd, state.envIndexCount, 1, 0, 0, 0); @@ -5987,7 +6011,7 @@ static void RecordAmbientDownsample(AppState& state, VkCommandBuffer cmd) { // e o ramo nao-esfera/nao-cubemap de RecordStereoFrame) — RecordFallbackQuad // e RecordPhotoFrame nao chamam esta funcao (sem video, sem cor pra derivar). static void DrawAmbientHalo(AppState& state, VkCommandBuffer cmd, const Mat4& proj, const Mat4& view, XrVector3f headCenter) { - if (state.ambientIntensity <= 0.001f || state.ambientHaloPipeline == VK_NULL_HANDLE) { + if (state.ambientIntensity <= 0.001f || state.ambientHaloPipeline == VK_NULL_HANDLE || state.currentEnvironmentId != "void") { return; } @@ -6088,7 +6112,7 @@ void RecordFallbackQuad( vkCmdSetScissor(cmd, 0, 1, &scissor); DrawSkyboxIfLoaded(state, cmd, proj, view); - DrawEnvironmentIfLoaded(state, cmd, proj, view); + DrawEnvironmentIfLoaded(state, cmd, proj, view, headCenter); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, state.pipeline); @@ -6402,7 +6426,7 @@ void RecordVideoFlat( vkCmdSetScissor(cmd, 0, 1, &scissor); DrawSkyboxIfLoaded(state, cmd, proj, view); - DrawEnvironmentIfLoaded(state, cmd, proj, view); + DrawEnvironmentIfLoaded(state, cmd, proj, view, headCenter); DrawAmbientHalo(state, cmd, proj, view, headCenter); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, state.videoPipeline); @@ -6430,6 +6454,7 @@ void RecordVideoFlat( pc.chromaColorRgb = get_chroma_key_color(); pc.chromaSimilarity = get_chroma_key_similarity(); pc.chromaSmoothness = get_chroma_key_smoothness(); + pc.colorTemperature = get_night_mode_enabled() ? -3000.0f : get_color_temperature(); vkCmdPushConstants( cmd, state.videoPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, @@ -6499,7 +6524,7 @@ void RecordStereoFrame( const bool cubemapMode = IsCubemapMode(state.screenMode); if (!sphereMode && !cubemapMode) { DrawSkyboxIfLoaded(state, cmd, proj, view); - DrawEnvironmentIfLoaded(state, cmd, proj, view); + DrawEnvironmentIfLoaded(state, cmd, proj, view, headCenter); DrawAmbientHalo(state, cmd, proj, view, headCenter); } @@ -6541,6 +6566,7 @@ void RecordStereoFrame( spc.chromaColorRgb = get_chroma_key_color(); spc.chromaSimilarity = get_chroma_key_similarity(); spc.chromaSmoothness = get_chroma_key_smoothness(); + spc.colorTemperature = get_night_mode_enabled() ? -3000.0f : get_color_temperature(); vkCmdPushConstants(cmd, state.stereoPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(spc), &spc); @@ -6607,7 +6633,7 @@ void RecordPhotoFrame( if (!sphereMode) { DrawSkyboxIfLoaded(state, cmd, proj, view); - DrawEnvironmentIfLoaded(state, cmd, proj, view); + DrawEnvironmentIfLoaded(state, cmd, proj, view, headCenter); } vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, @@ -6641,6 +6667,7 @@ void RecordPhotoFrame( spc.chromaColorRgb = get_chroma_key_color(); spc.chromaSimilarity = get_chroma_key_similarity(); spc.chromaSmoothness = get_chroma_key_smoothness(); + spc.colorTemperature = 6500.0f; vkCmdPushConstants(cmd, state.photoPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(spc), &spc); diff --git a/native/src/vr_player_input_vulkan.h b/native/src/vr_player_input_vulkan.h index a168799..2826256 100644 --- a/native/src/vr_player_input_vulkan.h +++ b/native/src/vr_player_input_vulkan.h @@ -607,19 +607,22 @@ inline void UpdateInteraction(AppState& state, XrTime predictedDisplayTime, XrVe // por MoveTowards em vez de aplicado direto — evita um "pop" visivel // quando o usuario liga/desliga o toggle ou entra/sai do ambiente Void. { + const float configuredIntensity = get_screen_glow_intensity(); + const bool is3dRoom = (state.currentEnvironmentId == "cinema" || state.currentEnvironmentId == "living_room"); const bool ambientGatesPass = get_ambient_mode_enabled() != 0 && !IsSphereMode(state.screenMode) && !IsCubemapMode(state.screenMode) && state.thermalLevel < 2 && state.activeVideoFrame != nullptr && - state.currentEnvironmentId == "void" && + (state.currentEnvironmentId == "void" || is3dRoom) && !state.passthroughActive; - const float ambientTarget = ambientGatesPass ? vrplayer::kAmbientMaxIntensity : 0.0f; + const float maxTarget = (configuredIntensity > 0.001f) ? configuredIntensity : vrplayer::kAmbientMaxIntensity; + const float ambientTarget = ambientGatesPass ? maxTarget : 0.0f; const float ambientFadeStep = (vrplayer::kAmbientColorSmoothSeconds > 0.0f) - ? (dt / vrplayer::kAmbientColorSmoothSeconds) * vrplayer::kAmbientMaxIntensity - : vrplayer::kAmbientMaxIntensity; + ? (dt / vrplayer::kAmbientColorSmoothSeconds) * maxTarget + : maxTarget; state.ambientIntensity = MoveTowards(state.ambientIntensity, ambientTarget, ambientFadeStep); } diff --git a/native/src/vr_player_jni_vulkan.cpp b/native/src/vr_player_jni_vulkan.cpp index e24c0aa..248d437 100644 --- a/native/src/vr_player_jni_vulkan.cpp +++ b/native/src/vr_player_jni_vulkan.cpp @@ -17,6 +17,7 @@ #include #include #include +#include "vr_player_ambient.h" extern std::string g_sessionId; extern std::mutex g_sessionIdMutex; @@ -668,6 +669,57 @@ Java_com_tucavr_VRActivity_nativeSetAmbientMode(JNIEnv*, jobject, jboolean enabl set_ambient_mode_enabled(enabled ? 1 : 0); } +// RF-ENV-007: Ajuste de Iluminação e Cor do Ambiente (Fase 0.5 §4) +static std::atomic g_environmentBrightness{1.0f}; +static std::atomic g_screenGlowIntensity{0.85f}; +static std::atomic g_colorTemperature{6500.0f}; +static std::atomic g_nightModeEnabled{false}; + +float get_environment_brightness() { + return g_environmentBrightness.load(std::memory_order_relaxed); +} + +float get_screen_glow_intensity() { + return g_screenGlowIntensity.load(std::memory_order_relaxed); +} + +float get_color_temperature() { + return g_colorTemperature.load(std::memory_order_relaxed); +} + +bool get_night_mode_enabled() { + return g_nightModeEnabled.load(std::memory_order_relaxed); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_tucavr_VRActivity_nativeSetEnvironmentBrightness(JNIEnv*, jobject, jfloat brightness) { + float clamped = brightness; + if (clamped < 0.0f) clamped = 0.0f; + if (clamped > 1.0f) clamped = 1.0f; + g_environmentBrightness.store(clamped, std::memory_order_relaxed); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_tucavr_VRActivity_nativeSetScreenGlowIntensity(JNIEnv*, jobject, jfloat intensity) { + float clamped = intensity; + if (clamped < 0.0f) clamped = 0.0f; + if (clamped > 1.0f) clamped = 1.0f; + g_screenGlowIntensity.store(clamped, std::memory_order_relaxed); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_tucavr_VRActivity_nativeSetColorTemperature(JNIEnv*, jobject, jfloat kelvin) { + float clamped = kelvin; + if (clamped < 2700.0f) clamped = 2700.0f; + if (clamped > 6500.0f) clamped = 6500.0f; + g_colorTemperature.store(clamped, std::memory_order_relaxed); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_tucavr_VRActivity_nativeSetNightMode(JNIEnv*, jobject, jboolean enabled) { + g_nightModeEnabled.store(enabled != JNI_FALSE, std::memory_order_relaxed); +} + extern "C" JNIEXPORT void JNICALL Java_com_tucavr_VRActivity_nativeSetUpscalingMode(JNIEnv*, jobject, jint mode) { set_upscaling_mode(static_cast(mode < 0 ? 0 : mode));