diff --git a/build.gradle b/build.gradle index 113b992314..8dade8c48e 100644 --- a/build.gradle +++ b/build.gradle @@ -182,17 +182,56 @@ task configureAndroidNDK { ndkBuildFile = "ndk-build.cmd" } - // ndkPath is defined in the root project gradle.properties file - String ndkBuildPath = ndkPath + File.separator + ndkBuildFile - //Use the environment variable for the NDK location if defined - if (System.env.ANDROID_NDK != null) { - ndkBuildPath = System.env.ANDROID_NDK + File.separator + ndkBuildFile + def ndkCandidates = [] + if (System.env.ANDROID_NDK) { + ndkCandidates << file(System.env.ANDROID_NDK) + } + if (System.env.ANDROID_NDK_HOME) { + ndkCandidates << file(System.env.ANDROID_NDK_HOME) + } + if (project.hasProperty('ndkPath') && ndkPath) { + ndkCandidates << file(ndkPath) } - if (new File(ndkBuildPath).exists()) { + def localProperties = file('local.properties') + if (localProperties.isFile()) { + Properties properties = new Properties() + localProperties.withInputStream { properties.load(it) } + if (properties.getProperty('ndk.dir')) { + ndkCandidates << file(properties.getProperty('ndk.dir')) + } + if (properties.getProperty('sdk.dir')) { + ndkCandidates.addAll(findAndroidNdkDirs(file(properties.getProperty('sdk.dir')))) + } + } + + if (System.env.ANDROID_HOME) { + ndkCandidates.addAll(findAndroidNdkDirs(file(System.env.ANDROID_HOME))) + } + if (System.env.ANDROID_SDK_ROOT) { + ndkCandidates.addAll(findAndroidNdkDirs(file(System.env.ANDROID_SDK_ROOT))) + } + ndkCandidates.addAll(findAndroidNdkDirs(file("${System.properties['user.home']}/Android/Sdk"))) + + def ndkBuildPath = ndkCandidates.collect { new File(it, ndkBuildFile) }.find { it.isFile() } + if (ndkBuildPath != null) { ndkExists = true - ndkCommandPath = ndkBuildPath + ndkCommandPath = ndkBuildPath.absolutePath + } +} + +def findAndroidNdkDirs(File sdkDir) { + def ndkDirs = [] + def nestedSdkDir = new File(sdkDir, 'Sdk') + if (nestedSdkDir.isDirectory()) { + ndkDirs.addAll(findAndroidNdkDirs(nestedSdkDir)) + } + def sideBySideDir = new File(sdkDir, 'ndk') + if (sideBySideDir.isDirectory()) { + ndkDirs.addAll(sideBySideDir.listFiles()?.findAll { it.isDirectory() }?.sort { a, b -> b.name <=> a.name } ?: []) } + ndkDirs << new File(sdkDir, 'ndk-bundle') + return ndkDirs } gradle.rootProject.ext.set("usePrebuildNatives", buildNativeProjects!="true"); diff --git a/common-android-app.gradle b/common-android-app.gradle index 703536bd27..8670bf7e78 100644 --- a/common-android-app.gradle +++ b/common-android-app.gradle @@ -6,6 +6,6 @@ version = jmeFullVersion repositories { mavenCentral() maven { - url "http://nifty-gui.sourceforge.net/nifty-maven-repo" + url "https://nifty-gui.sourceforge.net/nifty-maven-repo" } } diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index f7f2e48481..5491d4eb55 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -61,7 +61,9 @@ reporting { def jacocoSubprojects = subprojects.findAll { p -> (p.file("src/main/java").exists() || p.file("src/main/groovy").exists()) && - (p.file("src/test/java").exists() || p.file("src/test/groovy").exists()) + (p.file("src/test/java").exists() || p.file("src/test/groovy").exists()) && + p.tasks.findByName('test') && + p.tasks.findByName('jacocoTestReport') } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5151b44419..830a8d203a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,9 +11,11 @@ spotbugs = "4.9.8" [libraries] androidx-annotation = "androidx.annotation:annotation:1.7.1" +androidx-fragment = "androidx.fragment:fragment:1.8.9" androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.7.0" android-build-gradle = "com.android.tools.build:gradle:9.1.0" android-support-appcompat = "com.android.support:appcompat-v7:28.0.0" +androidx-test-runner = "androidx.test:runner:1.7.0" gradle-git = "org.ajoberstar:gradle-git:1.2.0" groovy-test = "org.apache.groovy:groovy-test:4.0.31" gson = "com.google.code.gson:gson:2.13.2" @@ -23,6 +25,7 @@ jinput = "net.java.jinput:jinput:2.0.9" jna = "net.java.dev.jna:jna:5.18.1" jnaerator-runtime = "com.nativelibs4java:jnaerator-runtime:0.12" junit-bom = "org.junit:junit-bom:5.13.4" +junit4 = "junit:junit:4.13.2" junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } lwjgl2 = "org.jmonkeyengine:lwjgl:2.9.5" diff --git a/jme3-android-examples/build.gradle b/jme3-android-examples/build.gradle index 66c591a931..2aeab98640 100644 --- a/jme3-android-examples/build.gradle +++ b/jme3-android-examples/build.gradle @@ -1,5 +1,7 @@ apply plugin: 'com.android.application' +def examplesJar = project(':jme3-examples').tasks.named('jar') + android { namespace "org.jmonkeyengine.jme3androidexamples" compileSdk 34 @@ -16,12 +18,13 @@ android { targetSdk 34 // Android 14 versionCode 1 versionName "1.0" // TODO: from settings.gradle + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -40,23 +43,106 @@ android { srcDir '../jme3-testdata/src/main/resources' srcDir '../jme3-examples/src/main/resources' } + jniLibs { + srcDir '../build/native/openalsoft' + srcDir '../build/native/decode' + srcDir '../build/native/allocator' + } } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation libs.junit4 - implementation libs.androidx.appcompat - + testImplementation platform(libs.junit.bom) + testImplementation libs.junit.jupiter + testRuntimeOnly libs.junit.platform.launcher + androidTestImplementation libs.junit4 + androidTestImplementation libs.androidx.test.runner + implementation libs.androidx.fragment implementation project(':jme3-core') implementation project(':jme3-android') implementation project(':jme3-android-native') implementation project(':jme3-effects') implementation project(':jme3-jbullet') + implementation project(':jme3-jogg') implementation project(':jme3-networking') implementation project(':jme3-niftygui') implementation project(':jme3-plugins') + implementation project(':jme3-plugins-json') + implementation project(':jme3-plugins-json-gson') implementation project(':jme3-terrain') - implementation fileTree(dir: '../jme3-examples/build/libs', include: ['*.jar'], exclude: ['*sources*.*']) + implementation files(examplesJar.flatMap { it.archiveFile }) +} + +tasks.named('preBuild') { + dependsOn examplesJar + if (buildNativeProjects == "true") { + dependsOn ':jme3-android-native:updatePreCompiledOpenAlSoftLibs' + dependsOn ':jme3-android-native:updatePreCompiledLibs' + dependsOn ':jme3-android-native:updatePreCompiledLibsBufferAllocator' + } else if (skipPrebuildLibraries != "true") { + dependsOn ':jme3-android-native:copyPreCompiledOpenAlSoftLibs' + dependsOn ':jme3-android-native:copyPreCompiledLibs' + dependsOn ':jme3-android-native:copyPreCompiledLibsBufferAllocator' + } +} + +tasks.register('installAndroidExamples', Exec) { + group = 'application' + description = 'Install the Android examples selector on a connected emulator or device.' + + dependsOn tasks.named('assembleDebug') + + doFirst { + def adbExecutable = findAdbExecutable() + if (adbExecutable == null) { + throw new GradleException("ADB not found. Set -Pandroid.sdk.path, ANDROID_HOME, or ANDROID_SDK_ROOT.") + } + def apkFile = layout.buildDirectory.file('outputs/apk/debug/jme3-android-examples-debug.apk').get().asFile + if (!apkFile.isFile()) { + throw new GradleException("Debug APK not found at ${apkFile}.") + } + executable adbExecutable.absolutePath + args 'install', '-r', apkFile.absolutePath + } +} + +tasks.register('runAndroidExamples', Exec) { + group = 'application' + description = 'Install and launch the Android examples selector on a connected emulator or device.' + + dependsOn tasks.named('installAndroidExamples') + + doFirst { + def adbExecutable = findAdbExecutable() + if (adbExecutable == null) { + throw new GradleException("ADB not found. Set -Pandroid.sdk.path, ANDROID_HOME, or ANDROID_SDK_ROOT.") + } + executable adbExecutable.absolutePath + } + + args 'shell', 'am', 'start', + '-n', 'org.jmonkeyengine.jme3androidexamples/.MainActivity' +} + +def findAdbExecutable() { + def adbName = System.properties['os.name'].toLowerCase().contains('windows') ? 'adb.exe' : 'adb' + def sdkDirs = [] + if (project.findProperty('android.sdk.path')) { + sdkDirs << file(project.findProperty('android.sdk.path')) + } + if (System.env.ANDROID_HOME) { + sdkDirs << file(System.env.ANDROID_HOME) + } + if (System.env.ANDROID_SDK_ROOT) { + sdkDirs << file(System.env.ANDROID_SDK_ROOT) + } + sdkDirs << file("${System.properties['user.home']}/Android/Sdk") + + def expandedSdkDirs = sdkDirs.collectMany { sdkDir -> + [sdkDir, new File(sdkDir, 'Sdk')] + }.unique { it.absolutePath } + + return expandedSdkDirs.collect { new File(new File(it, 'platform-tools'), adbName) }.find { it.isFile() } } diff --git a/jme3-android-examples/src/androidTest/java/org/jmonkeyengine/jme3androidexamples/ApplicationTest.java b/jme3-android-examples/src/androidTest/java/org/jmonkeyengine/jme3androidexamples/ApplicationTest.java index dff82ddb86..b10ebf4789 100644 --- a/jme3-android-examples/src/androidTest/java/org/jmonkeyengine/jme3androidexamples/ApplicationTest.java +++ b/jme3-android-examples/src/androidTest/java/org/jmonkeyengine/jme3androidexamples/ApplicationTest.java @@ -1,13 +1,19 @@ package org.jmonkeyengine.jme3androidexamples; -import android.app.Application; -import android.test.ApplicationTestCase; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; /** * Testing Fundamentals */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); +public class ApplicationTest { + @Test + public void testApplicationPackage() { + String packageName = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getPackageName(); + assertEquals("org.jmonkeyengine.jme3androidexamples", packageName); } -} \ No newline at end of file +} diff --git a/jme3-android-examples/src/main/AndroidManifest.xml b/jme3-android-examples/src/main/AndroidManifest.xml index 95789aa79c..541dfdc356 100644 --- a/jme3-android-examples/src/main/AndroidManifest.xml +++ b/jme3-android-examples/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ android:theme="@style/AppTheme"> @@ -25,9 +26,9 @@ - + @@ -41,6 +42,7 @@ android:normalScreens="true" android:smallScreens="true"/> + diff --git a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java index 0e2faeea3a..3a3e399930 100644 --- a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java +++ b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java @@ -2,6 +2,8 @@ import android.os.Bundle; import com.jme3.app.AndroidHarnessFragment; +import com.jme3.app.LegacyApplication; +import com.jme3.system.AppSettings; import java.util.logging.Level; import java.util.logging.LogManager; @@ -9,53 +11,14 @@ * A placeholder fragment containing a jME GLSurfaceView. */ public class JmeFragment extends AndroidHarnessFragment { + private String appClass; + private boolean joystickEventsEnabled; + private boolean keyEventsEnabled = true; + private boolean mouseEventsEnabled = true; public JmeFragment() { - // Set the desired EGL configuration - eglBitsPerPixel = 24; - eglAlphaBits = 0; - eglDepthBits = 16; - eglSamples = 0; - eglStencilBits = 0; - - // Set the maximum framerate - // (default = -1 for unlimited) - frameRate = -1; - - // Set the maximum resolution dimension - // (the smaller side, height or width, is set automatically - // to maintain the original device screen aspect ratio) - // (default = -1 to match device screen resolution) - maxResolutionDimension = -1; - - /* - Skip these settings and use the settings stored in the Bundle retrieved during onCreate. - - // Set main project class (fully qualified path) - appClass = ""; - - // Set input configuration settings - joystickEventsEnabled = false; - keyEventsEnabled = true; - mouseEventsEnabled = true; - */ - - // Set application exit settings finishOnAppStop = true; - handleExitHook = true; - exitDialogTitle = "Do you want to exit?"; - exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; - - // Set splash screen resource id, if used - // (default = 0, no splash screen) - // For example, if the image file name is "splash"... - // splashPicID = R.drawable.splash; - splashPicID = 0; -// splashPicID = R.drawable.android_splash; - - // Set the default logging level (default=Level.INFO, Level.ALL=All Debug Info) LogManager.getLogManager().getLogger("").setLevel(Level.INFO); - } @Override @@ -82,4 +45,25 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } + + @Override + protected LegacyApplication createApplication() throws Exception { + Class> clazz = Class.forName(appClass); + return (LegacyApplication) clazz.getDeclaredConstructor().newInstance(); + } + + @Override + protected void configureSettings(AppSettings settings) { + settings.setEmulateMouse(mouseEventsEnabled); + settings.setUseJoysticks(joystickEventsEnabled); + settings.setEmulateKeyboard(keyEventsEnabled); + + settings.setBitsPerPixel(24); + settings.setAlphaBits(0); + settings.setGammaCorrection(true); + settings.setDepthBits(16); + settings.setSamples(4); + settings.setStencilBits(0); + settings.setFrameRate(-1); + } } diff --git a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java index d02f7527ca..960e53bc43 100644 --- a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java +++ b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java @@ -2,8 +2,8 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.app.Activity; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -20,6 +20,7 @@ import dalvik.system.DexFile; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -30,7 +31,7 @@ * applications that are started via TestsHarness Activity. * @author iwgeric */ -public class MainActivity extends AppCompatActivity implements OnItemClickListener, View.OnClickListener, TextWatcher { +public class MainActivity extends Activity implements OnItemClickListener, View.OnClickListener, TextWatcher { private static final String TAG = "MainActivity"; /** @@ -127,13 +128,14 @@ public void onCreate(Bundle savedInstanceState) { editFilterText = (EditText) findViewById(R.id.txtFilter); - /* Define the root package to start with */ + /* Define the root package shared by the desktop and Android examples. */ rootPackage = "jme3test"; /* Create an array of Strings to define which classes to exclude */ exclusions.add("$"); // inner classes exclusions.add("TestChooser"); // Desktop test chooser class exclusions.add("awt"); // Desktop test chooser class + exclusions.add("package-info"); // mExclusions.add(""); @@ -147,24 +149,25 @@ public void onCreate(Bundle savedInstanceState) { ApplicationInfo ai = this.getApplicationInfo(); String classPath = ai.sourceDir; DexFile dex = null; - Enumeration apkClassNames = null; try { dex = new DexFile(classPath); - apkClassNames = dex.entries(); + Enumeration apkClassNames = dex.entries(); while (apkClassNames.hasMoreElements()) { String className = apkClassNames.nextElement(); - if (checkClassName(className) && checkClassType(className)) { + if (checkClassName(className) && checkClassType(className) && !classNames.contains(className)) { classNames.add(className); } -// classNames.add(className); } + Collections.sort(classNames); } catch (IOException e) { e.printStackTrace(); } finally { - try { - dex.close(); - } catch (IOException e) { - e.printStackTrace(); + if (dex != null) { + try { + dex.close(); + } catch (IOException e) { + e.printStackTrace(); + } } } @@ -286,7 +289,7 @@ private boolean checkClassName(String className) { private boolean checkClassType(String className) { boolean include = true; try { - Class> clazz = Class.forName(className); + Class> clazz = Class.forName(className, false, getClassLoader()); if (Application.class.isAssignableFrom(clazz)) { Log.d(TAG, "Class " + className + " is a jME Application"); } else { @@ -294,9 +297,9 @@ private boolean checkClassType(String className) { Log.d(TAG, "Skipping Class " + className + ". Not a jME Application"); } - } catch (NoClassDefFoundError ncdf) { + } catch (LinkageError ncdf) { include = false; - Log.d(TAG, "Skipping Class " + className + ". No Class Def found."); + Log.d(TAG, "Skipping Class " + className + ". Could not link class."); } catch (ClassNotFoundException cnfe) { include = false; Log.d(TAG, "Skipping Class " + className + ". Class not found."); @@ -430,29 +433,25 @@ public boolean onPrepareOptionsMenu (Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.optionMouseEvents: - enableMouseEvents = !enableMouseEvents; - Log.d(TAG, "enableMouseEvents set to: " + enableMouseEvents); - break; - case R.id.optionJoystickEvents: - enableJoystickEvents = !enableJoystickEvents; - Log.d(TAG, "enableJoystickEvents set to: " + enableJoystickEvents); - break; - case R.id.optionKeyEvents: - enableKeyEvents = !enableKeyEvents; - Log.d(TAG, "enableKeyEvents set to: " + enableKeyEvents); - break; - case R.id.optionVerboseLogging: - verboseLogging = !verboseLogging; - Log.d(TAG, "verboseLogging set to: " + verboseLogging); - break; - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.optionMouseEvents) { + enableMouseEvents = !enableMouseEvents; + Log.d(TAG, "enableMouseEvents set to: " + enableMouseEvents); + } else if (itemId == R.id.optionJoystickEvents) { + enableJoystickEvents = !enableJoystickEvents; + Log.d(TAG, "enableJoystickEvents set to: " + enableJoystickEvents); + } else if (itemId == R.id.optionKeyEvents) { + enableKeyEvents = !enableKeyEvents; + Log.d(TAG, "enableKeyEvents set to: " + enableKeyEvents); + } else if (itemId == R.id.optionVerboseLogging) { + verboseLogging = !verboseLogging; + Log.d(TAG, "verboseLogging set to: " + verboseLogging); + } else { + return super.onOptionsItemSelected(item); } return true; } -} \ No newline at end of file +} diff --git a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java index 70e7075705..1251c94118 100644 --- a/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java +++ b/jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java @@ -1,14 +1,14 @@ package org.jmonkeyengine.jme3androidexamples; -import android.app.FragmentTransaction; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; import com.jme3.system.JmeSystem; -public class TestActivity extends AppCompatActivity { +public class TestActivity extends FragmentActivity { JmeFragment fragment; @Override @@ -49,7 +49,7 @@ protected void onCreate(Bundle savedInstanceState) { fragment.setArguments(args); - FragmentTransaction transaction = getFragmentManager().beginTransaction(); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); // Replace whatever is in the fragment_container view with this fragment, // and add the transaction to the back stack so the user can navigate back @@ -83,13 +83,11 @@ public boolean onPrepareOptionsMenu (Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.optionToggleKeyboard: - toggleKeyboard(true); -// Log.d(this.getClass().getSimpleName(), "showing soft keyboard"); - break; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == R.id.optionToggleKeyboard) { + toggleKeyboard(true); +// Log.d(this.getClass().getSimpleName(), "showing soft keyboard"); + } else { + return super.onOptionsItemSelected(item); } return true; diff --git a/jme3-android-examples/src/main/res/layout-land/test_chooser_layout.xml b/jme3-android-examples/src/main/res/layout-land/test_chooser_layout.xml new file mode 100644 index 0000000000..4285fa3c5f --- /dev/null +++ b/jme3-android-examples/src/main/res/layout-land/test_chooser_layout.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jme3-android-examples/src/main/res/layout/test_chooser_row.xml b/jme3-android-examples/src/main/res/layout/test_chooser_row.xml index b0c7bb5868..fbe28d7c26 100644 --- a/jme3-android-examples/src/main/res/layout/test_chooser_row.xml +++ b/jme3-android-examples/src/main/res/layout/test_chooser_row.xml @@ -3,9 +3,13 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layoutTestChooserRow" android:orientation="horizontal" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:padding="10dp"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="44dp" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:paddingTop="6dp" + android:paddingBottom="6dp"> + android:textSize="16sp" + android:textColor="#000000" /> - \ No newline at end of file + diff --git a/jme3-android-examples/src/main/res/menu/menu_items.xml b/jme3-android-examples/src/main/res/menu/menu_items.xml index 69a82cfb6c..87c8cc6111 100644 --- a/jme3-android-examples/src/main/res/menu/menu_items.xml +++ b/jme3-android-examples/src/main/res/menu/menu_items.xml @@ -1,5 +1,4 @@ @@ -7,23 +6,23 @@ android:id="@+id/optionMouseEvents" android:orderInCategory="100" android:title="@string/strOptionEnableMouseEventsTitle" - app:showAsAction="never" /> + android:showAsAction="never" /> + android:showAsAction="never" /> + android:showAsAction="never" /> + android:showAsAction="never" /> - diff --git a/jme3-android/build.gradle b/jme3-android/build.gradle index bfe69898f9..2bf4908a10 100644 --- a/jme3-android/build.gradle +++ b/jme3-android/build.gradle @@ -1,3 +1,11 @@ +sourceSets { + main { + java { + srcDir 'src/androidx-stubs/java' + } + } +} + dependencies { //added annotations used by JmeSurfaceView. compileOnly libs.androidx.annotation @@ -10,3 +18,15 @@ compileJava { // The Android-Native Project requires the jni headers to be generated, so we do that here options.compilerArgs += ["-h", "${project.rootDir}/jme3-android-native/src/native/headers"] } + +tasks.withType(Jar).configureEach { + exclude('androidx/**') +} + +tasks.named('sourcesJar') { + exclude('androidx/**') +} + +javadoc { + exclude('androidx/**') +} diff --git a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/Fragment.java b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/Fragment.java new file mode 100644 index 0000000000..1a31df369a --- /dev/null +++ b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/Fragment.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package androidx.fragment.app; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Compile-time stub for the AndroidX Fragment API. + * + * The real AndroidX Fragment dependency must be supplied by the Android + * application. This class is excluded from jme3-android artifacts. + */ +public class Fragment { + + public void onAttach(Context context) { + } + + public void onCreate(Bundle savedInstanceState) { + } + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return null; + } + + public void onActivityCreated(Bundle savedInstanceState) { + } + + public void onStart() { + } + + public void onResume() { + } + + public void onPause() { + } + + public void onStop() { + } + + public void onDestroyView() { + } + + public void onDestroy() { + } + + public void onDetach() { + } + + public FragmentActivity requireActivity() { + return null; + } + + public Context requireContext() { + return null; + } + + public Resources getResources() { + return null; + } +} diff --git a/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java new file mode 100644 index 0000000000..4a1ad4af46 --- /dev/null +++ b/jme3-android/src/androidx-stubs/java/androidx/fragment/app/FragmentActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package androidx.fragment.app; + +import android.app.Activity; + +/** + * Compile-time stub for the AndroidX FragmentActivity API. + * + * The real AndroidX Fragment dependency must be supplied by the Android + * application. This class is excluded from jme3-android artifacts. + */ +public class FragmentActivity extends Activity { +} diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java deleted file mode 100644 index 66d5abc0a9..0000000000 --- a/jme3-android/src/main/java/com/jme3/app/AndroidHarness.java +++ /dev/null @@ -1,592 +0,0 @@ -package com.jme3.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; -import android.opengl.GLSurfaceView; -import android.os.Bundle; -import android.util.Log; -import android.view.*; -import android.view.ViewGroup.LayoutParams; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import com.jme3.audio.AudioRenderer; -import com.jme3.input.JoyInput; -import com.jme3.input.TouchInput; -import com.jme3.input.android.AndroidSensorJoyInput; -import com.jme3.input.controls.TouchListener; -import com.jme3.input.controls.TouchTrigger; -import com.jme3.input.event.TouchEvent; -import com.jme3.system.AppSettings; -import com.jme3.system.SystemListener; -import com.jme3.system.android.JmeAndroidSystem; -import com.jme3.system.android.OGLESContext; -import com.jme3.util.AndroidLogHandler; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; - -/** - * AndroidHarness wraps a jme application object and runs it on - * Android - * - * @author Kirill - * @author larynx - */ -public class AndroidHarness extends Activity implements TouchListener, DialogInterface.OnClickListener, SystemListener { - - protected final static Logger logger = Logger.getLogger(AndroidHarness.class.getName()); - /** - * The application class to start - */ - protected String appClass = "jme3test.android.Test"; - /** - * The jme3 application object - */ - protected LegacyApplication app = null; - - /** - * Sets the desired RGB size for the surfaceview. 16 = RGB565, 24 = RGB888. - * (default = 24) - */ - protected int eglBitsPerPixel = 24; - - /** - * Sets the desired number of Alpha bits for the surfaceview. This affects - * how the surfaceview is able to display Android views that are located - * under the surfaceview jME uses to render the scenegraph. - * 0 = Opaque surfaceview background (fastest) - * 1->7 = Transparent surfaceview background - * 8 or higher = Translucent surfaceview background - * (default = 0) - */ - protected int eglAlphaBits = 0; - - /** - * The number of depth bits specifies the precision of the depth buffer. - * (default = 16) - */ - protected int eglDepthBits = 16; - - /** - * Sets the number of samples to use for multisampling. - * Leave 0 (default) to disable multisampling. - * Set to 2 or 4 to enable multisampling. - */ - protected int eglSamples = 0; - - /** - * Set the number of stencil bits. - * (default = 0) - */ - protected int eglStencilBits = 0; - - /** - * Set the desired frame rate. If frameRate higher than 0, the application - * will be capped at the desired frame rate. - * (default = -1, no frame rate cap) - */ - protected int frameRate = -1; - - /** - * Sets the type of Audio Renderer to be used. - * - * Android MediaPlayer / SoundPool can be used on all - * supported Android platform versions (2.2+) - * OpenAL Soft uses an OpenSL backend and is only supported on Android - * versions 2.3+. - * - * Only use ANDROID_ static strings found in AppSettings - * - */ - protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT; - - /** - * If true Android Sensors are used as simulated Joysticks. Users can use the - * Android sensor feedback through the RawInputListener or by registering - * JoyAxisTriggers. - */ - protected boolean joystickEventsEnabled = false; - /** - * If true KeyEvents are generated from TouchEvents - */ - protected boolean keyEventsEnabled = true; - /** - * If true MouseEvents are generated from TouchEvents - */ - protected boolean mouseEventsEnabled = true; - /** - * Flip X axis - */ - protected boolean mouseEventsInvertX = false; - /** - * Flip Y axis - */ - protected boolean mouseEventsInvertY = false; - /** - * if true finish this activity when the jme app is stopped - */ - protected boolean finishOnAppStop = true; - /** - * set to false if you don't want the harness to handle the exit hook - */ - protected boolean handleExitHook = true; - /** - * Title of the exit dialog, default is "Do you want to exit?" - */ - protected String exitDialogTitle = "Do you want to exit?"; - /** - * Message of the exit dialog, default is "Use your home key to bring this - * app into the background or exit to terminate it." - */ - protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; - /** - * Set the screen window mode. If screenFullSize is true, then the - * notification bar and title bar are removed and the screen covers the - * entire display. If screenFullSize is false, then the notification bar - * remains visible if screenShowTitle is true while screenFullScreen is - * false, then the title bar is also displayed under the notification bar. - */ - protected boolean screenFullScreen = true; - /** - * if screenShowTitle is true while screenFullScreen is false, then the - * title bar is also displayed under the notification bar - */ - protected boolean screenShowTitle = true; - /** - * Splash Screen picture Resource ID. If a Splash Screen is desired, set - * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If - * splashPicID = 0, then no splash screen will be displayed. - */ - protected int splashPicID = 0; - - - protected OGLESContext ctx; - protected GLSurfaceView view = null; - protected boolean isGLThreadPaused = true; - protected ImageView splashImageView = null; - protected FrameLayout frameLayout = null; - final private String ESCAPE_EVENT = "TouchEscape"; - private boolean firstDrawFrame = true; - private boolean inConfigChange = false; - - private class DataObject { - protected LegacyApplication app = null; - } - - @Override - public Object onRetainNonConfigurationInstance() { - logger.log(Level.FINE, "onRetainNonConfigurationInstance"); - final DataObject data = new DataObject(); - data.app = this.app; - inConfigChange = true; - return data; - } - - @Override - @SuppressWarnings("unchecked") - public void onCreate(Bundle savedInstanceState) { - initializeLogHandler(); - - logger.fine("onCreate"); - super.onCreate(savedInstanceState); - - if (screenFullScreen) { - requestWindowFeature(Window.FEATURE_NO_TITLE); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } else { - if (!screenShowTitle) { - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - } - - final DataObject data = (DataObject) getLastNonConfigurationInstance(); - if (data != null) { - logger.log(Level.FINE, "Using Retained App"); - this.app = data.app; - } else { - // Discover the screen resolution - //TODO try to find a better way to get a hand on the resolution - WindowManager wind = this.getWindowManager(); - Display disp = wind.getDefaultDisplay(); - Log.d("AndroidHarness", "Resolution from Window, width:" + disp.getWidth() + ", height: " + disp.getHeight()); - - // Create Settings - logger.log(Level.FINE, "Creating settings"); - AppSettings settings = new AppSettings(true); - settings.setEmulateMouse(mouseEventsEnabled); - settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY); - settings.setUseJoysticks(joystickEventsEnabled); - settings.setEmulateKeyboard(keyEventsEnabled); - - settings.setBitsPerPixel(eglBitsPerPixel); - settings.setAlphaBits(eglAlphaBits); - settings.setDepthBits(eglDepthBits); - settings.setSamples(eglSamples); - settings.setStencilBits(eglStencilBits); - - settings.setResolution(disp.getWidth(), disp.getHeight()); - settings.setAudioRenderer(audioRendererType); - - settings.setFrameRate(frameRate); - - // Create application instance - try { - if (app == null) { - Class clazz = Class.forName(appClass); - app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance(); - } - - app.setSettings(settings); - app.start(); - } catch (Exception ex) { - handleError("Class " + appClass + " init failed", ex); - setContentView(new TextView(this)); - } - } - - ctx = (OGLESContext) app.getContext(); - view = ctx.createView(this); - // store the glSurfaceView in JmeAndroidSystem for future use - JmeAndroidSystem.setView(view); - // AndroidHarness wraps the app as a SystemListener. - ctx.setSystemListener(this); - layoutDisplay(); - } - - @Override - protected void onRestart() { - logger.fine("onRestart"); - super.onRestart(); - if (app != null) { - app.restart(); - } - } - - @Override - protected void onStart() { - logger.fine("onStart"); - super.onStart(); - } - - @Override - protected void onResume() { - logger.fine("onResume"); - super.onResume(); - - gainFocus(); - } - - @Override - protected void onPause() { - logger.fine("onPause"); - loseFocus(); - - super.onPause(); - } - - @Override - protected void onStop() { - logger.fine("onStop"); - super.onStop(); - } - - @Override - protected void onDestroy() { - logger.fine("onDestroy"); - final DataObject data = (DataObject) getLastNonConfigurationInstance(); - if (data != null || inConfigChange) { - logger.fine("In Config Change, not stopping app."); - } else { - if (app != null) { - app.stop(!isGLThreadPaused); - } - } - setContentView(new TextView(this)); - ctx = null; - app = null; - view = null; - JmeAndroidSystem.setView(null); - - super.onDestroy(); - } - - public Application getJmeApplication() { - return app; - } - - /** - * Called when an error has occurred. By default, will show an error message - * to the user and print the exception/error to the log. - */ - @Override - public void handleError(final String errorMsg, final Throwable t) { - String stackTrace = ""; - String title = "Error"; - - if (t != null) { - // Convert exception to string - StringWriter sw = new StringWriter(100); - t.printStackTrace(new PrintWriter(sw)); - stackTrace = sw.toString(); - title = t.toString(); - } - - final String finalTitle = title; - final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") - + "\n" + stackTrace; - - logger.log(Level.SEVERE, finalMsg); - - runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) - .setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create(); - dialog.show(); - } - }); - } - - /** - * Called by the android alert dialog, terminate the activity and OpenGL - * rendering - * - * @param dialog ignored - * @param whichButton the button index - */ - @Override - public void onClick(DialogInterface dialog, int whichButton) { - if (whichButton != -2) { - if (app != null) { - app.stop(true); - } - app = null; - this.finish(); - } - } - - /** - * Gets called by the InputManager on all touch/drag/scale events - */ - @Override - public void onTouch(String name, TouchEvent evt, float tpf) { - if (name.equals(ESCAPE_EVENT)) { - switch (evt.getType()) { - case KEY_UP: - runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) - .setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create(); - dialog.show(); - } - }); - break; - default: - break; - } - } - } - - public void layoutDisplay() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (view == null) { - logger.log(Level.FINE, "view is null!"); - } - if (splashPicID != 0) { - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - Gravity.CENTER); - - frameLayout = new FrameLayout(this); - splashImageView = new ImageView(this); - - Drawable drawable = this.getResources().getDrawable(splashPicID); - if (drawable instanceof NinePatchDrawable) { - splashImageView.setBackgroundDrawable(drawable); - } else { - splashImageView.setImageResource(splashPicID); - } - - if (view.getParent() != null) { - ((ViewGroup) view.getParent()).removeView(view); - } - frameLayout.addView(view); - - if (splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - frameLayout.addView(splashImageView, lp); - - setContentView(frameLayout); - logger.log(Level.FINE, "Splash Screen Created"); - } else { - logger.log(Level.FINE, "Splash Screen Skipped."); - setContentView(view); - } - } - - public void removeSplashScreen() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (splashPicID != 0) { - if (frameLayout != null) { - if (splashImageView != null) { - this.runOnUiThread(new Runnable() { - @Override - public void run() { - splashImageView.setVisibility(View.INVISIBLE); - frameLayout.removeView(splashImageView); - } - }); - } else { - logger.log(Level.FINE, "splashImageView is null"); - } - } else { - logger.log(Level.FINE, "frameLayout is null"); - } - } - } - - /** - * Removes the standard Android log handler due to an issue with not logging - * entries lower than INFO level and adds a handler that produces - * JME formatted log messages. - */ - protected void initializeLogHandler() { - Logger log = LogManager.getLogManager().getLogger(""); - for (Handler handler : log.getHandlers()) { - if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) { - Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName()); - } - log.removeHandler(handler); - } - Handler handler = new AndroidLogHandler(); - log.addHandler(handler); - handler.setLevel(Level.ALL); - } - - @Override - public void initialize() { - app.initialize(); - if (handleExitHook) { - // remove existing mapping from SimpleApplication that stops the app - // when the esc key is pressed (esc key = android back key) so that - // AndroidHarness can produce the exit app dialog box. - if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) { - app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT); - } - - app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK)); - app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT}); - } - } - - @Override - public void reshape(int width, int height) { - app.reshape(width, height); - } - - @Override - public void rescale(float x, float y) { - app.rescale(x, y); - } - - @Override - public void update() { - app.update(); - // call to remove the splash screen, if present. - // call after app.update() to make sure no gap between - // splash screen going away and app display being shown. - if (firstDrawFrame) { - removeSplashScreen(); - firstDrawFrame = false; - } - } - - @Override - public void requestClose(boolean esc) { - app.requestClose(esc); - } - - @Override - public void destroy() { - if (app != null) { - app.destroy(); - } - if (finishOnAppStop) { - finish(); - } - } - - @Override - public void gainFocus() { - logger.fine("gainFocus"); - if (view != null) { - view.onResume(); - } - - if (app != null) { - //resume the audio - AudioRenderer audioRenderer = app.getAudioRenderer(); - if (audioRenderer != null) { - audioRenderer.resumeAll(); - } - //resume the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.resumeSensors(); - } - } - } - } - - isGLThreadPaused = false; - - if (app != null) { - app.gainFocus(); - } - } - - @Override - public void loseFocus() { - logger.fine("loseFocus"); - if (app != null) { - app.loseFocus(); - } - - if (view != null) { - view.onPause(); - } - - if (app != null) { - //pause the audio - AudioRenderer audioRenderer = app.getAudioRenderer(); - if (audioRenderer != null) { - audioRenderer.pauseAll(); - } - //pause the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.pauseSensors(); - } - } - } - } - isGLThreadPaused = true; - } -} diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java index 11de175389..d3ddab8d2f 100644 --- a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java +++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,34 +31,25 @@ */ package com.jme3.app; -import android.app.Activity; import android.app.AlertDialog; -import android.app.Fragment; -import android.content.DialogInterface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; +import android.content.Context; import android.opengl.GLSurfaceView; import android.os.Bundle; import android.util.Log; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; +import androidx.fragment.app.Fragment; import com.jme3.audio.AudioRenderer; import com.jme3.input.JoyInput; -import com.jme3.input.TouchInput; import com.jme3.input.android.AndroidSensorJoyInput; -import com.jme3.input.controls.TouchListener; -import com.jme3.input.controls.TouchTrigger; -import com.jme3.input.event.TouchEvent; -import static com.jme3.input.event.TouchEvent.Type.KEY_UP; import com.jme3.system.AppSettings; import com.jme3.system.SystemListener; import com.jme3.system.android.JmeAndroidSystem; import com.jme3.system.android.OGLESContext; import com.jme3.util.AndroidLogHandler; +import com.jme3.util.AndroidNativeBufferAllocator; +import com.jme3.util.BufferAllocatorFactory; import java.io.PrintWriter; import java.io.StringWriter; import java.util.logging.Handler; @@ -67,313 +58,122 @@ import java.util.logging.Logger; /** + * AndroidX Fragment that hosts a jME application on Android 11+. * - * @author iwgeric + * The fragment is intentionally small: subclasses create the application and + * optionally customize settings; this class owns the Android view lifecycle and + * forwards render callbacks to {@link LegacyApplication}. */ -public class AndroidHarnessFragment extends Fragment implements - TouchListener, DialogInterface.OnClickListener, View.OnLayoutChangeListener, SystemListener { +public abstract class AndroidHarnessFragment extends Fragment implements SystemListener { private static final Logger logger = Logger.getLogger(AndroidHarnessFragment.class.getName()); - /** - * The application class to start - */ - protected String appClass = "jme3test.android.Test"; - - /** - * Sets the desired RGB size for the surfaceview. 16 = RGB565, 24 = RGB888. - * (default = 24) - */ - protected int eglBitsPerPixel = 24; - - /** - * Sets the desired number of Alpha bits for the surfaceview. This affects - * how the surfaceview is able to display Android views that are located - * under the surfaceview jME uses to render the scenegraph. - * 0 = Opaque surfaceview background (fastest) - * 1->7 = Transparent surfaceview background - * 8 or higher = Translucent surfaceview background - * (default = 0) - */ - protected int eglAlphaBits = 0; - - /** - * The number of depth bits specifies the precision of the depth buffer. - * (default = 16) - */ - protected int eglDepthBits = 16; - - /** - * Sets the number of samples to use for multisampling. - * Leave 0 (default) to disable multisampling. - * Set to 2 or 4 to enable multisampling. - */ - protected int eglSamples = 0; - - /** - * Set the number of stencil bits. - * (default = 0) - */ - protected int eglStencilBits = 0; - - /** - * Set the desired frame rate. If frameRate higher than 0, the application - * will be capped at the desired frame rate. - * (default = -1, no frame rate cap) - */ - protected int frameRate = -1; - - /** - * Set the maximum resolution for the surfaceview in either the - * width or height screen direction depending on the screen size. - * If the surfaceview is rectangular, the longest side (width or height) - * will have the resolution set to a maximum of maxResolutionDimension. - * The other direction will be set to a value that maintains the aspect - * ratio of the surfaceview. - * Any value less than 0 (default = -1) will result in the surfaceview having the - * same resolution as the view layout (i.e. no max resolution). - */ - protected int maxResolutionDimension = -1; - - /** - * Sets the type of Audio Renderer to be used. - * - * Android MediaPlayer / SoundPool can be used on all - * supported Android platform versions (2.2+) - * OpenAL Soft uses an OpenSL backend and is only supported on Android - * versions 2.3+. - * - * Only use ANDROID_ static strings found in AppSettings - * - */ - protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT; - - /** - * If true Android Sensors are used as simulated Joysticks. Users can use the - * Android sensor feedback through the RawInputListener or by registering - * JoyAxisTriggers. - */ - protected boolean joystickEventsEnabled = false; - - /** - * If true KeyEvents are generated from TouchEvents - */ - protected boolean keyEventsEnabled = true; - - /** - * If true MouseEvents are generated from TouchEvents - */ - protected boolean mouseEventsEnabled = true; - - /** - * Flip X axis - */ - protected boolean mouseEventsInvertX = false; - - /** - * Flip Y axis - */ - protected boolean mouseEventsInvertY = false; - - /** - * if true finish this activity when the jme app is stopped - */ + protected GLSurfaceView view; + protected LegacyApplication app; protected boolean finishOnAppStop = true; - /** - * set to false if you don't want the harness to handle the exit hook - */ - protected boolean handleExitHook = true; - - /** - * Title of the exit dialog, default is "Do you want to exit?" - */ - protected String exitDialogTitle = "Do you want to exit?"; - - /** - * Message of the exit dialog, default is "Use your home key to bring this - * app into the background or exit to terminate it." - */ - protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; + @Override + public void onAttach(Context context) { + super.onAttach(context); + } - /** - * Splash Screen picture Resource ID. If a Splash Screen is desired, set - * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If - * splashPicID = 0, then no splash screen will be displayed. - */ - protected int splashPicID = 0; - - protected FrameLayout frameLayout = null; - protected GLSurfaceView view = null; - protected ImageView splashImageView = null; - final private String ESCAPE_EVENT = "TouchEscape"; - private boolean firstDrawFrame = true; - private LegacyApplication app = null; - private int viewWidth = 0; - private int viewHeight = 0; - - // Retrieves the jME application object public Application getJmeApplication() { return app; } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); + public void setFinishOnAppStop(boolean finishOnAppStop) { + this.finishOnAppStop = finishOnAppStop; } - /** - * This Fragment uses setRetainInstance(true) so the onCreate method will only - * be called once. During device configuration changes, the instance of - * this Fragment will be reused in the new Activity. This method should not - * contain any View related objects. They are created and destroyed by - * other methods. View related objects should not be reused, but rather - * created and destroyed along with the Activity. - * - * @param savedInstanceState the saved instance state - */ @Override - @SuppressWarnings("unchecked") public void onCreate(Bundle savedInstanceState) { initializeLogHandler(); logger.fine("onCreate"); super.onCreate(savedInstanceState); - // Create Settings - logger.log(Level.FINE, "Creating settings"); - AppSettings settings = new AppSettings(true); - settings.setEmulateMouse(mouseEventsEnabled); - settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY); - settings.setUseJoysticks(joystickEventsEnabled); - settings.setEmulateKeyboard(keyEventsEnabled); - - settings.setBitsPerPixel(eglBitsPerPixel); - settings.setAlphaBits(eglAlphaBits); - settings.setDepthBits(eglDepthBits); - settings.setSamples(eglSamples); - settings.setStencilBits(eglStencilBits); - settings.setAudioRenderer(audioRendererType); + System.setProperty( + BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION, + AndroidNativeBufferAllocator.class.getName()); - settings.setFrameRate(frameRate); - - // Create application instance try { - if (app == null) { - Class clazz = Class.forName(appClass); - app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance(); - } + app = createApplication(); + AppSettings settings = createSettings(); + configureSettings(settings); app.setSettings(settings); app.start(); - } catch (Exception ex) { - handleError("Class " + appClass + " init failed", ex); + + OGLESContext context = (OGLESContext) app.getContext(); + context.setSystemListener(this); + } catch (Exception exception) { + handleError("jME application initialization failed", exception); } + } - OGLESContext ctx = (OGLESContext) app.getContext(); - // AndroidHarness wraps the app as a SystemListener. - ctx.setSystemListener(this); + /** + * Creates the jME application hosted by this fragment. + * + * @return the application instance + * @throws Exception if the application cannot be created + */ + protected abstract LegacyApplication createApplication() throws Exception; - setRetainInstance(true); + /** + * Creates the default Android settings. Subclasses can override this when + * they need to replace the settings object rather than adjust it. + * + * @return default settings for Android + */ + protected AppSettings createSettings() { + AppSettings settings = new AppSettings(true); + settings.setAudioRenderer(AppSettings.ANDROID_OPENAL_SOFT); + return settings; } /** - * Called by the system to create the View hierarchy associated with this - * Fragment. For jME, this is a FrameLayout that contains the GLSurfaceView - * and an overlaying SplashScreen Image (if used). The View that is returned - * will be placed on the screen within the boundaries of the View borders defined - * by the Activity's layout parameters for this Fragment. For jME, we also - * update the application reference to the new view. + * Customizes the settings before the application starts. * - * @param inflater ignored - * @param container ignored - * @param savedInstanceState ignored - * @return the new view + * @param settings the settings to customize */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - logger.fine("onCreateView"); - // Create the GLSurfaceView for the application - view = ((OGLESContext) app.getContext()).createView(getActivity()); - // store the glSurfaceView in JmeAndroidSystem for future use - JmeAndroidSystem.setView(view); - createLayout(); - view.addOnLayoutChangeListener(this); - return frameLayout; + protected void configureSettings(AppSettings settings) { } @Override - public void onActivityCreated(Bundle savedInstanceState) { - logger.fine("onActivityCreated"); - super.onActivityCreated(savedInstanceState); - } + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + logger.fine("onCreateView"); + if (app == null) { + return new View(requireContext()); + } - @Override - public void onStart() { - logger.fine("onStart"); - super.onStart(); + view = ((OGLESContext) app.getContext()).createView(requireActivity()); + JmeAndroidSystem.setView(view); + return view; } - /** - * When the Fragment resumes (i.e. after app resumes or device screen turned - * back on), call the gainFocus() in the jME application. - */ @Override public void onResume() { logger.fine("onResume"); super.onResume(); - gainFocus(); } - /** - * When the Fragment pauses (i.e. after home button pressed on the device - * or device screen turned off) , call the loseFocus() in the jME application. - */ @Override public void onPause() { logger.fine("onPause"); loseFocus(); - super.onPause(); } - @Override - public void onStop() { - logger.fine("onStop"); - super.onStop(); - } - - /** - * Called by the Android system each time the Activity is destroyed or recreated. - * For jME, we clear references to the GLSurfaceView. - */ @Override public void onDestroyView() { logger.fine("onDestroyView"); - if (splashImageView != null && splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - if (view.getParent() != null) { + if (view != null && view.getParent() instanceof ViewGroup) { ((ViewGroup) view.getParent()).removeView(view); } - if (frameLayout != null && frameLayout.getParent() != null) { - ((ViewGroup) frameLayout.getParent()).removeView(frameLayout); - } - view.removeOnLayoutChangeListener(this); - - splashImageView = null; - frameLayout = null; view = null; JmeAndroidSystem.setView(null); - super.onDestroyView(); } - /** - * Called by the system when the application is being destroyed. In this case, - * the jME application is actually closed as well. This method is not called - * during device configuration changes or when the application is put in the - * background. - */ @Override public void onDestroy() { logger.fine("onDestroy"); @@ -385,186 +185,63 @@ public void onDestroy() { } @Override - public void onDetach() { - logger.fine("onDetach"); - super.onDetach(); - } - - - /** - * Called when an error has occurred. By default, will show an error message - * to the user and print the exception/error to the log. - */ - @Override - public void handleError(final String errorMsg, final Throwable t) { + public void handleError(final String errorMsg, final Throwable throwable) { String stackTrace = ""; String title = "Error"; - if (t != null) { - // Convert exception to string - StringWriter sw = new StringWriter(100); - t.printStackTrace(new PrintWriter(sw)); - stackTrace = sw.toString(); - title = t.toString(); + if (throwable != null) { + StringWriter writer = new StringWriter(100); + throwable.printStackTrace(new PrintWriter(writer)); + stackTrace = writer.toString(); + title = throwable.toString(); } final String finalTitle = title; - final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") + final String finalMessage = (errorMsg != null ? errorMsg : "Uncaught Exception") + "\n" + stackTrace; - logger.log(Level.SEVERE, finalMsg); + logger.log(Level.SEVERE, finalMessage); - getActivity().runOnUiThread(new Runnable() { + requireActivity().runOnUiThread(new Runnable() { @Override public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(finalTitle); - builder.setPositiveButton("Kill", AndroidHarnessFragment.this); - builder.setMessage(finalMsg); - - AlertDialog dialog = builder.create(); + AlertDialog dialog = new AlertDialog.Builder(requireActivity()) + .setTitle(finalTitle) + .setMessage(finalMessage) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, (d, w) -> { + if (app != null) { + app.stop(true); + } + requireActivity().finish(); + }) + .setNegativeButton("Kill", (d, w) -> { + android.os.Process.killProcess(android.os.Process.myPid()); + }) + .create(); dialog.show(); } }); } - /** - * Called by the android alert dialog, terminate the activity and OpenGL - * rendering - * - * @param dialog ignored - * @param whichButton the button index - */ - @Override - public void onClick(DialogInterface dialog, int whichButton) { - if (whichButton != -2) { - if (app != null) { - app.stop(true); - } - app = null; - getActivity().finish(); - } - } - - /** - * Gets called by the InputManager on all touch/drag/scale events - */ - @Override - public void onTouch(String name, TouchEvent evt, float tpf) { - if (name.equals(ESCAPE_EVENT)) { - switch (evt.getType()) { - case KEY_UP: - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(exitDialogTitle); - builder.setPositiveButton("Yes", AndroidHarnessFragment.this); - builder.setNegativeButton("No", AndroidHarnessFragment.this); - builder.setMessage(exitDialogMessage); - - AlertDialog dialog = builder.create(); - dialog.show(); - } - }); - break; - default: - break; - } - } - } - - public void createLayout() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER); - - if (frameLayout != null && frameLayout.getParent() != null) { - ((ViewGroup) frameLayout.getParent()).removeView(frameLayout); - } - frameLayout = new FrameLayout(getActivity()); - - if (view.getParent() != null) { - ((ViewGroup) view.getParent()).removeView(view); - } - frameLayout.addView(view); - - if (splashPicID != 0) { - splashImageView = new ImageView(getActivity()); - - Drawable drawable = getResources().getDrawable(splashPicID); - if (drawable instanceof NinePatchDrawable) { - splashImageView.setBackgroundDrawable(drawable); - } else { - splashImageView.setImageResource(splashPicID); - } - - if (splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - frameLayout.addView(splashImageView, lp); - - logger.fine("Splash Screen Created"); - } else { - logger.fine("Splash Screen Skipped."); - } - } - - public void removeSplashScreen() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (splashPicID != 0) { - if (frameLayout != null) { - if (splashImageView != null) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - splashImageView.setVisibility(View.INVISIBLE); - frameLayout.removeView(splashImageView); - } - }); - } else { - logger.fine("splashImageView is null"); - } - } else { - logger.fine("frameLayout is null"); - } - } - } - - /** - * Removes the standard Android log handler due to an issue with not logging - * entries lower than INFO level and adds a handler that produces - * JME formatted log messages. - */ protected void initializeLogHandler() { - Logger log = LogManager.getLogManager().getLogger(""); - for (Handler handler : log.getHandlers()) { - if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) { + Logger rootLogger = LogManager.getLogManager().getLogger(""); + for (Handler handler : rootLogger.getHandlers()) { + if (rootLogger.getLevel() != null + && rootLogger.getLevel().intValue() <= Level.FINE.intValue()) { Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName()); } - log.removeHandler(handler); + rootLogger.removeHandler(handler); } + Handler handler = new AndroidLogHandler(); - log.addHandler(handler); handler.setLevel(Level.ALL); + rootLogger.addHandler(handler); } @Override public void initialize() { app.initialize(); - if (handleExitHook) { - // remove existing mapping from SimpleApplication that stops the app - // when the esc key is pressed (esc key = android back key) so that - // AndroidHarness can produce the exit app dialog box. - if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) { - app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT); - } - - app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK)); - app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT}); - } } @Override @@ -580,13 +257,6 @@ public void rescale(float x, float y) { @Override public void update() { app.update(); - // call to remove the splash screen, if present. - // call after app.update() to make sure no gap between - // splash screen going away and app display being shown. - if (firstDrawFrame) { - removeSplashScreen(); - firstDrawFrame = false; - } } @Override @@ -600,7 +270,7 @@ public void destroy() { app.destroy(); } if (finishOnAppStop) { - getActivity().finish(); + requireActivity().finish(); } } @@ -612,24 +282,16 @@ public void gainFocus() { } if (app != null) { - //resume the audio AudioRenderer audioRenderer = app.getAudioRenderer(); if (audioRenderer != null) { audioRenderer.resumeAll(); } - //resume the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.resumeSensors(); - } - } + + JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; + if (joyInput instanceof AndroidSensorJoyInput) { + ((AndroidSensorJoyInput) joyInput).resumeSensors(); } - } - if (app != null) { app.gainFocus(); } } @@ -646,71 +308,15 @@ public void loseFocus() { } if (app != null) { - //pause the audio AudioRenderer audioRenderer = app.getAudioRenderer(); if (audioRenderer != null) { audioRenderer.pauseAll(); } - //pause the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.pauseSensors(); - } - } - } - } - } - - @Override - public void onLayoutChange(View v, - int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - - if (v.equals(view)) { -// logger.log(Level.INFO, "surfaceview layout changed. left: {0}, top: {1}, right: {2}, bottom: {3}", -// new Object[]{left, top, right, bottom}); - - if (v.equals(view) && maxResolutionDimension > 0) { - int newWidth = right-left; - int newHeight = bottom-top; - - if (viewWidth != newWidth || viewHeight != newHeight) { - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "SurfaceView layout changed: old width: {0}, old height: {1}, new width: {2}, new height: {3}", - new Object[]{viewWidth, viewHeight, newWidth, newHeight}); - } - viewWidth = newWidth; - viewHeight = newHeight; - - int fixedSizeWidth = viewWidth; - int fixedSizeHeight = viewHeight; - if (viewWidth > viewHeight && viewWidth > maxResolutionDimension) { - // landscape - fixedSizeWidth = maxResolutionDimension; - fixedSizeHeight = (int)(maxResolutionDimension * (viewHeight / (float)viewWidth)); - } else if (viewHeight > viewWidth && viewHeight > maxResolutionDimension) { - // portrait - fixedSizeWidth = (int)(maxResolutionDimension * (viewWidth / (float)viewHeight)); - fixedSizeHeight = maxResolutionDimension; - } else if (viewWidth == viewHeight && viewWidth > maxResolutionDimension) { - fixedSizeWidth = maxResolutionDimension; - fixedSizeHeight = maxResolutionDimension; - } - // set the surfaceview resolution if the size != current view size - if (fixedSizeWidth != viewWidth || fixedSizeHeight != viewHeight) { - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "setting surfaceview resolution to width: {0}, height: {1}", - new Object[]{fixedSizeWidth, fixedSizeHeight}); - } - view.getHolder().setFixedSize(fixedSizeWidth, fixedSizeHeight); - } - } + JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; + if (joyInput instanceof AndroidSensorJoyInput) { + ((AndroidSensorJoyInput) joyInput).pauseSensors(); } } } - } diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java index aff21a20ce..02d9110098 100644 --- a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java +++ b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java @@ -610,6 +610,19 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor GLES30.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + return GLES31.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + /* + * GLES 3.1 exposes shader storage block binding through GLSL layout(binding = N). + * Android's GLES31 Java bindings do not expose glShaderStorageBlockBinding. + */ + } + @Override public void glBindFramebufferEXT(int param1, int param2) { GLES20.glBindFramebuffer(param1, param2); diff --git a/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java b/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java index 8f023c6266..215107c6a9 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java +++ b/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java @@ -1,6 +1,6 @@ package com.jme3.system.android; -import android.opengl.GLSurfaceView.EGLConfigChooser; +import android.opengl.GLSurfaceView; import com.jme3.renderer.android.RendererUtil; import com.jme3.system.AppSettings; import java.util.logging.Level; @@ -10,486 +10,282 @@ import javax.microedition.khronos.egl.EGLDisplay; /** - * AndroidConfigChooser is used to determine the best suited EGL Config + * AndroidConfigChooser is used to determine the best suited EGL Config. * * @author iwgeric */ -public class AndroidConfigChooser implements EGLConfigChooser { +public final class AndroidConfigChooser implements GLSurfaceView.EGLConfigChooser { private static final Logger logger = Logger.getLogger(AndroidConfigChooser.class.getName()); - protected AppSettings settings; - private final static int EGL_OPENGL_ES2_BIT = 4; - private final static int EGL_OPENGL_ES3_BIT = 0x40; + + private static final int EGL_OPENGL_ES3_BIT = 0x40; + private static final int EGL_WINDOW_BIT = 0x0004; + + private static final int REJECTED = Integer.MIN_VALUE / 2; + + private final AppSettings settings; public AndroidConfigChooser(AppSettings settings) { this.settings = settings; } - /** - * Gets called by the GLSurfaceView class to return the best config - */ @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { - logger.fine("GLSurfaceView asking for egl config"); - Config requestedConfig = getRequestedConfig(); + RequestedConfig requested = getRequestedConfig(); EGLConfig[] configs = getConfigs(egl, display); + EGLConfig chosenConfig = chooseBestConfig(egl, display, configs, requested); - // First try to find an exact match, but allowing a higher stencil - EGLConfig chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true); - if (chosenConfig == null && requestedConfig.d > 16) { - logger.log(Level.INFO, "EGL configuration not found, reducing depth"); - requestedConfig.d = 16; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true); - } - - if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher RGB"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); - } - - if (chosenConfig == null && requestedConfig.a > 0) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); + if (chosenConfig == null && requested.samples > 0) { + logger.log(Level.INFO, "EGL configuration not found with requested samples, disabling MSAA"); + requested = requested.withSamples(0); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.s > 0) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher samples"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true); + if (chosenConfig == null && requested.alpha > 0) { + logger.log(Level.INFO, "EGL configuration not found with requested alpha, allowing opaque config"); + requested = requested.withAlpha(0); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.a > 0) { - logger.log(Level.INFO, "EGL configuration not found, reducing alpha"); - requestedConfig.a = 1; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); + if (chosenConfig == null && requested.depth > 16) { + logger.log(Level.INFO, "EGL configuration not found with requested depth, reducing depth to 16"); + requested = requested.withDepth(16); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.s > 0) { - logger.log(Level.INFO, "EGL configuration not found, reducing samples"); - requestedConfig.s = 1; - if (requestedConfig.a > 0) { - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true); - } else { - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true); - } - } - - if (chosenConfig == null && requestedConfig.getBitsPerPixel() > 16) { - logger.log(Level.INFO, "EGL configuration not found, setting to RGB565"); - requestedConfig.r = 5; - requestedConfig.g = 6; - requestedConfig.b = 5; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); - - if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); - } + if (chosenConfig == null) { + logger.log(Level.INFO, "EGL configuration not found, using minimal GLES3 window config"); + requested = new RequestedConfig(0, 0, 0, 0, 16, 0, 0, false); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, looking for best config with >= 16 bit Depth"); - // failsafe: pick the best config with depth >= 16 - requestedConfig = new Config(0, 0, 0, 0, 16, 0, 0); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); + throw new IllegalStateException("No suitable GLES3 EGLConfig found"); } - if (chosenConfig != null) { - logger.fine("GLSurfaceView asks for egl config, returning: "); - logEGLConfig(chosenConfig, display, egl, Level.FINE); - - storeSelectedConfig(egl, display, chosenConfig); - return chosenConfig; - } else { - logger.severe("No EGL Config found"); - return null; + storeSelectedConfig(egl, display, chosenConfig); + if (logger.isLoggable(Level.INFO)) { + logEGLConfig(chosenConfig, display, egl, Level.INFO); } + return chosenConfig; } - private Config getRequestedConfig() { - int r, g, b; - if (settings.getBitsPerPixel() == 24) { - r = g = b = 8; + private RequestedConfig getRequestedConfig() { + int bitsPerPixel = settings.getBitsPerPixel(); + boolean gamma = settings.isGammaCorrection(); + int red; + int green; + int blue; + + if (gamma || bitsPerPixel >= 24) { + red = 8; + green = 8; + blue = 8; + if (bitsPerPixel < 24) { + settings.setBitsPerPixel(24); + } } else { - if (settings.getBitsPerPixel() != 16) { - logger.log(Level.SEVERE, "Invalid bitsPerPixel setting: {0}, setting to RGB565 (16)", settings.getBitsPerPixel()); + red = 5; + green = 6; + blue = 5; + if (bitsPerPixel != 16) { + logger.log(Level.INFO, "Invalid bitsPerPixel setting: {0}, using RGB565", bitsPerPixel); settings.setBitsPerPixel(16); } - r = 5; - g = 6; - b = 5; - } - - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Requested Display Config:"); - logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}", - new Object[]{settings.getBitsPerPixel(), - settings.getAlphaBits(), settings.getDepthBits(), - settings.getSamples(), settings.getStencilBits()}); } - return new Config( - r, g, b, - settings.getAlphaBits(), - settings.getDepthBits(), - settings.getSamples(), - settings.getStencilBits()); + return new RequestedConfig( + red, + green, + blue, + Math.max(0, settings.getAlphaBits()), + Math.max(0, settings.getDepthBits()), + Math.max(0, settings.getStencilBits()), + Math.max(0, settings.getSamples()), + gamma); } - /** - * Query egl for the available configs - * @param egl - * @param display - * @return - */ private EGLConfig[] getConfigs(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + int[] configSpec = { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL10.EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL10.EGL_NONE + }; + + if (!egl.eglChooseConfig(display, configSpec, null, 0, numConfig)) { + RendererUtil.checkEGLError(egl); + throw new AssertionError("Unable to query GLES3 EGL configs"); + } - int[] num_config = new int[1]; - int[] configSpec = new int[]{ - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - EGL10.EGL_NONE}; - boolean gles3=true; - - // Try openGL ES 3 - try { - if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) { - RendererUtil.checkEGLError(egl); - gles3=false; - } - } catch (com.jme3.renderer.RendererException re) { - // it's just the device not supporting GLES3. Fallback to GLES2 - gles3=false; - } - - if(!gles3) - { - // Get back to openGL ES 2 - configSpec[1]=EGL_OPENGL_ES2_BIT; - if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) { - RendererUtil.checkEGLError(egl); - throw new AssertionError(); - } + int numConfigs = numConfig[0]; + if (numConfigs == 0) { + throw new IllegalStateException("No GLES3 window EGL configs found"); } - int numConfigs = num_config[0]; EGLConfig[] configs = new EGLConfig[numConfigs]; - if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) { + if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, numConfig)) { RendererUtil.checkEGLError(egl); - throw new AssertionError(); + throw new AssertionError("Unable to enumerate GLES3 EGL configs"); } - logger.fine("--------------Display Configurations---------------"); - for (EGLConfig eGLConfig : configs) { - logEGLConfig(eGLConfig, display, egl, Level.FINE); - logger.fine("----------------------------------------"); + if (logger.isLoggable(Level.FINE)) { + logger.fine("--------------Display Configurations---------------"); + for (EGLConfig config : configs) { + logEGLConfig(config, display, egl, Level.FINE); + logger.fine("----------------------------------------"); + } } - return configs; } - private EGLConfig chooseConfig( - EGL10 egl, EGLDisplay display, EGLConfig[] configs, Config requestedConfig, - boolean higherRGB, boolean higherAlpha, - boolean higherSamples, boolean higherStencil) { + private EGLConfig chooseBestConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs, RequestedConfig requested) { + EGLConfig bestConfig = null; + int bestScore = REJECTED; - EGLConfig keptConfig = null; - int kr = 0; - int kg = 0; - int kb = 0; - int ka = 0; - int kd = 0; - int ks = 0; - int kst = 0; - - - // first pass through config list. Try to find an exact match. for (EGLConfig config : configs) { - int r = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_RED_SIZE); - int g = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_GREEN_SIZE); - int b = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_BLUE_SIZE); - int a = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_ALPHA_SIZE); - int d = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_DEPTH_SIZE); - int s = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_SAMPLES); - int st = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_STENCIL_SIZE); + int red = getAttrib(egl, display, config, EGL10.EGL_RED_SIZE); + int green = getAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE); + int blue = getAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE); + int alpha = getAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE); + int depth = getAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE); + int stencil = getAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE); + int sampleBuffers = getAttrib(egl, display, config, EGL10.EGL_SAMPLE_BUFFERS); + int samples = getAttrib(egl, display, config, EGL10.EGL_SAMPLES); + + int score = scoreConfig(requested, red, green, blue, alpha, depth, + stencil, sampleBuffers, samples); if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}", - new Object[]{r, g, b, a, d, s, st}); + logger.log(Level.FINE, + "Checking EGLConfig R{0} G{1} B{2} A{3} D{4} S{5} MSAA[{6},{7}] score {8}", + new Object[]{red, green, blue, alpha, depth, stencil, sampleBuffers, samples, score}); } - if (higherRGB && r < requestedConfig.r) { continue; } - if (!higherRGB && r != requestedConfig.r) { continue; } - - if (higherRGB && g < requestedConfig.g) { continue; } - if (!higherRGB && g != requestedConfig.g) { continue; } - - if (higherRGB && b < requestedConfig.b) { continue; } - if (!higherRGB && b != requestedConfig.b) { continue; } - - if (higherAlpha && a < requestedConfig.a) { continue; } - if (!higherAlpha && a != requestedConfig.a) { continue; } - - if (d < requestedConfig.d) { continue; } // always allow higher depth + if (score > bestScore) { + bestScore = score; + bestConfig = config; + } + } - if (higherSamples && s < requestedConfig.s) { continue; } - if (!higherSamples && s != requestedConfig.s) { continue; } + return bestScore == REJECTED ? null : bestConfig; + } - if (higherStencil && st < requestedConfig.st) { continue; } - if (!higherStencil && !inRange(st, 0, requestedConfig.st)) { continue; } + private int scoreConfig(RequestedConfig requested, int red, int green, int blue, + int alpha, int depth, int stencil, int sampleBuffers, int samples) { + if (requested.gamma && (red < 8 || green < 8 || blue < 8)) { + return REJECTED; + } + if (red < requested.red || green < requested.green || blue < requested.blue) { + return REJECTED; + } + if (alpha < requested.alpha || depth < requested.depth || stencil < requested.stencil) { + return REJECTED; + } + if (requested.samples > 0 && (sampleBuffers == 0 || samples < requested.samples)) { + return REJECTED; + } - //we keep the config if it is better - if ( r >= kr || g >= kg || b >= kb || a >= ka || - d >= kd || s >= ks || st >= kst ) { - kr = r; kg = g; kb = b; ka = a; - kd = d; ks = s; kst = st; - keptConfig = config; - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}", - new Object[]{r, g, b, a, d, s, st}); - } - } + int score = 0; + score += closenessScore(red, requested.red, 8); + score += closenessScore(green, requested.green, 8); + score += closenessScore(blue, requested.blue, 8); + score += requested.alpha > 0 ? closenessScore(alpha, requested.alpha, 8) : opaqueBonus(alpha); + score += closenessScore(depth, requested.depth, 32); + score += closenessScore(stencil, requested.stencil, 8); + if (requested.samples > 0) { + score += 100 + closenessScore(samples, requested.samples, 16); + } else { + score += samples == 0 ? 10 : -samples; } - if (keptConfig != null) { - return keptConfig; + return score; + } + + private int closenessScore(int actual, int requested, int maxUseful) { + if (requested <= 0) { + return 0; } + return maxUseful - Math.min(maxUseful, actual - requested); + } - //no match found - logger.log(Level.SEVERE, "No egl config match found"); - return null; + private int opaqueBonus(int alpha) { + return alpha == 0 ? 10 : -Math.min(alpha, 8); } - private static int eglGetConfigAttribSafe(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) { + private int getAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) { int[] value = new int[1]; if (!egl.eglGetConfigAttrib(display, config, attribute, value)) { RendererUtil.checkEGLError(egl); - throw new AssertionError(); + throw new AssertionError("Unable to query EGL attribute " + attribute); } return value[0]; } private void storeSelectedConfig(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { - int r = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_RED_SIZE); - int g = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE); - int b = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE); - settings.setBitsPerPixel(r+g+b); - - settings.setAlphaBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE)); - settings.setDepthBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE)); - settings.setSamples( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_SAMPLES)); - settings.setStencilBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE)); + int red = getAttrib(egl, display, eglConfig, EGL10.EGL_RED_SIZE); + int green = getAttrib(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE); + int blue = getAttrib(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE); + int samples = getAttrib(egl, display, eglConfig, EGL10.EGL_SAMPLE_BUFFERS) > 0 + ? getAttrib(egl, display, eglConfig, EGL10.EGL_SAMPLES) + : 0; + + settings.setBitsPerPixel(red + green + blue); + settings.setAlphaBits(getAttrib(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE)); + settings.setDepthBits(getAttrib(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE)); + settings.setStencilBits(getAttrib(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE)); + settings.setSamples(samples); } - /** - * log output with egl config details - * - * @param conf - * @param display - * @param egl - */ - private void logEGLConfig(EGLConfig conf, EGLDisplay display, EGL10 egl, Level level) { - - logger.log(level, "EGL_RED_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RED_SIZE)); - - logger.log(level, "EGL_GREEN_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_GREEN_SIZE)); - - logger.log(level, "EGL_BLUE_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_BLUE_SIZE)); - - logger.log(level, "EGL_ALPHA_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_ALPHA_SIZE)); - - logger.log(level, "EGL_DEPTH_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_DEPTH_SIZE)); - - logger.log(level, "EGL_STENCIL_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_STENCIL_SIZE)); - - logger.log(level, "EGL_RENDERABLE_TYPE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RENDERABLE_TYPE)); - - logger.log(level, "EGL_SURFACE_TYPE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SURFACE_TYPE)); - - logger.log(level, "EGL_SAMPLE_BUFFERS = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLE_BUFFERS)); - - logger.log(level, "EGL_SAMPLES = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLES)); + private void logEGLConfig(EGLConfig config, EGLDisplay display, EGL10 egl, Level level) { + logger.log(level, + "EGLConfig chosen: R{0} G{1} B{2} A{3} D{4} S{5} MSAA[{6} buffers, {7} samples]", + new Object[]{ + getAttrib(egl, display, config, EGL10.EGL_RED_SIZE), + getAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE), + getAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE), + getAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE), + getAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE), + getAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE), + getAttrib(egl, display, config, EGL10.EGL_SAMPLE_BUFFERS), + getAttrib(egl, display, config, EGL10.EGL_SAMPLES) + }); } - private boolean inRange(int val, int min, int max) { - return min <= val && val <= max; - } + private static final class RequestedConfig { + private final int red; + private final int green; + private final int blue; + private final int alpha; + private final int depth; + private final int stencil; + private final int samples; + private final boolean gamma; + + private RequestedConfig(int red, int green, int blue, int alpha, + int depth, int stencil, int samples, boolean gamma) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + this.depth = depth; + this.stencil = stencil; + this.samples = samples; + this.gamma = gamma; + } - private class Config { - /** - * red, green, blue, alpha, depth, samples, stencil - */ - int r, g, b, a, d, s, st; - - private Config(int r, int g, int b, int a, int d, int s, int st) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - this.d = d; - this.s = s; - this.st = st; + private RequestedConfig withSamples(int samples) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); } - private int getBitsPerPixel() { - return r+g+b; + private RequestedConfig withAlpha(int alpha) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); } - } -//DON'T REMOVE THIS, USED FOR UNIT TESTING FAILING CONFIGURATION LISTS. -// private static class Config { -// -// int r, g, b, a, d, s, ms, ns; -// -// public Config(int r, int g, int b, int a, int d, int s, int ms, int ns) { -// this.r = r; -// this.g = g; -// this.b = b; -// this.a = a; -// this.d = d; -// this.s = s; -// this.ms = ms; -// this.ns = ns; -// } -// -// @Override -// public String toString() { -// return "Config{" + "r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + ", d=" + d + ", s=" + s + ", ms=" + ms + ", ns=" + ns + '}'; -// } -// } -// -// public static Config chooseConfig(List configs, ConfigType configType, int mSamples) { -// -// Config keptConfig = null; -// int kd = 0; -// int knbMs = 0; -// -// -// // first pass through config list. Try to find an exact match. -// for (Config config : configs) { -//// logEGLConfig(config, display, egl); -// int r = config.r; -// int g = config.g; -// int b = config.b; -// int a = config.a; -// int d = config.d; -// int s = config.s; -// int isMs = config.ms; -// int nbMs = config.ns; -// -// if (inRange(r, configType.mr, configType.r) -// && inRange(g, configType.mg, configType.g) -// && inRange(b, configType.mb, configType.b) -// && inRange(a, configType.ma, configType.a) -// && inRange(d, configType.md, configType.d) -// && inRange(s, configType.ms, configType.s)) { -// if (mSamples == 0 && isMs != 0) { -// continue; -// } -// boolean keep = false; -// //we keep the config if the depth is better or if the AA setting is better -// if (d >= kd) { -// kd = d; -// keep = true; -// } else { -// keep = false; -// } -// -// if (mSamples != 0) { -// if (nbMs >= knbMs && nbMs <= mSamples) { -// knbMs = nbMs; -// keep = true; -// } else { -// keep = false; -// } -// } -// -// if (keep) { -// keptConfig = config; -// } -// } -// } -// -// if (keptConfig != null) { -// return keptConfig; -// } -// -// if (configType == ConfigType.BEST) { -// keptConfig = chooseConfig(configs, ConfigType.BEST_TRANSLUCENT, mSamples); -// -// if (keptConfig != null) { -// return keptConfig; -// } -// } -// -// if (configType == ConfigType.BEST_TRANSLUCENT) { -// keptConfig = chooseConfig(configs, ConfigType.FASTEST, mSamples); -// -// if (keptConfig != null) { -// return keptConfig; -// } -// } -// // failsafe. pick the 1st config. -// -// for (Config config : configs) { -// if (config.d >= 16) { -// return config; -// } -// } -// -// return null; -// } -// -// private static boolean inRange(int val, int min, int max) { -// return min <= val && val <= max; -// } -// -// public static void main(String... argv) { -// List confs = new ArrayList(); -// confs.add(new Config(5, 6, 5, 0, 0, 0, 0, 0)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 0, 0)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 0, 0)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 0, 0)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 0, 0)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 0, 0)); -// -// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 2)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 2)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 2)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 2)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 2)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 2)); -// -// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 4)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 4)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 4)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 4)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 4)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 4)); -// -// Config chosen = chooseConfig(confs, ConfigType.BEST, 0); -// -// System.err.println(chosen); -// -// } + private RequestedConfig withDepth(int depth) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); + } + } } diff --git a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java index 4acb4aa66f..a553afa25a 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java +++ b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java @@ -48,6 +48,8 @@ import android.view.ViewGroup.LayoutParams; import android.widget.EditText; import android.widget.FrameLayout; +import com.jme3.app.Application; +import com.jme3.asset.AssetManager; import com.jme3.input.*; import com.jme3.input.android.AndroidInputHandler; import com.jme3.input.android.AndroidInputHandler14; @@ -55,19 +57,33 @@ import com.jme3.input.android.AndroidInputHandler26; import com.jme3.input.controls.SoftTextDialogInputListener; import com.jme3.input.dummy.DummyKeyInput; +import com.jme3.material.Material; +import com.jme3.renderer.Caps; +import com.jme3.renderer.RenderManager; import com.jme3.renderer.android.AndroidGL; import com.jme3.renderer.opengl.*; import com.jme3.system.*; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; +import com.jme3.ui.Picture; import com.jme3.util.BufferAllocatorFactory; import com.jme3.util.PrimitiveAllocator; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.opengles.GL10; public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTextDialogInput { + private static final String BLIT_MATERIAL = "Common/MatDefs/Blit/Blit.j3md"; private static final Logger logger = Logger.getLogger(OGLESContext.class.getName()); private static final String SAFER_BUFFER_ALLOCATOR_CLASS = "com.jme3.util.SaferBufferAllocator"; protected final AtomicBoolean created = new AtomicBoolean(false); @@ -82,6 +98,13 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex protected AndroidInputHandler androidInput; protected long minFrameDuration = 0; // No FPS cap protected long lastUpdateTime = 0; + private Application application; + private Material blitMaterial; + private Picture blitGeometry; + private FrameBuffer linearFrameBuffer; + private Texture2D linearFrameBufferColorTexture; + private boolean linearFrameBufferDirty; + private boolean multisampleTextureWarningIssued; static { final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION; @@ -124,16 +147,8 @@ public Type getType() { public GLSurfaceView createView(Context context) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo info = am.getDeviceConfigurationInfo(); - // NOTE: We assume all ICS devices have OpenGL ES 2.0. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - // below 4.0, check OpenGL ES 2.0 support. - if (info.reqGlEsVersion < 0x20000) { - throw new UnsupportedOperationException( - "OpenGL ES 2.0 or better is not supported on this device" - ); - } - } else if (Build.VERSION.SDK_INT < 9) { - throw new UnsupportedOperationException("jME3 requires Android 2.3 or later"); + if (info.reqGlEsVersion < 0x30000) { + throw new UnsupportedOperationException("OpenGL ES 3.0 or better is not supported on this device"); } // Start to set up the view @@ -153,10 +168,8 @@ public GLSurfaceView createView(Context context) { androidInput.setView(view); androidInput.loadSettings(settings); - // setEGLContextClientVersion must be set before calling setRenderer - // this means it cannot be set in AndroidConfigChooser (too late) - // use proper openGL ES version - view.setEGLContextClientVersion(info.reqGlEsVersion >> 16); + // setEGLContextClientVersion must be set before calling setRenderer. + view.setEGLContextClientVersion(3); view.setFocusableInTouchMode(true); view.setFocusable(true); @@ -165,12 +178,7 @@ public GLSurfaceView createView(Context context) { //view.setClickable(true); // setFormat must be set before AndroidConfigChooser is called by the surfaceview. - // if setFormat is called after ConfigChooser is called, then execution - // stops at the setFormat call without a crash. - // We look at the user setting for alpha bits and set the surfaceview - // PixelFormat to either Opaque, Transparent, or Translucent. - // ConfigChooser will do its best to honor the alpha requested by the user - // For best rendering performance, use Opaque (alpha bits = 0). + // For best rendering performance and sRGB support, prefer Opaque (alpha bits = 0). int curAlphaBits = settings.getAlphaBits(); logger.log(Level.FINE, "curAlphaBits: {0}", curAlphaBits); if (curAlphaBits >= 8) { @@ -187,6 +195,7 @@ public GLSurfaceView createView(Context context) { AndroidConfigChooser configChooser = new AndroidConfigChooser(settings); view.setEGLConfigChooser(configChooser); + view.setEGLContextFactory(new Gles3ContextFactory()); view.setRenderer(this); // Attempt to preserve the EGL Context on app pause/resume. @@ -200,11 +209,63 @@ public GLSurfaceView createView(Context context) { return view; } + private static final class Gles3ContextFactory implements GLSurfaceView.EGLContextFactory { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final int EGL_CONTEXT_PRIORITY_LEVEL_IMG = 0x3100; + private static final int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101; + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + EGLContext context = createGles3Context(egl, display, config, true); + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + context = createGles3Context(egl, display, config, false); + } + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + throw new IllegalStateException("Unable to create an OpenGL ES 3 context"); + } + return context; + } + + private EGLContext createGles3Context(EGL10 egl, EGLDisplay display, + EGLConfig config, boolean preferHighPriority) { + boolean usePriority = preferHighPriority && hasExtension(egl, display, "EGL_IMG_context_priority"); + int[] attributes = usePriority + ? new int[]{ + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL10.EGL_NONE + } + : new int[]{ + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL10.EGL_NONE + }; + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attributes); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + } + + private static boolean hasExtension(EGL10 egl, EGLDisplay display, String extension) { + String extensions = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (extensions == null) { + return false; + } + // EGL extension list is space separated. Ensure we only match full + // extension names to avoid false positives when one name is a + // substring of another. + String padded = " " + extensions + " "; + return padded.contains(" " + extension + " "); + } + // renderer:initialize @Override public void onSurfaceCreated(GL10 gl, EGLConfig cfg) { if (created.get() && renderer != null) { renderer.resetGLObjects(); + destroyLinearFrameBuffer(); } else { if (!created.get()) { logger.fine("GL Surface created, initializing JME3 renderer"); @@ -253,6 +314,13 @@ public void uncaughtException(Thread thread, Throwable thrown) { renderer = new GLRenderer(gl, (GLExt) gl, (GLFbo) gl); renderer.initialize(); + boolean blitSrgbConversion = useBlitSrgbConversion(); + renderer.setMainFrameBufferSrgb(false); + renderer.setLinearizeSrgbImages(settings.isGammaCorrection()); + logger.log(Level.INFO, + "Android gamma correction: requested={0}, main framebuffer sRGB=false, blit sRGB={1}", + new Object[]{settings.isGammaCorrection(), blitSrgbConversion}); + JmeSystem.setSoftTextDialogInput(this); needClose.set(false); @@ -264,6 +332,7 @@ public void uncaughtException(Thread thread, Throwable thrown) { protected void deinitInThread() { if (renderable.get()) { created.set(false); + destroyLinearFrameBufferResources(); if (renderer != null) { renderer.cleanup(); } @@ -318,6 +387,9 @@ public SystemListener getSystemListener() { @Override public void setSystemListener(SystemListener listener) { this.listener = listener; + if (listener instanceof Application) { + application = (Application) listener; + } } @Override @@ -410,7 +482,9 @@ public void onDrawFrame(GL10 gl) { throw new IllegalStateException("onDrawFrame without create"); } - listener.update(); + if (!renderFrameWithBlitSrgbConversion()) { + listener.update(); + } if (autoFlush) { renderer.postFrame(); } @@ -469,6 +543,150 @@ protected void waitFor(boolean createdVal) { } } + private boolean useBlitSrgbConversion() { + return settings.isGammaCorrection() && application != null; + } + + private int getLinearFrameBufferSampleCount() { + int samples = Math.max(settings.getSamples(), 1); + if (samples > 1 && renderer != null && !renderer.getCaps().contains(Caps.TextureMultisample)) { + if (!multisampleTextureWarningIssued) { + logger.warning("sRGB blit conversion requires multisampled textures for MSAA. " + + "Falling back to a single-sample linear framebuffer."); + multisampleTextureWarningIssued = true; + } + return 1; + } + return samples; + } + + private void rebuildLinearFrameBufferIfNeeded() { + if (!useBlitSrgbConversion()) { + destroyLinearFrameBufferResources(); + return; + } + + int width = Math.max(settings.getWidth(), 1); + int height = Math.max(settings.getHeight(), 1); + int samples = getLinearFrameBufferSampleCount(); + + if (linearFrameBuffer != null && linearFrameBuffer.getWidth() == width + && linearFrameBuffer.getHeight() == height && linearFrameBuffer.getSamples() == samples) { + return; + } + + destroyLinearFrameBuffer(); + + FrameBuffer frameBuffer = new FrameBuffer(width, height, samples); + frameBuffer.setName("Android Linear Blit FrameBuffer"); + frameBuffer.setSrgb(false); + + Texture2D colorTexture = new Texture2D( + new Image(getLinearFrameBufferColorFormat(), width, height, null, ColorSpace.Linear)); + if (samples > 1) { + colorTexture.getImage().setMultiSamples(samples); + } + frameBuffer.addColorTarget(FrameBufferTarget.newTarget(colorTexture)); + + if (settings.getDepthBits() > 0 || settings.getStencilBits() > 0) { + frameBuffer.setDepthTarget(FrameBufferTarget + .newTarget(renderer.getBestDepthTargetFormat(false, false, settings.getStencilBits() > 0))); + } + + linearFrameBufferColorTexture = colorTexture; + linearFrameBuffer = frameBuffer; + linearFrameBufferDirty = true; + } + + private Format getLinearFrameBufferColorFormat() { + if (renderer != null && renderer.getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + logger.warning("RGBA16F color framebuffer is not supported. " + + "Falling back to RGBA8 for Android sRGB blit conversion."); + return Format.RGBA8; + } + + private boolean ensureBlitResources() { + if (!useBlitSrgbConversion()) { + return false; + } + + AssetManager assetManager = application.getAssetManager(); + RenderManager renderManager = application.getRenderManager(); + if (assetManager == null || renderManager == null) { + return false; + } + + if (blitMaterial == null) { + blitMaterial = new Material(assetManager, BLIT_MATERIAL); + blitMaterial.setBoolean("Srgb", true); + blitMaterial.getAdditionalRenderState().setDepthTest(false); + blitMaterial.getAdditionalRenderState().setDepthWrite(false); + } + + if (blitGeometry == null) { + blitGeometry = new Picture("Linear to sRGB Blit"); + blitGeometry.setWidth(1f); + blitGeometry.setHeight(1f); + blitGeometry.setMaterial(blitMaterial); + } + + if (linearFrameBufferDirty && linearFrameBufferColorTexture != null) { + blitMaterial.setTexture("Texture", linearFrameBufferColorTexture); + if (linearFrameBuffer != null && linearFrameBuffer.getSamples() > 1) { + blitMaterial.setInt("NumSamples", linearFrameBuffer.getSamples()); + } else { + blitMaterial.clearParam("NumSamples"); + } + linearFrameBufferDirty = false; + } + + return true; + } + + private void destroyLinearFrameBuffer() { + if (linearFrameBuffer != null) { + linearFrameBuffer.dispose(); + linearFrameBuffer = null; + } + if (linearFrameBufferColorTexture != null && linearFrameBufferColorTexture.getImage() != null) { + linearFrameBufferColorTexture.getImage().dispose(); + } + linearFrameBufferColorTexture = null; + linearFrameBufferDirty = true; + } + + private void destroyLinearFrameBufferResources() { + destroyLinearFrameBuffer(); + blitMaterial = null; + blitGeometry = null; + multisampleTextureWarningIssued = false; + } + + private boolean renderFrameWithBlitSrgbConversion() { + if (!useBlitSrgbConversion()) { + return false; + } + + rebuildLinearFrameBufferIfNeeded(); + if (linearFrameBuffer == null || !ensureBlitResources()) { + return false; + } + + renderer.setMainFrameBufferOverride(linearFrameBuffer); + try { + listener.update(); + } finally { + renderer.setMainFrameBufferOverride(null); + } + + renderer.setFrameBuffer(null); + blitGeometry.updateGeometricState(); + application.getRenderManager().renderGeometry(blitGeometry); + return true; + } + @Override public void requestDialog( final int id, diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java index 2f8ee78d9b..47b259a2dd 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java @@ -66,7 +66,7 @@ public class EnvironmentCamera extends BaseAppState { protected static Vector3f[] axisY = new Vector3f[6]; protected static Vector3f[] axisZ = new Vector3f[6]; - protected Image.Format imageFormat = Image.Format.RGB16F; + protected Image.Format imageFormat = null; public TextureCubeMap debugEnv; @@ -174,23 +174,24 @@ public Void call() throws Exception { public void render(final RenderManager renderManager) { if (isBusy()) { final SnapshotJob job = jobs.get(0); + final Image.Format format = getImageFormat(renderManager.getRenderer()); for (int i = 0; i < 6; i++) { viewports[i].clearScenes(); viewports[i].attachScene(job.scene); renderManager.renderViewPort(viewports[i], 0.16f); buffers[i] = BufferUtils.createByteBuffer( - size * size * imageFormat.getBitsPerPixel() / 8); + size * size * format.getBitsPerPixel() / 8); renderManager.getRenderer().readFrameBufferWithFormat( - framebuffers[i], buffers[i], imageFormat); - images[i] = new Image(imageFormat, size, size, buffers[i], + framebuffers[i], buffers[i], format); + images[i] = new Image(format, size, size, buffers[i], ColorSpace.Linear); MipMapGenerator.generateMipMaps(images[i]); } final TextureCubeMap map = EnvMapUtils.makeCubeMap(images[0], images[1], images[2], images[3], images[4], images[5], - imageFormat); + format); debugEnv = map; job.callback.done(map); map.getImage().dispose(); @@ -292,6 +293,8 @@ public boolean isBusy() { @Override protected void initialize(Application app) { this.backGroundColor = app.getViewPort().getBackgroundColor().clone(); + final Renderer renderer = app.getRenderManager().getRenderer(); + final Image.Format colorFormat = getImageFormat(renderer); final Camera[] cameras = new Camera[6]; final Texture2D[] textures = new Texture2D[6]; @@ -305,7 +308,7 @@ protected void initialize(Application app) { cameras[i] = createOffCamera(size, position, axisX[i], axisY[i], axisZ[i]); viewports[i] = createOffViewPort("EnvView" + i, cameras[i]); framebuffers[i] = createOffScreenFrameBuffer(size, viewports[i]); - textures[i] = new Texture2D(size, size, imageFormat); + textures[i] = new Texture2D(size, size, colorFormat); framebuffers[i].setColorTexture(textures[i]); } } @@ -325,13 +328,27 @@ protected void cleanup(Application app) { } } + protected Image.Format getImageFormat(Renderer renderer) { + if (this.imageFormat == null) { + this.imageFormat = renderer.getBestColorTargetFormat(true, false, false); + } + return this.imageFormat; + } + + protected Image.Format getDepthFormat(Renderer renderer) { + return renderer.getBestDepthTargetFormat(false, false, false); + } + /** * returns the images format used for the generated maps. * * @return the enum value */ public Image.Format getImageFormat() { - return imageFormat; + if (this.imageFormat == null && getApplication() != null) { + return getImageFormat(getApplication().getRenderManager().getRenderer()); + } + return this.imageFormat; } @Override @@ -384,9 +401,22 @@ protected ViewPort createOffViewPort(final String name, final Camera offCamera) * @return a new instance */ protected FrameBuffer createOffScreenFrameBuffer(int mapSize, ViewPort offView) { + Image.Format depthFormat = getDepthFormat(getApplication().getRenderManager().getRenderer()); + return createOffScreenFrameBuffer(mapSize, offView, depthFormat); + } + + /** + * create an off-screen framebuffer. + * + * @param mapSize the desired size (pixels per side) + * @param offView the off-screen viewport to be used (alias created) + * @param depthFormat the depth format to use + * @return a new instance + */ + protected FrameBuffer createOffScreenFrameBuffer(int mapSize, ViewPort offView, Image.Format depthFormat) { // create offscreen framebuffer final FrameBuffer offBuffer = new FrameBuffer(mapSize, mapSize, 1); - offBuffer.setDepthBuffer(Image.Format.Depth); + offBuffer.setDepthBuffer(depthFormat); offView.setOutputFrameBuffer(offBuffer); return offBuffer; } diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java index 6f07fd1c1c..82d45f32de 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -287,9 +287,8 @@ public void setAssetManager(AssetManager assetManager) { } void rebakeNow(RenderManager renderManager) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, - Format.Depth, - envMapSize, envMapSize); + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, + null, envMapSize, envMapSize); baker.setTexturePulling(isRequiredSavableResults()); baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter); diff --git a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java index 12ba5e99c1..8705265572 100644 --- a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java @@ -75,7 +75,7 @@ public class FastLightProbeFactory { * @return The baked LightProbe */ public static LightProbe makeProbe(RenderManager rm, AssetManager am, int size, Vector3f pos, float frustumNear, float frustumFar, Spatial scene) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, null, null, size, size); baker.setTexturePulling(true); diff --git a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java index 7becab82e8..f035f95267 100644 --- a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java @@ -36,6 +36,7 @@ import com.jme3.environment.generation.*; import com.jme3.environment.util.EnvMapUtils; import com.jme3.light.LightProbe; +import com.jme3.renderer.Renderer; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.texture.TextureCubeMap; @@ -123,7 +124,8 @@ public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener listener) { final LightProbe probe = new LightProbe(); probe.setPosition(envCam.getPosition()); - probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat())); + Renderer renderer = envCam.getApplication().getRenderManager().getRenderer(); + probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat(renderer))); envCam.snapshot(scene, new JobProgressAdapter() { @Override @@ -168,7 +170,8 @@ public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCa probe.getPrefilteredEnvMap().getImage().dispose(); } - probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat())); + Renderer renderer = envCam.getApplication().getRenderManager().getRenderer(); + probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat(renderer))); envCam.snapshot(scene, new JobProgressAdapter() { diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java index 4f15dc71e0..0e1bd5a5b2 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java @@ -98,6 +98,7 @@ public abstract class GenericEnvBaker implements EnvBaker { protected TextureCubeMap envMap; protected Format depthFormat; + protected Format colorFormat; protected final RenderManager renderManager; protected final AssetManager assetManager; @@ -107,19 +108,34 @@ public abstract class GenericEnvBaker implements EnvBaker { protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) { this.depthFormat = depthFormat; + this.colorFormat = colorFormat; renderManager = rm; assetManager = am; cam = new Camera(128, 128); - envMap = new TextureCubeMap(env_size, env_size, colorFormat); + envMap = new TextureCubeMap(env_size, env_size, getColorFormat()); envMap.setMagFilter(MagFilter.Bilinear); envMap.setMinFilter(MinFilter.BilinearNoMipMaps); envMap.setWrap(WrapMode.EdgeClamp); envMap.getImage().setColorSpace(ColorSpace.Linear); } + protected Format getColorFormat() { + if (colorFormat == null) { + this.colorFormat = renderManager.getRenderer().getBestColorTargetFormat(true, false, false); + } + return colorFormat; + } + + protected Format getDepthFormat() { + if (depthFormat == null) { + this.depthFormat = renderManager.getRenderer().getBestDepthTargetFormat(false, false, false); + } + return depthFormat; + } + @Override public void setTexturePulling(boolean v) { texturePulling = v; @@ -170,7 +186,7 @@ public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, FrameBuffer envbakers[] = new FrameBuffer[6]; for (int i = 0; i < 6; i++) { envbakers[i] = new FrameBuffer(envMap.getImage().getWidth(), envMap.getImage().getHeight(), 1); - envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(depthFormat)); + envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(getDepthFormat())); envbakers[i].setSrgb(false); envbakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i])); } diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java index 022f4c349f..606a894699 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java @@ -85,7 +85,7 @@ public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBak public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) { super(rm, am, format, depthFormat, env_size); - specular = new TextureCubeMap(specular_size, specular_size, format); + specular = new TextureCubeMap(specular_size, specular_size, getColorFormat()); specular.setWrap(WrapMode.EdgeClamp); specular.setMagFilter(MagFilter.Bilinear); specular.setMinFilter(MinFilter.Trilinear); diff --git a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java index aec38731aa..25115963c8 100644 --- a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java +++ b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java @@ -207,16 +207,7 @@ public void initialize(RenderManager rm, ViewPort vp) { // Determine optimal framebuffer format based on renderer capabilities if (fbFormat == null) { - fbFormat = Format.RGB111110F; - if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) { - if (renderer.getCaps().contains(Caps.FloatColorBufferRGB)) { - fbFormat = Format.RGB16F; - } else if (renderer.getCaps().contains(Caps.FloatColorBufferRGBA)) { - fbFormat = Format.RGBA16F; - } else { - fbFormat = Format.RGB8; - } - } + fbFormat = renderer.getBestColorTargetFormat(true, false, false); } Camera cam = vp.getCamera(); diff --git a/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java b/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java index 857e78dbc3..fb205bad7b 100644 --- a/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java +++ b/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java @@ -106,9 +106,9 @@ public HDRRenderer(AssetManager manager, Renderer renderer) { Collection caps = renderer.getCaps(); if (caps.contains(Caps.PackedFloatColorBuffer)) bufFormat = Format.RGB111110F; - else if (caps.contains(Caps.FloatColorBufferRGB)) + else if (caps.contains(Caps.HalfFloatColorBufferRGB)) bufFormat = Format.RGB16F; - else if (caps.contains(Caps.FloatColorBufferRGBA)) + else if (caps.contains(Caps.HalfFloatColorBufferRGBA)) bufFormat = Format.RGBA16F; else { enabled = false; diff --git a/jme3-core/src/main/java/com/jme3/renderer/Caps.java b/jme3-core/src/main/java/com/jme3/renderer/Caps.java index 647d44a9a6..5b111c811b 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Caps.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Caps.java @@ -223,28 +223,77 @@ public enum Caps { TextureBuffer, /** - * Supports floating point and half textures (Format.RGB16F). + * Supports 32-bit floating point textures. */ FloatTexture, /** - * Supports rendering on RGB floating point textures + * Supports 16-bit floating point textures. + */ + HalfFloatTexture, + + /** + * Supports linear filtering of floating point textures. + */ + FloatTextureFilter, + + /** + * Supports linear filtering of half floating point textures. + */ + HalfFloatTextureFilter, + + /** + * Supports rendering on RGB 32-bit floating point textures. */ FloatColorBufferRGB, /** - * Supports rendering on RGBA floating point textures + * Supports rendering on R 32-bit floating point textures. + */ + FloatColorBufferR, + + /** + * Supports rendering on RG 32-bit floating point textures. + */ + FloatColorBufferRG, + + /** + * Supports rendering on RGBA 32-bit floating point textures. */ FloatColorBufferRGBA, + /** + * Supports rendering on R 16-bit floating point textures. + */ + HalfFloatColorBufferR, + + /** + * Supports rendering on RG 16-bit floating point textures. + */ + HalfFloatColorBufferRG, + + /** + * Supports rendering on RGB 16-bit floating point textures. + */ + HalfFloatColorBufferRGB, + + /** + * Supports rendering on RGBA 16-bit floating point textures. + */ + HalfFloatColorBufferRGBA, + /** * Supports integer textures. */ IntegerTexture, /** - * Supports floating point FBO color buffers (Format.RGB16F). + * Supports full floating point FBO color buffers. + * + * @deprecated use the format-specific FloatColorBuffer* and + * HalfFloatColorBuffer* caps. */ + @Deprecated FloatColorBuffer, @@ -253,6 +302,11 @@ public enum Caps { */ FloatDepthBuffer, + /** + * Supports FBO with Depth32 image format. + */ + Depth32, + /** * Supports Format.RGB111110F for textures. */ @@ -311,6 +365,12 @@ public enum Caps { */ Srgb, + /** + * Supports enabling and disabling linear-to-sRGB conversion for framebuffer + * writes using GL_FRAMEBUFFER_SRGB. + */ + SrgbWriteControl, + /** * Supports blitting framebuffers. */ @@ -491,15 +551,34 @@ public static boolean supports(Collection caps, Texture tex) { return caps.contains(Caps.PackedDepthStencilBuffer); case Depth32F: return caps.contains(Caps.FloatDepthBuffer); + case Depth32: + return caps.contains(Caps.Depth32); + case Depth24: + return caps.contains(Caps.Depth24); case RGB16F_to_RGB111110F: + return caps.contains(Caps.HalfFloatTexture) && caps.contains(Caps.PackedFloatTexture); case RGB111110F: return caps.contains(Caps.PackedFloatTexture); case RGB16F_to_RGB9E5: + return caps.contains(Caps.HalfFloatTexture) && caps.contains(Caps.SharedExponentTexture); case RGB9E5: return caps.contains(Caps.SharedExponentTexture); + case Luminance16F: + case Luminance16FAlpha16F: + case RGB16F: + case RGBA16F: + case R16F: + case RG16F: + return caps.contains(Caps.HalfFloatTexture); + case Luminance32F: + case RGB32F: + case RGBA32F: + case R32F: + case RG32F: + return caps.contains(Caps.FloatTexture); default: if (fmt.isFloatingPont()) { - return caps.contains(Caps.FloatTexture); + return caps.contains(Caps.FloatTexture) || caps.contains(Caps.HalfFloatTexture); } return true; @@ -519,13 +598,32 @@ private static boolean supportsColorBuffer(Collection caps, RenderBuffer c switch (colorFmt) { case RGB111110F: return caps.contains(Caps.PackedFloatColorBuffer); + case Luminance16F: + case R16F: + return caps.contains(Caps.HalfFloatColorBufferR); + case Luminance32F: + case R32F: + return caps.contains(Caps.FloatColorBufferR); + case Luminance16FAlpha16F: + case RG16F: + return caps.contains(Caps.HalfFloatColorBufferRG); + case RG32F: + return caps.contains(Caps.FloatColorBufferRG); + case RGB16F: + return caps.contains(Caps.HalfFloatColorBufferRGB); + case RGB32F: + return caps.contains(Caps.FloatColorBufferRGB); + case RGBA16F: + return caps.contains(Caps.HalfFloatColorBufferRGBA); + case RGBA32F: + return caps.contains(Caps.FloatColorBufferRGBA); case RGB16F_to_RGB111110F: case RGB16F_to_RGB9E5: case RGB9E5: return false; default: if (colorFmt.isFloatingPont()) { - return caps.contains(Caps.FloatColorBuffer); + return false; } return true; @@ -565,6 +663,16 @@ public static boolean supports(Collection caps, FrameBuffer fb) { && !caps.contains(Caps.PackedDepthStencilBuffer)) { return false; } + + if (depthFmt == Format.Depth32 + && !caps.contains(Caps.Depth32)) { + return false; + } + + if (depthFmt == Format.Depth24 + && !caps.contains(Caps.Depth24)) { + return false; + } } } for (int i = 0; i < fb.getNumColorBuffers(); i++) { diff --git a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java index 00f2f8ac57..05f64fb339 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java @@ -42,6 +42,7 @@ import com.jme3.system.AppSettings; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; import com.jme3.texture.Texture; import com.jme3.texture.TextureImage; import com.jme3.util.NativeObject; @@ -422,13 +423,15 @@ public void setTexture(int unit, Texture tex) * As a shorthand, the user can set {@link AppSettings#setGammaCorrection(boolean)} to true * to toggle both {@link Renderer#setLinearizeSrgbImages(boolean)} and * {@link Renderer#setMainFrameBufferSrgb(boolean)} if the - * {@link Caps#Srgb} is supported by the GPU. + * {@link Caps#Srgb} and {@link Caps#SrgbWriteControl} capabilities are + * supported by the GPU. * * @param srgb true for sRGB colorspace, false for linear colorspace * @throws RendererException If the GPU hardware does not support sRGB. * * @see FrameBuffer#setSrgb(boolean) * @see Caps#Srgb + * @see Caps#SrgbWriteControl */ public void setMainFrameBufferSrgb(boolean srgb); @@ -560,4 +563,70 @@ public default void pushDebugGroup(String name) { * Registers a NativeObject to be cleaned up by this renderer. */ public void registerNativeObject(NativeObject nativeObject); + + public default Format getBestColorTargetFormat(boolean floatingPoint) { + return getBestColorTargetFormat(floatingPoint, true, false); + } + + public default Format getBestColorTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withAlpha) { + if (!floatingPoint) { + return Format.RGBA8; + } + + if (!highPrecision) { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } + } + + if (withAlpha) { + if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } else { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGB)) { + return Format.RGB16F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } + + return Format.RGBA8; + } + + public default Format getBestDepthTargetFormat() { + return getBestDepthTargetFormat(false, false, false); + } + + public default Format getBestDepthTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withStencil) { + if (withStencil) { + if (getCaps().contains(Caps.PackedDepthStencilBuffer)) { + return Format.Depth24Stencil8; + } + } else { + if (floatingPoint && getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + if (highPrecision) { + if (getCaps().contains(Caps.Depth32)) { + return Format.Depth32; + } + if (getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + } + if (getCaps().contains(Caps.Depth24)) { + return Format.Depth24; + } + } + + return Format.Depth; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java index 10c5c7ff88..2db094c437 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java @@ -293,6 +293,29 @@ public default void glUniformBlockBinding(int program, int uniformBlockIndex, in throw new UnsupportedOperationException("Uniform buffer objects are not supported"); } + /** + * Retrieves the index of a named program resource. + * + * @param program the name of a program object + * @param programInterface the program interface containing the resource + * @param name the name of the resource + * @return the resource index + */ + public default int glGetProgramResourceIndex(int program, int programInterface, String name) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + + /** + * Assigns a shader storage block to a binding point. + * + * @param program the name of a program object + * @param storageBlockIndex the index of the shader storage block within {@code program} + * @param storageBlockBinding the binding point to assign + */ + public default void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + public default void glPushDebugGroup(int source, int id, String message) { } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java index 8e65fbdef1..1d974fa9a4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java @@ -42,6 +42,9 @@ public final class GLImageFormat { public final int format; public final int dataType; public final boolean compressed; + public final boolean colorRenderable; + public final boolean depthRenderable; + public final boolean filterable; public final boolean swizzleRequired; /** @@ -51,11 +54,14 @@ public final class GLImageFormat { * @param format OpenGL format * @param dataType OpenGL datatype */ - public GLImageFormat(int internalFormat, int format, int dataType) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = false; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -66,12 +72,18 @@ public GLImageFormat(int internalFormat, int format, int dataType) { * @param format OpenGL format * @param dataType OpenGL datatype * @param compressed Format is compressed + * @param colorRenderable Format can be used as a color render target + * @param depthRenderable Format can be used as a depth render target + * @param filterable Format can be filtered */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -84,11 +96,47 @@ public GLImageFormat(int internalFormat, int format, int dataType, boolean compr * @param compressed Format is compressed * @param swizzleRequired Need to use texture swizzle to upload texture */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = swizzleRequired; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + GLImageFormat other = (GLImageFormat) obj; + return internalFormat == other.internalFormat + && format == other.format + && dataType == other.dataType + && compressed == other.compressed + && colorRenderable == other.colorRenderable + && depthRenderable == other.depthRenderable + && filterable == other.filterable + && swizzleRequired == other.swizzleRequired; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + this.internalFormat; + hash = 97 * hash + this.format; + hash = 97 * hash + this.dataType; + hash = 97 * hash + (this.compressed ? 1 : 0); + hash = 97 * hash + (this.colorRenderable ? 1 : 0); + hash = 97 * hash + (this.depthRenderable ? 1 : 0); + hash = 97 * hash + (this.filterable ? 1 : 0); + hash = 97 * hash + (this.swizzleRequired ? 1 : 0); + return hash; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java index c73943c54e..d7d2dd58c4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java @@ -44,50 +44,66 @@ public final class GLImageFormats { private GLImageFormats() { } - + private static void format(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, colorRenderable, depthRenderable, filterable); } - + private static void formatSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable + ) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, false, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgbSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatComp(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } - + private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } /** @@ -103,231 +119,277 @@ private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format fo public static GLImageFormat[][] getFormatsForCaps(EnumSet caps) { GLImageFormat[][] formatToGL = new GLImageFormat[2][Image.Format.values().length]; + boolean opengles = caps.contains(Caps.OpenGLES20); + boolean opengles3 = opengles && caps.contains(Caps.OpenGLES30); + boolean opengles2Only = opengles && !opengles3; + boolean webgl = caps.contains(Caps.WebGL); + boolean opengl = !opengles; + boolean coreProfile = caps.contains(Caps.CoreProfile); + boolean colorRenderableHalfFloatR = caps.contains(Caps.HalfFloatColorBufferR); + boolean colorRenderableHalfFloatRG = caps.contains(Caps.HalfFloatColorBufferRG); + boolean colorRenderableHalfFloatRGB = caps.contains(Caps.HalfFloatColorBufferRGB); + boolean colorRenderableHalfFloatRGBA = caps.contains(Caps.HalfFloatColorBufferRGBA); + boolean colorRenderableFloatR = caps.contains(Caps.FloatColorBufferR); + boolean colorRenderableFloatRG = caps.contains(Caps.FloatColorBufferRG); + boolean colorRenderableFloatRGB = caps.contains(Caps.FloatColorBufferRGB); + boolean colorRenderableFloatRGBA = caps.contains(Caps.FloatColorBufferRGBA); + boolean colorRenderablePackedFloat = caps.contains(Caps.PackedFloatColorBuffer); + boolean filterableHalfFloat = caps.contains(Caps.HalfFloatTextureFilter); + boolean filterableFloat = caps.contains(Caps.FloatTextureFilter); + int halfFloatFormat = GLExt.GL_HALF_FLOAT_ARB; - if (caps.contains(Caps.OpenGLES20)) { + if (opengles2Only) { halfFloatFormat = GLExt.GL_HALF_FLOAT_OES; } // Core Profile Formats (supported by both OpenGL Core 3.3 and OpenGL ES 3.0+) - if (caps.contains(Caps.CoreProfile)) { - formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat); - formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT); - formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); + if (coreProfile) { + formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); - formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); + formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); } - if (caps.contains(Caps.OpenGL20)||caps.contains(Caps.OpenGLES30)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.OpenGL20)||opengles3) { + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (opengles3 || webgl) { + format(formatToGL, Format.RGB565, GLES_30.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); + } else { + format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl || opengles3 || webgl, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - // Additional desktop-specific formats: - format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); + // Additional desktop-specific formats. + if (opengl) { + format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + } // sRGB formats if (caps.contains(Caps.Srgb)) { - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - if (!caps.contains(Caps.CoreProfile)) { - formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl, false, true); + formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, opengl, false, true); + + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (!coreProfile) { + formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + if (opengl) { + formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); } - formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); if (caps.contains(Caps.TextureCompressionS3TC)) { - formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } } } else if (caps.contains(Caps.Rgba8)) { // A more limited form of 32-bit RGBA. Only GL_RGBA8 is available. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); - formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (opengl) { + formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } } else { // Actually, the internal format isn't used for OpenGL ES 2! This is the same as the above. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); } - if (caps.contains(Caps.OpenGLES20)) { - format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); + if (opengles) { + format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); } - format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); + format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, true, false, true); - if (caps.contains(Caps.FloatTexture)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat); - format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT); - format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat); + if (caps.contains(Caps.HalfFloatTexture) || caps.contains(Caps.FloatTexture)) { + if (!coreProfile) { + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat, false, false, filterableHalfFloat); + format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat, false, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT, false, false, filterableFloat); + } + } + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT, colorRenderableFloatRG, false, filterableFloat); + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); } - format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat); - format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT); - format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); - format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT); - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); } if (caps.contains(Caps.PackedFloatTexture)) { - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } if (caps.contains(Caps.SharedExponentTexture)) { - format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT, false, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } // Supported in GLES30 core if (caps.contains(Caps.OpenGLES30)) { - format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV); - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV, true, false, true); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_ALPHA, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + } - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.Srgb)) { + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } - //Depending on the device could be better to use the previously defined extension based float textures instead of gles3.0 texture formats -// if (!caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); -// } - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); + // Depending on the device, the extension-based definitions above may have already defined these. + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); + } + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, true); } // Need to check whether Caps.DepthTexture is supported before using it for textures. // But for render buffers it's OK. - format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); if (caps.contains(Caps.WebGL)) { // NOTE: fallback to 24-bit depth as workaround for firefox bug in WebGL 2 where DEPTH_COMPONENT16 is not handled properly - format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } else if (caps.contains(Caps.OpenGLES20)) { // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth. - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); } else { - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE, false, true, false); + } + if (caps.contains(Caps.Depth24)) { + format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } - if (caps.contains(Caps.OpenGLES30) || caps.contains(Caps.OpenGL20) || caps.contains(Caps.Depth24)) { - format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.Depth32)) { + format(formatToGL, Format.Depth32, GL2.GL_DEPTH_COMPONENT32, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } if (caps.contains(Caps.FloatDepthBuffer)) { - format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT); + format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, false, true, false); } if (caps.contains(Caps.PackedDepthStencilBuffer)) { - format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT); + format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT, false, true, false); } // Compressed formats if (caps.contains(Caps.TextureCompressionS3TC)) { - formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)){ - formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE); - formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE); + if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)) { + formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE, false, false, true); + formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE, false, false, true); } if (caps.contains(Caps.TextureCompressionETC2)) { - formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); if (caps.contains(Caps.Srgb)) { - formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } } else if (caps.contains(Caps.TextureCompressionETC1)) { - formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { - formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); - formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { + formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); } // Integer formats - if(caps.contains(Caps.IntegerTexture)) { - format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT); - format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.IntegerTexture)) { + format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT); - format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE, false, false, false); + format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE, false, false, false); + format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT, false, false, false); + format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT, false, false, false); + format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT, false, false, false); + format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT, false, false, false); - format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); } return formatToGL; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 59927fc3aa..b7d11f9ba6 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -62,6 +62,7 @@ import com.jme3.texture.Texture.ShadowCompareMode; import com.jme3.texture.Texture.WrapAxis; import com.jme3.texture.TextureImage; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.LastTextureState; import com.jme3.util.BufferUtils; import com.jme3.util.ListMap; @@ -410,29 +411,35 @@ private void loadCapabilitiesCommon() { // == texture format extensions == - boolean hasFloatTexture; + boolean coreFloatTextures = caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) + || caps.contains(Caps.WebGL); + boolean arbFloatTextures = hasExtension("GL_ARB_texture_float"); + boolean hasFloatTexture = coreFloatTextures || arbFloatTextures || hasExtension("GL_OES_texture_float"); + boolean hasHalfFloatTexture = coreFloatTextures || hasExtension("GL_OES_texture_half_float") + || (arbFloatTextures && hasExtension("GL_ARB_half_float_pixel")); - hasFloatTexture = hasExtension("GL_OES_texture_half_float") && - hasExtension("GL_OES_texture_float"); + if (hasFloatTexture) { + caps.add(Caps.FloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = hasExtension("GL_ARB_texture_float") && - hasExtension("GL_ARB_half_float_pixel"); + if (hasHalfFloatTexture) { + caps.add(Caps.HalfFloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL); - } + if (hasFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_float_linear"))) { + caps.add(Caps.FloatTextureFilter); } - if (hasFloatTexture) { - caps.add(Caps.FloatTexture); + if (hasHalfFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_half_float_linear"))) { + caps.add(Caps.HalfFloatTextureFilter); } // integer texture format extensions - if(hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) - || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) + if (hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { caps.add(Caps.IntegerTexture); + } if (hasExtension("GL_OES_depth_texture") || hasExtension("WEBGL_depth_texture") || gl2 != null || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { @@ -444,6 +451,10 @@ private void loadCapabilitiesCommon() { caps.add(Caps.Depth24); } + if (caps.contains(Caps.OpenGL20) || hasExtension("GL_OES_depth32")) { + caps.add(Caps.Depth32); + } + if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) || hasExtension("GL_OES_rgb8_rgba8") || hasExtension("GL_ARM_rgba8") || @@ -452,20 +463,32 @@ private void loadCapabilitiesCommon() { } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) - || hasExtension("GL_OES_packed_depth_stencil")) { + || hasAnyExtension("GL_OES_packed_depth_stencil", "GL_EXT_packed_depth_stencil")) { caps.add(Caps.PackedDepthStencilBuffer); } - if (hasExtension("GL_ARB_color_buffer_float") && - hasExtension("GL_ARB_half_float_pixel") - ||caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL)) { - // XXX: Require both 16- and 32-bit float support for FloatColorBuffer. + boolean hasDesktopFloatColorBuffer = (hasExtension("GL_ARB_color_buffer_float") + && hasExtension("GL_ARB_texture_float") + && hasExtension("GL_ARB_half_float_pixel")) + || caps.contains(Caps.OpenGL30); + boolean hasExtFloatColorBuffer = hasExtension("GL_EXT_color_buffer_float"); + boolean hasExtHalfFloatColorBuffer = hasExtension("GL_EXT_color_buffer_half_float"); + + if (hasDesktopFloatColorBuffer || hasExtFloatColorBuffer) { caps.add(Caps.FloatColorBuffer); + caps.add(Caps.FloatColorBufferR); + caps.add(Caps.FloatColorBufferRG); caps.add(Caps.FloatColorBufferRGBA); - if (!caps.contains(Caps.OpenGLES30) && !caps.contains(Caps.WebGL)) { - caps.add(Caps.FloatColorBufferRGB); - } + caps.add(Caps.HalfFloatColorBufferR); + caps.add(Caps.HalfFloatColorBufferRG); + caps.add(Caps.HalfFloatColorBufferRGBA); + } else if (hasExtHalfFloatColorBuffer && hasHalfFloatTexture) { + caps.add(Caps.HalfFloatColorBufferRGBA); + } + + if (hasDesktopFloatColorBuffer) { + caps.add(Caps.FloatColorBufferRGB); + caps.add(Caps.HalfFloatColorBufferRGB); } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) @@ -473,15 +496,15 @@ private void loadCapabilitiesCommon() { caps.add(Caps.FloatDepthBuffer); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) + if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) + || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { - // Either GL3/GLES3 is available or both packed_float & half_float_pixel. caps.add(Caps.PackedFloatTexture); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30)) { + if ((hasExtension("GL_EXT_packed_float") && hasDesktopFloatColorBuffer) + || caps.contains(Caps.OpenGL30) || hasExtFloatColorBuffer) { caps.add(Caps.PackedFloatColorBuffer); } @@ -551,7 +574,12 @@ private void loadCapabilitiesCommon() { if (hasExtension("GL_EXT_texture_filter_anisotropic")) { caps.add(Caps.TextureFilterAnisotropic); - limits.put(Limits.TextureAnisotropy, getInteger(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT)); + floatBuf16.clear(); + gl.glGetFloat(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, floatBuf16); + limits.put(Limits.TextureAnisotropy, + Math.max(1, Math.round(floatBuf16.get(0)))); + } else { + limits.put(Limits.TextureAnisotropy, 1); } if (hasExtension("GL_EXT_framebuffer_object") @@ -619,6 +647,11 @@ private void loadCapabilitiesCommon() { || caps.contains(Caps.WebGL)) { caps.add(Caps.Srgb); } + if (hasExtension("GL_ARB_framebuffer_sRGB") + || caps.contains(Caps.OpenGL30) + || hasExtension("GL_EXT_sRGB_write_control")) { + caps.add(Caps.SrgbWriteControl); + } // Supports seamless cubemap if (hasExtension("GL_ARB_seamless_cube_map") || caps.contains(Caps.OpenGL32)) { @@ -647,7 +680,8 @@ private void loadCapabilitiesCommon() { caps.add(Caps.TesselationShader); } - if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) || caps.contains(Caps.OpenGLES31)) { + if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) + || caps.contains(Caps.OpenGLES31)) { caps.add(Caps.ShaderStorageBufferObject); limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE)); @@ -778,6 +812,29 @@ private void bindUniformBlock(int program, int uniformBlockIndex, int uniformBlo } } + private int getProgramResourceIndex(int program, int programInterface, String name) { + if (gl4 != null) { + return gl4.glGetProgramResourceIndex(program, programInterface, name); + } + return glext.glGetProgramResourceIndex(program, programInterface, name); + } + + private void bindShaderStorageBufferBase(int bindingPoint, int buffer) { + if (gl4 != null) { + gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } else { + glext.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } + } + + private void bindShaderStorageBlock(int program, int storageBlockIndex, int storageBlockBinding) { + if (gl4 != null) { + gl4.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } else { + glext.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + } + @SuppressWarnings("fallthrough") @Override public void initialize() { @@ -1583,11 +1640,11 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl if (bufferBlock.isUpdateNeeded() ) { int blockIndex = bufferBlock.getLocation(); if (blockIndex < 0) { - blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); + blockIndex = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); bufferBlock.setLocation(blockIndex); } if (bufferBlock.getLocation() != NativeObject.INVALID_ID) { - gl4.glShaderStorageBlockBinding(shaderId, bufferBlock.getLocation(), bindingPoint); + bindShaderStorageBlock(shaderId, bufferBlock.getLocation(), bindingPoint); } } break; @@ -2078,7 +2135,7 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { // Check NPOT requirements checkNonPowerOfTwo(tex); - updateTexImageData(image, tex.getType(), 0, false); + updateTexImageData(image, tex.getType(), 0, false, false); // NOTE: For depth textures, sets nearest/no-mips mode // Required to fix "framebuffer unsupported" @@ -2102,6 +2159,19 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { } public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { + Image.Format format = rb.getFormat(); + boolean depthTarget = rb.getSlot() == FrameBuffer.SLOT_DEPTH + || rb.getSlot() == FrameBuffer.SLOT_DEPTH_STENCIL; + boolean srgb = !depthTarget && fb.isSrgb(); + GLImageFormat glFormat = texUtil.getImageFormatWithError(format, srgb); + if (!depthTarget && !glFormat.colorRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not color-renderable and cannot be used as a color attachment."); + } else if (depthTarget && !glFormat.depthRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not depth-renderable and cannot be used as a depth attachment."); + } + boolean needAttach; if (rb.getTexture() == null) { // if it hasn't been created yet, then attach is required. @@ -2144,14 +2214,14 @@ private void toggleFramebufferSrgb(FrameBuffer fb) { boolean isSrgb = fb == null ? mainFrameBufferSrgb : fb.isSrgb(); if (isSrgb != context.srgbWriteEnabled) { - if (caps.contains(Caps.Srgb)) { + if (caps.contains(Caps.SrgbWriteControl) && caps.contains(Caps.Srgb)) { if (isSrgb) { gl.glEnable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } else { gl.glDisable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } - context.srgbWriteEnabled = isSrgb; } + context.srgbWriteEnabled = isSrgb; } } @@ -2299,11 +2369,17 @@ public void setFrameBuffer(FrameBuffer fb) { } // generate mipmaps for last FB if needed - if (context.boundFB != null && (context.boundFB.getMipMapsGenerationHint()!=null?context.boundFB.getMipMapsGenerationHint():generateMipmapsForFramebuffers)) { - for (int i = 0; i < context.boundFB.getNumColorBuffers(); i++) { - RenderBuffer rb = context.boundFB.getColorBuffer(i); + FrameBuffer boundFB = context.boundFB; + if (boundFB != null && (boundFB.getMipMapsGenerationHint() != null + ? boundFB.getMipMapsGenerationHint() + : generateMipmapsForFramebuffers)) { + for (int i = 0; i < boundFB.getNumColorBuffers(); i++) { + RenderBuffer rb = boundFB.getColorBuffer(i); Texture tex = rb.getTexture(); - if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + if (tex != null && tex.getMinFilter().usesMipMapLevels() + && isMipmapGenerationSupported(tex.getImage().getFormat(), + linearizeSrgbImages && boundFB.isSrgb() + ? ColorSpace.sRGB : ColorSpace.Linear)) { try { final int textureUnitIndex = 0; setTexture(textureUnitIndex, rb.getTexture()); @@ -2316,6 +2392,9 @@ public void setFrameBuffer(FrameBuffer fb) { int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); glfbo.glGenerateMipmapEXT(textureType); } + } else if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + logger.warning("Cannot generate mipmaps for framebuffer texture: " + tex + + " with image format: " + tex.getImage().getFormat()); } } } @@ -2519,7 +2598,11 @@ private void setupTextureParams(int unit, Texture tex) { boolean haveMips = true; if (image != null) { - haveMips = image.isGeneratedMipmapsRequired() || image.hasMipmaps(); + haveMips = image.hasMipmaps() + || image.isMipmapsGenerated() + || (image.isGeneratedMipmapsRequired() + && isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)); } LastTextureState curState = image.getLastTextureState(); @@ -2529,10 +2612,12 @@ private void setupTextureParams(int unit, Texture tex) { gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, convertMagFilter(tex.getMagFilter())); curState.magFilter = tex.getMagFilter(); } - if (curState.minFilter != tex.getMinFilter()) { + if (curState.minFilter != tex.getMinFilter() + || curState.minFilterMipmapsAvailable != haveMips) { bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, convertMinFilter(tex.getMinFilter(), haveMips)); curState.minFilter = tex.getMinFilter(); + curState.minFilterMipmapsAvailable = haveMips; } int desiredAnisoFilter = tex.getAnisotropicFilter() == 0 @@ -2703,7 +2788,13 @@ private void bindTextureOnly(int target, Image img, int unit) { * before being uploaded. */ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot) { + updateTexImageData(img, type, unit, scaleToPot, true); + } + + private void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot, + boolean allowCpuMipmapFallback) { int texId = img.getId(); + boolean textureWasUnuploaded = texId == -1; if (texId == -1) { // create texture gl.glGenTextures(intBuf1); @@ -2719,29 +2810,79 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s bindTextureAndUnit(target, img, unit); int imageSamples = img.getMultiSamples(); + boolean sourceMipmapsUsable = img.hasMipmaps() && !scaleToPot; + boolean needsMipmaps = !sourceMipmapsUsable && img.isGeneratedMipmapsRequired(); + boolean hwMipmapSupported = needsMipmaps && isMipmapGenerationSupported(img.getFormat(), + linearizeSrgbImages ? img.getColorSpace() : ColorSpace.Linear); + Image imageForUpload = img; + boolean cpuMipmapsGenerated = false; if (imageSamples <= 1) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) { - // Image does not have mipmaps, but they are required. - // Generate from base level. + boolean cpuMipmapFallbackFailed = false; + if (needsMipmaps) { + /* + * Some formats cannot use glGenerateMipmap because they are not both + * renderable and filterable. On a first upload, fall back to a CPU-built + * mip chain when the image data is suitable. If NPOT scaling is also + * required, build the CPU mips from the resized upload image. + */ + boolean needsCpuMipmapFallback = !hwMipmapSupported + && allowCpuMipmapFallback + && textureWasUnuploaded + && MipMapGenerator.canGenerateMipmaps(img); + if (needsCpuMipmapFallback) { + try { + Image cpuMipmapUploadImage = cloneImageForUpload(img, scaleToPot); + if (cpuMipmapUploadImage != null) { + MipMapGenerator.generateMipMaps(cpuMipmapUploadImage, linearizeSrgbImages, + img.getColorSpace() == ColorSpace.sRGB); + imageForUpload = cpuMipmapUploadImage; + cpuMipmapsGenerated = true; + scaleToPot = false; + img.setMipmapsGenerated(true); + } + } catch (RuntimeException exception) { + cpuMipmapFallbackFailed = true; + logger.log(Level.WARNING, + "Texture " + img + " requires mipmaps, but hardware mipmap generation is not supported" + + " and CPU mipmap generation failed. Mipmaps will not be generated.", + exception); + } + } - if (!caps.contains(Caps.FrameBuffer) && gl2 != null) { + /* + * Old desktop GL without FBO support can auto-generate mipmaps during + * texture upload. Newer paths generate explicitly after upload below. + */ + if (hwMipmapSupported && !caps.contains(Caps.FrameBuffer) && gl2 != null) { gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE); img.setMipmapsGenerated(true); - } else { - // For OpenGL3 and up. - // We'll generate mipmaps via glGenerateMipmapEXT (see below) } - } else if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30)) { - if (img.hasMipmaps()) { - // Image already has mipmaps, set the max level based on the - // number of mipmaps we have. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1); - } else { - // Image does not have mipmaps, and they are not required. - // Specify that the texture has no mipmaps. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0); + + if (!hwMipmapSupported + && !sourceMipmapsUsable + && !cpuMipmapsGenerated + && !cpuMipmapFallbackFailed) { + logger.log(Level.WARNING, "Texture " + img + " requires mipmaps, but hardware mipmaps generation is not supported. Mipmaps will not be generated."); } } + + /* + * Clamp the mip range to the levels actually uploaded. This is still + * needed when mipmaps are not requested, otherwise GL may sample + * missing levels left from a previous texture state. When hardware + * mipmap generation is pending, reopen the full generated range in + * case an earlier upload clamped this texture to the base level. + */ + boolean canSetTextureMaxLevel = caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30); + boolean hasUploadMipmaps = sourceMipmapsUsable || cpuMipmapsGenerated; + int uploadWidth = scaleToPot ? FastMath.nearestPowerOfTwo(img.getWidth()) : imageForUpload.getWidth(); + int uploadHeight = scaleToPot ? FastMath.nearestPowerOfTwo(img.getHeight()) : imageForUpload.getHeight(); + int maxLevel = textureMaxLevelForUpload(canSetTextureMaxLevel, needsMipmaps, hwMipmapSupported, + hasUploadMipmaps, cpuMipmapsGenerated ? imageForUpload.getMipMapSizes() : img.getMipMapSizes(), + generatedMipMaxLevel(uploadWidth, uploadHeight, imageForUpload.getDepth())); + if (maxLevel >= 0) { + gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, maxLevel); + } } else { // Check if graphics card doesn't support multisample textures if (!caps.contains(Caps.TextureMultisample)) { @@ -2782,11 +2923,8 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s } } - Image imageForUpload; if (scaleToPot) { imageForUpload = MipMapGenerator.resizeToPowerOf2(img); - } else { - imageForUpload = img; } if (target == GL.GL_TEXTURE_CUBE_MAP) { List data = imageForUpload.getData(); @@ -2821,16 +2959,80 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s img.setMultiSamples(imageSamples); } - if (caps.contains(Caps.FrameBuffer) || gl2 == null) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired() && img.getData(0) != null) { - glfbo.glGenerateMipmapEXT(target); - img.setMipmapsGenerated(true); - } + if (needsMipmaps && hwMipmapSupported + && (caps.contains(Caps.FrameBuffer) || gl2 == null) + && img.getData(0) != null + && !img.isMipmapsGenerated()) { + glfbo.glGenerateMipmapEXT(target); + img.setMipmapsGenerated(true); } img.clearUpdateNeeded(); } + private boolean isMipmapGenerationSupported(Image.Format format, ColorSpace colorSpace) { + GLImageFormat gf = texUtil.getImageFormat(format, colorSpace == ColorSpace.sRGB); + return gf != null && gf.colorRenderable && gf.filterable; + } + + static int textureMaxLevelForUpload(boolean canSetTextureMaxLevel, + boolean needsMipmaps, + boolean hwMipmapSupported, + boolean hasUploadMipmaps, + int[] uploadMipMapSizes, + int generatedMipMaxLevel) { + if (!canSetTextureMaxLevel) { + return -1; + } + if (needsMipmaps && hwMipmapSupported) { + return generatedMipMaxLevel; + } + if (!hasUploadMipmaps) { + return 0; + } + return uploadMipMapSizes.length - 1; + } + + static int generatedMipMaxLevel(int width, int height, int depth) { + int maxDimension = Math.max(Math.max(width, height), Math.max(1, depth)); + int maxLevel = 0; + while (maxDimension > 1) { + maxDimension >>= 1; + maxLevel++; + } + return maxLevel; + } + + private Image cloneImageForUpload(Image image, boolean scaleToPot) { + if (scaleToPot) { + return MipMapGenerator.resizeToPowerOf2(image); + } + + ArrayList data = new ArrayList<>(image.getData().size()); + for (ByteBuffer buffer : image.getData()) { + if (buffer == null) { + return null; + } + data.add(buffer.duplicate()); + } + return new Image(image.getFormat(), image.getWidth(), image.getHeight(), image.getDepth(), + data, null, image.getColorSpace()); + } + + private boolean needsGeneratedMipmaps(Image image) { + if (!image.isGeneratedMipmapsRequired() || image.isMipmapsGenerated()) { + return false; + } + + if (isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)) { + return true; + } + + return image.getId() == -1 + && MipMapGenerator.canGenerateMipmaps(image); + } + @Override public void setTexture(int unit, Texture tex) throws TextureUnitException { if (unit < 0 || unit >= RenderContext.maxTextureUnits) { @@ -2838,7 +3040,7 @@ public void setTexture(int unit, Texture tex) throws TextureUnitException { } Image image = tex.getImage(); - if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated())) { + if (image.isUpdateNeeded() || needsGeneratedMipmaps(image)) { // Check NPOT requirements boolean scaleToPot = false; @@ -2907,7 +3109,7 @@ public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferOb updateShaderStorageBufferObjectData(bufferObject); } if (context.boundBO[bindingPoint] == null || context.boundBO[bindingPoint].get() != bufferObject) { - gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, bufferObject.getId()); + bindShaderStorageBufferBase(bindingPoint, bufferObject.getId()); bufferObject.setBinding(bindingPoint); context.boundBO[bindingPoint] = bufferObject.getWeakRef(); } @@ -3157,14 +3359,14 @@ private void updateBufferData(int type, BufferObject bo) { BufferRegion reg; while ((reg = it.next()) != null) { - gl3.glBindBuffer(type, bufferId); + gl.glBindBuffer(type, bufferId); if (reg.isFullBufferRegion()) { ByteBuffer bbf = bo.getData(); if (logger.isLoggable(java.util.logging.Level.FINER)) { logger.log(java.util.logging.Level.FINER, "Update full buffer {0} with {1} bytes", new Object[] { bo, bbf.remaining() }); } gl.glBufferData(type, bbf, usage); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); break; } else { @@ -3172,7 +3374,7 @@ private void updateBufferData(int type, BufferObject bo) { logger.log(java.util.logging.Level.FINER, "Update region {0} of {1}", new Object[] { reg, bo }); } gl.glBufferSubData(type, reg.getStart(), reg.getData()); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); } } @@ -3593,7 +3795,7 @@ public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceDat @Override public void setMainFrameBufferSrgb(boolean enableSrgb) { // Gamma correction - if (!caps.contains(Caps.Srgb) && enableSrgb) { + if ((!caps.contains(Caps.SrgbWriteControl) || !caps.contains(Caps.Srgb)) && enableSrgb) { // Not supported, sorry. logger.warning("sRGB framebuffer is not supported " + "by video hardware, but was requested."); @@ -3699,7 +3901,7 @@ public boolean isLinearizeSrgbImages() { */ @Override public boolean isMainFrameBufferSrgb() { - if (!caps.contains(Caps.Srgb)) { + if (!caps.contains(Caps.Srgb) || !caps.contains(Caps.SrgbWriteControl)) { return false; } else { return mainFrameBufferSrgb; diff --git a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java index f3cc721df9..8f696f4b49 100644 --- a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java +++ b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java @@ -849,8 +849,9 @@ public long getUniqueId() { * * The FrameBuffer must have an SRGB texture attached. * - * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} capability - * for this option to take any effect. + * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} and + * {@link Caps#SrgbWriteControl sRGB write control} capabilities for this + * option to take any effect. * * Rendering operations performed on this framebuffer shall undergo a linear * -> sRGB color space conversion when this flag is enabled. If diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3159f0856a..3181abd4f9 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -79,16 +79,16 @@ public enum Format { Reserved2(0), /** - * half-precision floating-point grayscale/luminance. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16F(16,true), /** * single-precision floating-point grayscale/luminance. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#FloatTexture}. */ Luminance32F(32,true), @@ -101,9 +101,9 @@ public enum Format { Reserved3(0), /** - * half-precision floating-point grayscale/luminance and alpha. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance and alpha. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16FAlpha16F(32,true), @@ -255,7 +255,7 @@ public enum Format { * but will be converted to {@link Format#RGB111110F} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#PackedFloatTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}. */ RGB16F_to_RGB111110F(48,true), @@ -271,7 +271,7 @@ public enum Format { * but will be converted to {@link Format#RGB9E5} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#SharedExponentTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}. */ RGB16F_to_RGB9E5(48,true), @@ -283,9 +283,9 @@ public enum Format { RGB9E5(32,true), /** - * half-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating point red, green, and blue. + * + * Requires {@link Caps#HalfFloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB16F(48,true), @@ -293,22 +293,22 @@ public enum Format { /** * half-precision floating point red, green, blue, and alpha. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RGBA16F(64,true), /** - * single-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, and blue. + * + * Requires {@link Caps#FloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB32F(96,true), /** - * single-precision floating point red, green, blue and alpha. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, blue and alpha. + * + * Requires {@link Caps#FloatTexture}. */ RGBA32F(128,true), @@ -507,21 +507,21 @@ public enum Format { /** * half-precision floating point red. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ R16F(16,true), /** - * single-precision floating point red. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. */ R32F(32,true), /** * half-precision floating point red and green. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RG16F(32,true), diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java index 0fe86270d8..e329f21a1f 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java @@ -172,11 +172,15 @@ public ImageCodec(int bpp, int flags, int maxAlpha, int maxRed, int maxGreen, in * @param format The format to lookup. * @return The codec capable of decoding it, or null if not found. */ - public static ImageCodec lookup(Format format) { - ImageCodec codec = params.get(format); - if (codec == null) { - throw new UnsupportedOperationException("The format " + format + " is not supported"); - } - return codec; - } -} + public static ImageCodec lookup(Format format) { + ImageCodec codec = params.get(format); + if (codec == null) { + throw new UnsupportedOperationException("The format " + format + " is not supported"); + } + return codec; + } + + static boolean isSupported(Format format) { + return params.containsKey(format); + } +} diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java index 1a75203225..18b21d364b 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java @@ -62,11 +62,22 @@ * * @author Kirill Vainer */ -public abstract class ImageRaster { - - /** - * Create new image reader / writer. - * +public abstract class ImageRaster { + + /** + * Tests whether {@link ImageRaster} can read and write pixels for the + * specified image format. + * + * @param format the image format to test + * @return true if ImageRaster supports the format + */ + public static boolean isSupported(Image.Format format) { + return ImageCodec.isSupported(format); + } + + /** + * Create new image reader / writer. + * * @param image The image to read / write to. * @param slice Which slice to use. Only applies to 3D images, 2D image * arrays or cubemaps. diff --git a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java index 7a08e1cdde..7c49782058 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java @@ -45,6 +45,7 @@ public final class LastTextureState { public Texture.WrapMode sWrap, tWrap, rWrap; public Texture.MagFilter magFilter; public Texture.MinFilter minFilter; + public boolean minFilterMipmapsAvailable; public int anisoFilter; public Texture.ShadowCompareMode shadowCompareMode; @@ -58,6 +59,7 @@ public void reset() { rWrap = null; magFilter = null; minFilter = null; + minFilterMipmapsAvailable = false; anisoFilter = 1; // The default in OpenGL is OFF, so we avoid setting this per texture diff --git a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java index 7073dfab52..9e8ac4bbd0 100644 --- a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java +++ b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,99 +34,605 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.ImageRaster; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Locale; -public class MipMapGenerator { +public final class MipMapGenerator { + + private static final float EPSILON_ALPHA = 1e-8f; private MipMapGenerator() { } + /** + * Scales the base level of a 2D image. + * + * The returned image keeps the same Image.Format and ColorSpace as the input. + * Pixel format conversion is delegated to ImageRaster. + * + * For normal color textures, this method filters in linear space. + */ public static Image scaleImage(Image inputImage, int outputWidth, int outputHeight) { - int size = outputWidth * outputHeight * inputImage.getFormat().getBitsPerPixel() / 8; - ByteBuffer buffer = BufferUtils.createByteBuffer(size); - Image outputImage = new Image(inputImage.getFormat(), - outputWidth, - outputHeight, - buffer, - inputImage.getColorSpace()); - - ImageRaster input = ImageRaster.create(inputImage, 0, 0, false); - ImageRaster output = ImageRaster.create(outputImage, 0, 0, false); - - float xRatio = ((float) (input.getWidth() - 1)) / output.getWidth(); - float yRatio = ((float) (input.getHeight() - 1)) / output.getHeight(); - - ColorRGBA outputColor = new ColorRGBA(0, 0, 0, 0); - ColorRGBA bottomLeft = new ColorRGBA(); - ColorRGBA bottomRight = new ColorRGBA(); - ColorRGBA topLeft = new ColorRGBA(); - ColorRGBA topRight = new ColorRGBA(); - - for (int y = 0; y < outputHeight; y++) { - for (int x = 0; x < outputWidth; x++) { - float x2f = x * xRatio; - float y2f = y * yRatio; - - int x2 = (int) x2f; - int y2 = (int) y2f; - - input.getPixel(x2, y2, bottomLeft); - input.getPixel(x2 + 1, y2, bottomRight); - input.getPixel(x2, y2 + 1, topLeft); - input.getPixel(x2 + 1, y2 + 1, topRight); - - outputColor.set(bottomLeft).addLocal(bottomRight) - .addLocal(topLeft).addLocal(topRight); - outputColor.multLocal(1f / 4f); - output.setPixel(x, y, outputColor); - } - } - return outputImage; + return scaleImage(inputImage, outputWidth, outputHeight, true, isSrgb(inputImage)); + } + + /** + * Scales the base level of a 2D image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha to reduce transparent-edge halos + */ + public static Image scaleImage(Image inputImage, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + return scaleLevel(inputImage, 0, outputWidth, outputHeight, convertToLinear, alphaWeighted); } - public static Image resizeToPowerOf2(Image original){ + public static Image resizeToPowerOf2(Image original) { int potWidth = FastMath.nearestPowerOfTwo(original.getWidth()); int potHeight = FastMath.nearestPowerOfTwo(original.getHeight()); return scaleImage(original, potWidth, potHeight); } - public static void generateMipMaps(Image image){ - int width = image.getWidth(); - int height = image.getHeight(); + /** + * Returns true if this image has CPU-side data in a format supported by this + * mipmap generator. + * + * This generator works on uncompressed, non-depth, byte-addressable texture + * formats supported by ImageRaster. + */ + public static boolean canGenerateMipmaps(Image image) { + if (image == null + || image.getWidth() < 1 + || image.getHeight() < 1 + || image.getDepth() > 1 + || image.getFormat().isCompressed() + || image.getFormat().isDepthFormat() + || image.getData() == null + || image.getData().isEmpty()) { + return false; + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + return false; + } + + int baseLevelSize; + try { + baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + } catch (RuntimeException exception) { + return false; + } + + for (ByteBuffer data : image.getData()) { + if (data == null || data.capacity() < baseLevelSize) { + return false; + } + } + + return ImageRaster.isSupported(image.getFormat()); + } + + /** + * Generates a complete mip chain for the image. + * + * Default behavior is intended for normal color/albedo textures: + * - filtering is done in linear space; + * - sRGB images with alpha use alpha-weighted RGB filtering. + * + * For normal maps, roughness, metallic, AO, height maps, or packed data maps, + * prefer generateMipMaps(image, true, false), assuming the image is not marked sRGB. + */ + public static void generateMipMaps(Image image) { + generateMipMaps(image, true, isSrgb(image)); + } + + /** + * Generates a complete mip chain for every data buffer/slice in the image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha + */ + public static void generateMipMaps(Image image, boolean convertToLinear, boolean alphaWeighted) { + validateImage(image); + + int baseWidth = image.getWidth(); + int baseHeight = image.getHeight(); + + ArrayList chains = new ArrayList<>(image.getData().size()); + int dataCount = image.getData().size(); + + for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { + chains.add(generateMipChainForSlice( + image, + dataIndex, + baseWidth, + baseHeight, + convertToLinear, + alphaWeighted + )); + } + + for (int dataIndex = 0; dataIndex < chains.size(); dataIndex++) { + image.setData(dataIndex, chains.get(dataIndex).combinedData); + } + + if (!chains.isEmpty()) { + image.setMipMapSizes(chains.get(0).mipSizes); + } + } + + private static MipChain generateMipChainForSlice(Image sourceImage, + int sourceSlice, + int baseWidth, + int baseHeight, + boolean convertToLinear, + boolean alphaWeighted) { + ArrayList levels = new ArrayList<>(); + + Image.Format format = sourceImage.getFormat(); + ColorSpace colorSpace = sourceImage.getColorSpace(); + + ByteBuffer baseLevel = copyBaseLevel( + sourceImage.getData(sourceSlice), + levelSize(format, baseWidth, baseHeight) + ); + + Image current = new Image(format, baseWidth, baseHeight, baseLevel, colorSpace); + levels.add(baseLevel); + + int width = baseWidth; + int height = baseHeight; + + while (width > 1 || height > 1) { + int nextWidth = Math.max(1, width / 2); + int nextHeight = Math.max(1, height / 2); + + Image next = scaleLevel( + current, + 0, + nextWidth, + nextHeight, + convertToLinear, + alphaWeighted + ); + + levels.add(next.getData(0)); + + current = next; + width = nextWidth; + height = nextHeight; + } - Image current = image; - ArrayList output = new ArrayList<>(); int totalSize = 0; + int[] mipSizes = new int[levels.size()]; + + for (int i = 0; i < levels.size(); i++) { + int size = levels.get(i).capacity(); + mipSizes[i] = size; + totalSize += size; + } - while (height >= 1 || width >= 1){ - output.add(current.getData(0)); - totalSize += current.getData(0).capacity(); + ByteBuffer combined = BufferUtils.createByteBuffer(totalSize); - if (height == 1 || width == 1) { - break; + for (ByteBuffer level : levels) { + ByteBuffer duplicate = level.duplicate(); + duplicate.clear(); + combined.put(duplicate); + } + + combined.flip(); + + return new MipChain(combined, mipSizes); + } + + private static Image scaleLevel(Image inputImage, + int inputSlice, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + if (outputWidth < 1 || outputHeight < 1) { + throw new IllegalArgumentException("Output size must be at least 1x1"); + } + + validateImage(inputImage); + + int outputSize = levelSize(inputImage.getFormat(), outputWidth, outputHeight); + ByteBuffer outputBuffer = BufferUtils.createByteBuffer(outputSize); + + Image outputImage = new Image( + inputImage.getFormat(), + outputWidth, + outputHeight, + outputBuffer, + inputImage.getColorSpace() + ); + + ImageRaster input = ImageRaster.create(inputImage, inputSlice, 0, convertToLinear); + ImageRaster output = ImageRaster.create(outputImage, 0, 0, convertToLinear); + + boolean downscale = outputWidth <= input.getWidth() && outputHeight <= input.getHeight(); + boolean clampOutput = !inputImage.getFormat().isFloatingPont(); + + if (downscale) { + areaResample(input, output, alphaWeighted, clampOutput); + } else { + bilinearResample(input, output, alphaWeighted, clampOutput); + } + + return outputImage; + } + + /** + * Area filter. + * + * This is the right default for mipmap generation because every destination + * pixel represents the average area of the corresponding source rectangle. + */ + private static void areaResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); + + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); + + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY0 = y * scaleY; + double sourceY1 = (y + 1) * scaleY; + + int yStart = Math.max(0, (int) Math.floor(sourceY0)); + int yEnd = Math.min(sourceHeight, Math.max(yStart + 1, (int) Math.ceil(sourceY1))); + + for (int x = 0; x < targetWidth; x++) { + double sourceX0 = x * scaleX; + double sourceX1 = (x + 1) * scaleX; + + int xStart = Math.max(0, (int) Math.floor(sourceX0)); + int xEnd = Math.min(sourceWidth, Math.max(xStart + 1, (int) Math.ceil(sourceX1))); + + accumulator.clear(); + + for (int sy = yStart; sy < yEnd; sy++) { + double overlapY0 = Math.max(sourceY0, sy); + double overlapY1 = Math.min(sourceY1, sy + 1.0); + float weightY = (float) Math.max(0.0, overlapY1 - overlapY0); + + if (weightY <= 0f) { + continue; + } + + for (int sx = xStart; sx < xEnd; sx++) { + double overlapX0 = Math.max(sourceX0, sx); + double overlapX1 = Math.min(sourceX1, sx + 1.0); + float weightX = (float) Math.max(0.0, overlapX1 - overlapX0); + + if (weightX <= 0f) { + continue; + } + + float weight = weightX * weightY; + + input.getPixel(sx, sy, sample); + accumulator.add(sample, weight, alphaWeighted, clampOutput); + } + } + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); } + } + } + + /** + * Bilinear filter. + * + * Used only when scaleImage() is asked to upscale. + * Mipmap generation itself normally uses areaResample(). + */ + private static void bilinearResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); - height /= 2; - width /= 2; + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); - current = scaleImage(current, width, height); + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY = (y + 0.5) * scaleY - 0.5; + + int y0 = (int) Math.floor(sourceY); + double ty = sourceY - y0; + + if (y0 < 0) { + y0 = 0; + ty = 0.0; + } + + int y1 = y0 + 1; + + if (y1 >= sourceHeight) { + y1 = sourceHeight - 1; + y0 = y1; + ty = 0.0; + } + + float wy0 = (float) (1.0 - ty); + float wy1 = (float) ty; + + for (int x = 0; x < targetWidth; x++) { + double sourceX = (x + 0.5) * scaleX - 0.5; + + int x0 = (int) Math.floor(sourceX); + double tx = sourceX - x0; + + if (x0 < 0) { + x0 = 0; + tx = 0.0; + } + + int x1 = x0 + 1; + + if (x1 >= sourceWidth) { + x1 = sourceWidth - 1; + x0 = x1; + tx = 0.0; + } + + float wx0 = (float) (1.0 - tx); + float wx1 = (float) tx; + + accumulator.clear(); + + input.getPixel(x0, y0, sample); + accumulator.add(sample, wx0 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x1, y0, sample); + accumulator.add(sample, wx1 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x0, y1, sample); + accumulator.add(sample, wx0 * wy1, alphaWeighted, clampOutput); + + input.getPixel(x1, y1, sample); + accumulator.add(sample, wx1 * wy1, alphaWeighted, clampOutput); + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); + } + } + } + + private static void validateImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null"); } - ByteBuffer combinedData = BufferUtils.createByteBuffer(totalSize); - int[] mipSizes = new int[output.size()]; - for (int i = 0; i < output.size(); i++){ - ByteBuffer data = output.get(i); - data.clear(); - combinedData.put(data); - mipSizes[i] = data.capacity(); + if (image.getWidth() < 1 || image.getHeight() < 1) { + throw new IllegalArgumentException("Image size must be at least 1x1"); } - combinedData.flip(); - // insert mip data into image - image.setData(0, combinedData); - image.setMipMapSizes(mipSizes); + if (image.getData() == null || image.getData().isEmpty()) { + throw new IllegalArgumentException("Image has no data buffers"); + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + throw new UnsupportedOperationException( + "CPU mipmap generation requires byte-addressable formats. Unsupported format: " + + image.getFormat() + + " with " + + bitsPerPixel + + " bits per pixel" + ); + } + + int baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + for (int dataIndex = 0; dataIndex < image.getData().size(); dataIndex++) { + ByteBuffer data = image.getData(dataIndex); + if (data == null) { + throw new IllegalArgumentException("Image data buffer " + dataIndex + " is null"); + } + if (data.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data buffer " + dataIndex + " is smaller than expected base level size. Data capacity=" + + data.capacity() + + ", expected=" + + baseLevelSize + ); + } + } + } + + private static int levelSize(Image.Format format, int width, int height) { + int bitsPerPixel = format.getBitsPerPixel(); + + long bits = (long) width * (long) height * (long) bitsPerPixel; + + if ((bits % 8L) != 0L) { + throw new UnsupportedOperationException( + "Image level is not byte-addressable: " + + width + + "x" + + height + + " " + + format + ); + } + + long bytes = bits / 8L; + + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Image level is too large: " + + width + + "x" + + height + + " " + + format + ); + } + + return (int) bytes; + } + + /** + * If the input image already has mipmaps, its ByteBuffer may contain all levels. + * For rebuilding mipmaps, we only want the base level. + */ + private static ByteBuffer copyBaseLevel(ByteBuffer source, int baseLevelSize) { + if (source == null) { + throw new IllegalArgumentException("Image data buffer is null"); + } + if (source.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data is smaller than expected base level size. Data capacity=" + + source.capacity() + + ", expected=" + + baseLevelSize + ); + } + + ByteBuffer duplicate = source.duplicate(); + duplicate.clear(); + duplicate.limit(baseLevelSize); + + ByteBuffer copy = BufferUtils.createByteBuffer(baseLevelSize); + copy.put(duplicate); + copy.flip(); + + return copy; + } + + private static boolean isSrgb(Image image) { + if (image.getColorSpace() == ColorSpace.sRGB) { + return true; + } + + String formatName = image.getFormat().name().toLowerCase(Locale.ROOT); + return formatName.contains("srgb"); + } + + private static final class MipChain { + final ByteBuffer combinedData; + final int[] mipSizes; + + MipChain(ByteBuffer combinedData, int[] mipSizes) { + this.combinedData = combinedData; + this.mipSizes = mipSizes; + } + } + + private static final class PixelAccumulator { + private float r; + private float g; + private float b; + private float a; + private float weight; + + void clear() { + r = 0f; + g = 0f; + b = 0f; + a = 0f; + weight = 0f; + } + + void add(ColorRGBA color, float sampleWeight, boolean alphaWeighted, boolean clampOutput) { + if (sampleWeight <= 0f) { + return; + } + + float alpha = clampOutput ? clamp01(color.a) : color.a; + + if (alphaWeighted) { + r += color.r * alpha * sampleWeight; + g += color.g * alpha * sampleWeight; + b += color.b * alpha * sampleWeight; + } else { + r += color.r * sampleWeight; + g += color.g * sampleWeight; + b += color.b * sampleWeight; + } + + a += alpha * sampleWeight; + weight += sampleWeight; + } + + void toColor(ColorRGBA store, boolean alphaWeighted, boolean clampOutput) { + if (weight <= 0f) { + store.set(0f, 0f, 0f, 0f); + return; + } + + float outA = a / weight; + + float outR; + float outG; + float outB; + + if (alphaWeighted) { + if (a > EPSILON_ALPHA) { + outR = r / a; + outG = g / a; + outB = b / a; + } else { + outR = 0f; + outG = 0f; + outB = 0f; + } + } else { + outR = r / weight; + outG = g / weight; + outB = b / weight; + } + + if (clampOutput) { + outR = clamp01(outR); + outG = clamp01(outG); + outB = clamp01(outB); + outA = clamp01(outA); + } + + store.set(outR, outG, outB, outA); + } + + private static float clamp01(float value) { + if (value <= 0f) { + return 0f; + } + + if (value >= 1f) { + return 1f; + } + + return value; + } } } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag new file mode 100644 index 0000000000..7ca7901b22 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag @@ -0,0 +1,21 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/ShaderLib/MultiSample.glsllib" + +uniform COLORTEXTURE m_Texture; +varying vec2 texCoord; + +vec3 linearToSrgb(vec3 color) { + vec3 linear = max(color, vec3(0.0)); + vec3 encodedLow = linear * 12.92; + vec3 encodedHigh = 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055; + return mix(encodedLow, encodedHigh, step(vec3(0.0031308), linear)); +} + +void main() { + vec4 color = getColor(m_Texture, texCoord); + #ifdef SRGB + gl_FragColor = vec4(linearToSrgb(color.rgb), color.a); + #else + gl_FragColor = color; + #endif +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md new file mode 100644 index 0000000000..2197032e08 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md @@ -0,0 +1,24 @@ +MaterialDef Blit { + + MaterialParameters { + Int BoundDrawBuffer + Int NumSamples + Boolean Srgb + Texture2D Texture + } + + Technique { + VertexShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.vert + FragmentShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.frag + + WorldParameters { + } + + Defines { + BOUND_DRAW_BUFFER : BoundDrawBuffer + RESOLVE_MS : NumSamples + SRGB: Srgb + } + } + +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert new file mode 100644 index 0000000000..5d55b1f5af --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert @@ -0,0 +1,11 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +attribute vec4 inPosition; +attribute vec2 inTexCoord; + +varying vec2 texCoord; + +void main() { + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); + texCoord = inTexCoord; +} \ No newline at end of file diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java new file mode 100644 index 0000000000..3338e6d4b4 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import com.jme3.renderer.Caps; +import com.jme3.texture.Image; +import java.util.EnumSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class GLImageFormatsTest { + + @Test + public void testGles3UsesCoreHalfFloatType() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.FloatTexture, Caps.HalfFloatTexture); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.R16F.ordinal()].dataType); + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.RGBA16F.ordinal()].dataType); + } + + @Test + public void testGles3DoesNotExposeDesktopByteOrderFormats() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.BGR8.ordinal()]); + assertNull(formats[0][Image.Format.ABGR8.ordinal()]); + assertNull(formats[0][Image.Format.ARGB8.ordinal()]); + assertNull(formats[0][Image.Format.BGRA8.ordinal()]); + assertNull(formats[1][Image.Format.BGR8.ordinal()]); + assertNull(formats[1][Image.Format.ABGR8.ordinal()]); + assertNull(formats[1][Image.Format.ARGB8.ordinal()]); + assertNull(formats[1][Image.Format.BGRA8.ordinal()]); + } + + @Test + public void testGles3LegacyAlphaUsesGlesInternalFormatWhenNoCoreProfile() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].internalFormat); + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].format); + } + + @Test + public void testGles3CoreFormatsRemainMapped() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb, Caps.FloatTexture, + Caps.IntegerTexture, Caps.PackedFloatTexture, + Caps.SharedExponentTexture, Caps.TextureCompressionETC2, + Caps.Depth24, Caps.FloatDepthBuffer, Caps.PackedDepthStencilBuffer); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.RGB10A2.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB111110F.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB9E5.ordinal()]); + assertNotNull(formats[0][Image.Format.RGBA8UI.ordinal()]); + assertNotNull(formats[0][Image.Format.ETC2.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + } + + @Test + public void testDepthFormatsFollowExplicitCaps() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth24); + caps.add(Caps.FloatDepthBuffer); + caps.add(Caps.PackedDepthStencilBuffer); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth32); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth32.ordinal()]); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java new file mode 100644 index 0000000000..26d9aba160 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GLRendererMipmapPolicyTest { + + @Test + public void testDoesNotClampWhenHardwareMipmapGenerationIsPending() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + true, + false, + null, + 7); + + assertEquals(7, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenNoMipmapsAreUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + false, + false, + false, + null, + 4); + + assertEquals(0, maxLevel); + } + + @Test + public void testUsesUploadedMipCountForExistingOrCpuMipmaps() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + true, + new int[] {64, 16, 4, 1}, + 7); + + assertEquals(3, maxLevel); + } + + @Test + public void testSkipsMaxLevelWhenCapabilityIsUnavailable() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + false, + false, + false, + true, + new int[] {64, 16}, + 7); + + assertEquals(-1, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenRequestedMipmapsCannotBeGeneratedOrUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + false, + null, + 7); + + assertEquals(0, maxLevel); + } + + @Test + public void testGeneratedMipMaxLevelUsesLargestUploadDimension() { + assertEquals(0, GLRenderer.generatedMipMaxLevel(1, 1, 1)); + assertEquals(2, GLRenderer.generatedMipMaxLevel(3, 5, 1)); + assertEquals(3, GLRenderer.generatedMipMaxLevel(4, 8, 1)); + assertEquals(4, GLRenderer.generatedMipMaxLevel(4, 8, 16)); + } +} diff --git a/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java new file mode 100644 index 0000000000..fc2a6765d8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.util; + +import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MipMapGeneratorTest { + + @Test + public void testGenerateMipMapsAfterResizeToPowerOf2() { + ByteBuffer data = BufferUtils.createByteBuffer(3 * 5 * 4); + for (int i = 0; i < data.capacity(); i++) { + data.put((byte) i); + } + data.flip(); + + Image image = new Image(Image.Format.RGBA8, 3, 5, data, ColorSpace.Linear); + Image resized = MipMapGenerator.resizeToPowerOf2(image); + + MipMapGenerator.generateMipMaps(resized, true, false); + + assertEquals(4, resized.getWidth()); + assertEquals(8, resized.getHeight()); + assertTrue(resized.hasMipmaps()); + assertNotNull(resized.getData(0)); + assertNotNull(resized.getMipMapSizes()); + assertTrue(resized.getMipMapSizes().length > 1); + } + + @Test + public void testGenerateMipMapsRejectsNullDataBuffer() { + ArrayList data = new ArrayList<>(); + data.add(null); + Image image = new Image(Image.Format.RGBA8, 2, 2, 1, data, null, ColorSpace.Linear); + + assertThrows(IllegalArgumentException.class, + () -> MipMapGenerator.generateMipMaps(image, true, false)); + } +} diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java index 9bbfb86f5c..44ded69fee 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java @@ -234,24 +234,18 @@ public void render(RenderManager rm) { */ private void setupMaterials(Application app) { AssetManager manager = app.getAssetManager(); - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(manager, ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(manager, ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(manager, ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(manager, ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(manager, ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(manager, ColorRGBA.Pink); + } + + private static Material createDebugMaterial(AssetManager manager, ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } private void updateRigidBodies() { diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java index 16bc38ac8c..7ea8f3ba0c 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java @@ -268,23 +268,17 @@ protected void setupDebugNode() { * Initialize all the DebugTools materials. */ protected void setupMaterials() { - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(ColorRGBA.Pink); + } + + private Material createDebugMaterial(ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } } diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java index 3e74f1ab3c..41c3b062d7 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java @@ -132,10 +132,12 @@ public static Mesh getDebugMesh(CollisionShape shape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConvexShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } else if (shape.getCShape() instanceof ConcaveShape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConcaveShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } return mesh; } @@ -157,10 +159,10 @@ private static FloatBuffer getVertices(ConcaveShape concaveShape) { /** * Processes the given convex shape to retrieve a correctly ordered FloatBuffer to - * construct the shape from with a TriMesh. + * construct the shape from with line segments. * * @param convexShape the shape to retrieve the vertices from. - * @return the vertices as a FloatBuffer, ordered as Triangles. + * @return the vertices as a FloatBuffer, ordered as line segments. */ private static FloatBuffer getVertices(ConvexShape convexShape) { // Check there is a hull shape to render @@ -180,8 +182,8 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { assert hull.numTriangles() > 0 : "Expecting the Hull shape to have triangles"; int numberOfTriangles = hull.numTriangles(); - // The number of bytes needed is: (floats in a vertex) * (vertices in a triangle) * (# of triangles) * (size of float in bytes) - final int numberOfFloats = 3 * 3 * numberOfTriangles; + // The number of floats needed is: (floats in a vertex) * (vertices in a triangle outline) * (# of triangles) + final int numberOfFloats = 3 * 6 * numberOfTriangles; FloatBuffer vertices = BufferUtils.createFloatBuffer(numberOfFloats); // Force the limit, set the cap - the largest number of floats we will use the buffer for @@ -199,15 +201,24 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { vertexB = hullVertices.get(hullIndices.get(index++)); vertexC = hullVertices.get(hullIndices.get(index++)); - // Put the vertices into the vertex buffer - vertices.put(vertexA.x).put(vertexA.y).put(vertexA.z); - vertices.put(vertexB.x).put(vertexB.y).put(vertexB.z); - vertices.put(vertexC.x).put(vertexC.y).put(vertexC.z); + putTriangleOutline(vertices, vertexA, vertexB, vertexC); } vertices.clear(); return vertices; } + + private static void putTriangleOutline(FloatBuffer vertices, Vector3f vertexA, + Vector3f vertexB, Vector3f vertexC) { + putLine(vertices, vertexA, vertexB); + putLine(vertices, vertexB, vertexC); + putLine(vertices, vertexC, vertexA); + } + + private static void putLine(FloatBuffer vertices, Vector3f start, Vector3f end) { + vertices.put(start.x).put(start.y).put(start.z); + vertices.put(end.x).put(end.y).put(end.z); + } } /** @@ -227,11 +238,13 @@ public BufferedTriangleCallback() { @Override public void processTriangle(Vector3f[] triangle, int partId, int triangleIndex) { - // Three sets of individual lines // The new Vector is needed as the given triangle reference is from a pool vertices.add(new Vector3f(triangle[0])); vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[2])); vertices.add(new Vector3f(triangle[2])); + vertices.add(new Vector3f(triangle[0])); } /** diff --git a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java index f51dbf56b3..0bbefd7cf5 100644 --- a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java +++ b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java @@ -40,10 +40,11 @@ import com.jme3.bullet.collision.shapes.CollisionShape; import com.jme3.bullet.collision.shapes.SphereCollisionShape; import com.jme3.bullet.control.BetterCharacterControl; -import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.KinematicRagdollControl; +import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.bullet.util.DebugShapeFactory; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.binary.BinaryExporter; @@ -171,6 +172,21 @@ public InputStream openStream() { Assertions.assertEquals(new Vector3f(0.26f, 0.27f, 0.28f), rbcCopy.getLinearVelocity()); } + /** + * Debug collision meshes should render as line primitives instead of + * relying on OpenGL polygon wireframe mode, which is unavailable in GLES. + */ + @Test + public void testDebugMeshesUseLines() { + CollisionShape shape = new BoxCollisionShape(Vector3f.UNIT_XYZ); + Mesh mesh = DebugShapeFactory.getDebugMesh(shape); + + Assertions.assertNotNull(mesh); + Assertions.assertEquals(Mesh.Mode.Lines, mesh.getMode()); + Assertions.assertTrue(mesh.getVertexCount() > 0); + Assertions.assertEquals(0, mesh.getVertexCount() % 6); + } + /** * Test case for JME issue #1004: RagdollUtils can't handle 16-bit bone indices. */ diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 56ef07ffab..4d132618cd 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -7,6 +7,8 @@ import java.nio.IntBuffer; import org.lwjgl.opengl.ARBDrawInstanced; import org.lwjgl.opengl.ARBInstancedArrays; +import org.lwjgl.opengl.ARBProgramInterfaceQuery; +import org.lwjgl.opengl.ARBShaderStorageBufferObject; import org.lwjgl.opengl.ARBSync; import org.lwjgl.opengl.ARBTextureMultisample; import org.lwjgl.opengl.ARBUniformBufferObject; @@ -87,6 +89,16 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(int condition, int flags) { return ARBSync.glFenceSync(condition, flags); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 74736f0666..83836aa3da 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -103,6 +103,16 @@ public void glUniformBlockBinding(final int program, final int uniformBlockIndex ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(final int condition, final int flags) { return ARBSync.glFenceSync(condition, flags);
The real AndroidX Fragment dependency must be supplied by the Android + * application. This class is excluded from jme3-android artifacts.
AndroidHarness
- * Android MediaPlayer / SoundPool can be used on all - * supported Android platform versions (2.2+) - * OpenAL Soft uses an OpenSL backend and is only supported on Android - * versions 2.3+. - *
- * Only use ANDROID_ static strings found in AppSettings - * - */ - protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT; - - /** - * If true Android Sensors are used as simulated Joysticks. Users can use the - * Android sensor feedback through the RawInputListener or by registering - * JoyAxisTriggers. - */ - protected boolean joystickEventsEnabled = false; - /** - * If true KeyEvents are generated from TouchEvents - */ - protected boolean keyEventsEnabled = true; - /** - * If true MouseEvents are generated from TouchEvents - */ - protected boolean mouseEventsEnabled = true; - /** - * Flip X axis - */ - protected boolean mouseEventsInvertX = false; - /** - * Flip Y axis - */ - protected boolean mouseEventsInvertY = false; - /** - * if true finish this activity when the jme app is stopped - */ - protected boolean finishOnAppStop = true; - /** - * set to false if you don't want the harness to handle the exit hook - */ - protected boolean handleExitHook = true; - /** - * Title of the exit dialog, default is "Do you want to exit?" - */ - protected String exitDialogTitle = "Do you want to exit?"; - /** - * Message of the exit dialog, default is "Use your home key to bring this - * app into the background or exit to terminate it." - */ - protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; - /** - * Set the screen window mode. If screenFullSize is true, then the - * notification bar and title bar are removed and the screen covers the - * entire display. If screenFullSize is false, then the notification bar - * remains visible if screenShowTitle is true while screenFullScreen is - * false, then the title bar is also displayed under the notification bar. - */ - protected boolean screenFullScreen = true; - /** - * if screenShowTitle is true while screenFullScreen is false, then the - * title bar is also displayed under the notification bar - */ - protected boolean screenShowTitle = true; - /** - * Splash Screen picture Resource ID. If a Splash Screen is desired, set - * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If - * splashPicID = 0, then no splash screen will be displayed. - */ - protected int splashPicID = 0; - - - protected OGLESContext ctx; - protected GLSurfaceView view = null; - protected boolean isGLThreadPaused = true; - protected ImageView splashImageView = null; - protected FrameLayout frameLayout = null; - final private String ESCAPE_EVENT = "TouchEscape"; - private boolean firstDrawFrame = true; - private boolean inConfigChange = false; - - private class DataObject { - protected LegacyApplication app = null; - } - - @Override - public Object onRetainNonConfigurationInstance() { - logger.log(Level.FINE, "onRetainNonConfigurationInstance"); - final DataObject data = new DataObject(); - data.app = this.app; - inConfigChange = true; - return data; - } - - @Override - @SuppressWarnings("unchecked") - public void onCreate(Bundle savedInstanceState) { - initializeLogHandler(); - - logger.fine("onCreate"); - super.onCreate(savedInstanceState); - - if (screenFullScreen) { - requestWindowFeature(Window.FEATURE_NO_TITLE); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } else { - if (!screenShowTitle) { - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - } - - final DataObject data = (DataObject) getLastNonConfigurationInstance(); - if (data != null) { - logger.log(Level.FINE, "Using Retained App"); - this.app = data.app; - } else { - // Discover the screen resolution - //TODO try to find a better way to get a hand on the resolution - WindowManager wind = this.getWindowManager(); - Display disp = wind.getDefaultDisplay(); - Log.d("AndroidHarness", "Resolution from Window, width:" + disp.getWidth() + ", height: " + disp.getHeight()); - - // Create Settings - logger.log(Level.FINE, "Creating settings"); - AppSettings settings = new AppSettings(true); - settings.setEmulateMouse(mouseEventsEnabled); - settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY); - settings.setUseJoysticks(joystickEventsEnabled); - settings.setEmulateKeyboard(keyEventsEnabled); - - settings.setBitsPerPixel(eglBitsPerPixel); - settings.setAlphaBits(eglAlphaBits); - settings.setDepthBits(eglDepthBits); - settings.setSamples(eglSamples); - settings.setStencilBits(eglStencilBits); - - settings.setResolution(disp.getWidth(), disp.getHeight()); - settings.setAudioRenderer(audioRendererType); - - settings.setFrameRate(frameRate); - - // Create application instance - try { - if (app == null) { - Class clazz = Class.forName(appClass); - app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance(); - } - - app.setSettings(settings); - app.start(); - } catch (Exception ex) { - handleError("Class " + appClass + " init failed", ex); - setContentView(new TextView(this)); - } - } - - ctx = (OGLESContext) app.getContext(); - view = ctx.createView(this); - // store the glSurfaceView in JmeAndroidSystem for future use - JmeAndroidSystem.setView(view); - // AndroidHarness wraps the app as a SystemListener. - ctx.setSystemListener(this); - layoutDisplay(); - } - - @Override - protected void onRestart() { - logger.fine("onRestart"); - super.onRestart(); - if (app != null) { - app.restart(); - } - } - - @Override - protected void onStart() { - logger.fine("onStart"); - super.onStart(); - } - - @Override - protected void onResume() { - logger.fine("onResume"); - super.onResume(); - - gainFocus(); - } - - @Override - protected void onPause() { - logger.fine("onPause"); - loseFocus(); - - super.onPause(); - } - - @Override - protected void onStop() { - logger.fine("onStop"); - super.onStop(); - } - - @Override - protected void onDestroy() { - logger.fine("onDestroy"); - final DataObject data = (DataObject) getLastNonConfigurationInstance(); - if (data != null || inConfigChange) { - logger.fine("In Config Change, not stopping app."); - } else { - if (app != null) { - app.stop(!isGLThreadPaused); - } - } - setContentView(new TextView(this)); - ctx = null; - app = null; - view = null; - JmeAndroidSystem.setView(null); - - super.onDestroy(); - } - - public Application getJmeApplication() { - return app; - } - - /** - * Called when an error has occurred. By default, will show an error message - * to the user and print the exception/error to the log. - */ - @Override - public void handleError(final String errorMsg, final Throwable t) { - String stackTrace = ""; - String title = "Error"; - - if (t != null) { - // Convert exception to string - StringWriter sw = new StringWriter(100); - t.printStackTrace(new PrintWriter(sw)); - stackTrace = sw.toString(); - title = t.toString(); - } - - final String finalTitle = title; - final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") - + "\n" + stackTrace; - - logger.log(Level.SEVERE, finalMsg); - - runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) - .setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create(); - dialog.show(); - } - }); - } - - /** - * Called by the android alert dialog, terminate the activity and OpenGL - * rendering - * - * @param dialog ignored - * @param whichButton the button index - */ - @Override - public void onClick(DialogInterface dialog, int whichButton) { - if (whichButton != -2) { - if (app != null) { - app.stop(true); - } - app = null; - this.finish(); - } - } - - /** - * Gets called by the InputManager on all touch/drag/scale events - */ - @Override - public void onTouch(String name, TouchEvent evt, float tpf) { - if (name.equals(ESCAPE_EVENT)) { - switch (evt.getType()) { - case KEY_UP: - runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) - .setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create(); - dialog.show(); - } - }); - break; - default: - break; - } - } - } - - public void layoutDisplay() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (view == null) { - logger.log(Level.FINE, "view is null!"); - } - if (splashPicID != 0) { - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - Gravity.CENTER); - - frameLayout = new FrameLayout(this); - splashImageView = new ImageView(this); - - Drawable drawable = this.getResources().getDrawable(splashPicID); - if (drawable instanceof NinePatchDrawable) { - splashImageView.setBackgroundDrawable(drawable); - } else { - splashImageView.setImageResource(splashPicID); - } - - if (view.getParent() != null) { - ((ViewGroup) view.getParent()).removeView(view); - } - frameLayout.addView(view); - - if (splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - frameLayout.addView(splashImageView, lp); - - setContentView(frameLayout); - logger.log(Level.FINE, "Splash Screen Created"); - } else { - logger.log(Level.FINE, "Splash Screen Skipped."); - setContentView(view); - } - } - - public void removeSplashScreen() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (splashPicID != 0) { - if (frameLayout != null) { - if (splashImageView != null) { - this.runOnUiThread(new Runnable() { - @Override - public void run() { - splashImageView.setVisibility(View.INVISIBLE); - frameLayout.removeView(splashImageView); - } - }); - } else { - logger.log(Level.FINE, "splashImageView is null"); - } - } else { - logger.log(Level.FINE, "frameLayout is null"); - } - } - } - - /** - * Removes the standard Android log handler due to an issue with not logging - * entries lower than INFO level and adds a handler that produces - * JME formatted log messages. - */ - protected void initializeLogHandler() { - Logger log = LogManager.getLogManager().getLogger(""); - for (Handler handler : log.getHandlers()) { - if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) { - Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName()); - } - log.removeHandler(handler); - } - Handler handler = new AndroidLogHandler(); - log.addHandler(handler); - handler.setLevel(Level.ALL); - } - - @Override - public void initialize() { - app.initialize(); - if (handleExitHook) { - // remove existing mapping from SimpleApplication that stops the app - // when the esc key is pressed (esc key = android back key) so that - // AndroidHarness can produce the exit app dialog box. - if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) { - app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT); - } - - app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK)); - app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT}); - } - } - - @Override - public void reshape(int width, int height) { - app.reshape(width, height); - } - - @Override - public void rescale(float x, float y) { - app.rescale(x, y); - } - - @Override - public void update() { - app.update(); - // call to remove the splash screen, if present. - // call after app.update() to make sure no gap between - // splash screen going away and app display being shown. - if (firstDrawFrame) { - removeSplashScreen(); - firstDrawFrame = false; - } - } - - @Override - public void requestClose(boolean esc) { - app.requestClose(esc); - } - - @Override - public void destroy() { - if (app != null) { - app.destroy(); - } - if (finishOnAppStop) { - finish(); - } - } - - @Override - public void gainFocus() { - logger.fine("gainFocus"); - if (view != null) { - view.onResume(); - } - - if (app != null) { - //resume the audio - AudioRenderer audioRenderer = app.getAudioRenderer(); - if (audioRenderer != null) { - audioRenderer.resumeAll(); - } - //resume the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.resumeSensors(); - } - } - } - } - - isGLThreadPaused = false; - - if (app != null) { - app.gainFocus(); - } - } - - @Override - public void loseFocus() { - logger.fine("loseFocus"); - if (app != null) { - app.loseFocus(); - } - - if (view != null) { - view.onPause(); - } - - if (app != null) { - //pause the audio - AudioRenderer audioRenderer = app.getAudioRenderer(); - if (audioRenderer != null) { - audioRenderer.pauseAll(); - } - //pause the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.pauseSensors(); - } - } - } - } - isGLThreadPaused = true; - } -} diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java index 11de175389..d3ddab8d2f 100644 --- a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java +++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,34 +31,25 @@ */ package com.jme3.app; -import android.app.Activity; import android.app.AlertDialog; -import android.app.Fragment; -import android.content.DialogInterface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; +import android.content.Context; import android.opengl.GLSurfaceView; import android.os.Bundle; import android.util.Log; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; +import androidx.fragment.app.Fragment; import com.jme3.audio.AudioRenderer; import com.jme3.input.JoyInput; -import com.jme3.input.TouchInput; import com.jme3.input.android.AndroidSensorJoyInput; -import com.jme3.input.controls.TouchListener; -import com.jme3.input.controls.TouchTrigger; -import com.jme3.input.event.TouchEvent; -import static com.jme3.input.event.TouchEvent.Type.KEY_UP; import com.jme3.system.AppSettings; import com.jme3.system.SystemListener; import com.jme3.system.android.JmeAndroidSystem; import com.jme3.system.android.OGLESContext; import com.jme3.util.AndroidLogHandler; +import com.jme3.util.AndroidNativeBufferAllocator; +import com.jme3.util.BufferAllocatorFactory; import java.io.PrintWriter; import java.io.StringWriter; import java.util.logging.Handler; @@ -67,313 +58,122 @@ import java.util.logging.Logger; /** + * AndroidX Fragment that hosts a jME application on Android 11+. * - * @author iwgeric + *
The fragment is intentionally small: subclasses create the application and + * optionally customize settings; this class owns the Android view lifecycle and + * forwards render callbacks to {@link LegacyApplication}.
- * Only use ANDROID_ static strings found in AppSettings - * - */ - protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT; - - /** - * If true Android Sensors are used as simulated Joysticks. Users can use the - * Android sensor feedback through the RawInputListener or by registering - * JoyAxisTriggers. - */ - protected boolean joystickEventsEnabled = false; - - /** - * If true KeyEvents are generated from TouchEvents - */ - protected boolean keyEventsEnabled = true; - - /** - * If true MouseEvents are generated from TouchEvents - */ - protected boolean mouseEventsEnabled = true; - - /** - * Flip X axis - */ - protected boolean mouseEventsInvertX = false; - - /** - * Flip Y axis - */ - protected boolean mouseEventsInvertY = false; - - /** - * if true finish this activity when the jme app is stopped - */ + protected GLSurfaceView view; + protected LegacyApplication app; protected boolean finishOnAppStop = true; - /** - * set to false if you don't want the harness to handle the exit hook - */ - protected boolean handleExitHook = true; - - /** - * Title of the exit dialog, default is "Do you want to exit?" - */ - protected String exitDialogTitle = "Do you want to exit?"; - - /** - * Message of the exit dialog, default is "Use your home key to bring this - * app into the background or exit to terminate it." - */ - protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; + @Override + public void onAttach(Context context) { + super.onAttach(context); + } - /** - * Splash Screen picture Resource ID. If a Splash Screen is desired, set - * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If - * splashPicID = 0, then no splash screen will be displayed. - */ - protected int splashPicID = 0; - - protected FrameLayout frameLayout = null; - protected GLSurfaceView view = null; - protected ImageView splashImageView = null; - final private String ESCAPE_EVENT = "TouchEscape"; - private boolean firstDrawFrame = true; - private LegacyApplication app = null; - private int viewWidth = 0; - private int viewHeight = 0; - - // Retrieves the jME application object public Application getJmeApplication() { return app; } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); + public void setFinishOnAppStop(boolean finishOnAppStop) { + this.finishOnAppStop = finishOnAppStop; } - /** - * This Fragment uses setRetainInstance(true) so the onCreate method will only - * be called once. During device configuration changes, the instance of - * this Fragment will be reused in the new Activity. This method should not - * contain any View related objects. They are created and destroyed by - * other methods. View related objects should not be reused, but rather - * created and destroyed along with the Activity. - * - * @param savedInstanceState the saved instance state - */ @Override - @SuppressWarnings("unchecked") public void onCreate(Bundle savedInstanceState) { initializeLogHandler(); logger.fine("onCreate"); super.onCreate(savedInstanceState); - // Create Settings - logger.log(Level.FINE, "Creating settings"); - AppSettings settings = new AppSettings(true); - settings.setEmulateMouse(mouseEventsEnabled); - settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY); - settings.setUseJoysticks(joystickEventsEnabled); - settings.setEmulateKeyboard(keyEventsEnabled); - - settings.setBitsPerPixel(eglBitsPerPixel); - settings.setAlphaBits(eglAlphaBits); - settings.setDepthBits(eglDepthBits); - settings.setSamples(eglSamples); - settings.setStencilBits(eglStencilBits); - settings.setAudioRenderer(audioRendererType); + System.setProperty( + BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION, + AndroidNativeBufferAllocator.class.getName()); - settings.setFrameRate(frameRate); - - // Create application instance try { - if (app == null) { - Class clazz = Class.forName(appClass); - app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance(); - } + app = createApplication(); + AppSettings settings = createSettings(); + configureSettings(settings); app.setSettings(settings); app.start(); - } catch (Exception ex) { - handleError("Class " + appClass + " init failed", ex); + + OGLESContext context = (OGLESContext) app.getContext(); + context.setSystemListener(this); + } catch (Exception exception) { + handleError("jME application initialization failed", exception); } + } - OGLESContext ctx = (OGLESContext) app.getContext(); - // AndroidHarness wraps the app as a SystemListener. - ctx.setSystemListener(this); + /** + * Creates the jME application hosted by this fragment. + * + * @return the application instance + * @throws Exception if the application cannot be created + */ + protected abstract LegacyApplication createApplication() throws Exception; - setRetainInstance(true); + /** + * Creates the default Android settings. Subclasses can override this when + * they need to replace the settings object rather than adjust it. + * + * @return default settings for Android + */ + protected AppSettings createSettings() { + AppSettings settings = new AppSettings(true); + settings.setAudioRenderer(AppSettings.ANDROID_OPENAL_SOFT); + return settings; } /** - * Called by the system to create the View hierarchy associated with this - * Fragment. For jME, this is a FrameLayout that contains the GLSurfaceView - * and an overlaying SplashScreen Image (if used). The View that is returned - * will be placed on the screen within the boundaries of the View borders defined - * by the Activity's layout parameters for this Fragment. For jME, we also - * update the application reference to the new view. + * Customizes the settings before the application starts. * - * @param inflater ignored - * @param container ignored - * @param savedInstanceState ignored - * @return the new view + * @param settings the settings to customize */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - logger.fine("onCreateView"); - // Create the GLSurfaceView for the application - view = ((OGLESContext) app.getContext()).createView(getActivity()); - // store the glSurfaceView in JmeAndroidSystem for future use - JmeAndroidSystem.setView(view); - createLayout(); - view.addOnLayoutChangeListener(this); - return frameLayout; + protected void configureSettings(AppSettings settings) { } @Override - public void onActivityCreated(Bundle savedInstanceState) { - logger.fine("onActivityCreated"); - super.onActivityCreated(savedInstanceState); - } + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + logger.fine("onCreateView"); + if (app == null) { + return new View(requireContext()); + } - @Override - public void onStart() { - logger.fine("onStart"); - super.onStart(); + view = ((OGLESContext) app.getContext()).createView(requireActivity()); + JmeAndroidSystem.setView(view); + return view; } - /** - * When the Fragment resumes (i.e. after app resumes or device screen turned - * back on), call the gainFocus() in the jME application. - */ @Override public void onResume() { logger.fine("onResume"); super.onResume(); - gainFocus(); } - /** - * When the Fragment pauses (i.e. after home button pressed on the device - * or device screen turned off) , call the loseFocus() in the jME application. - */ @Override public void onPause() { logger.fine("onPause"); loseFocus(); - super.onPause(); } - @Override - public void onStop() { - logger.fine("onStop"); - super.onStop(); - } - - /** - * Called by the Android system each time the Activity is destroyed or recreated. - * For jME, we clear references to the GLSurfaceView. - */ @Override public void onDestroyView() { logger.fine("onDestroyView"); - if (splashImageView != null && splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - if (view.getParent() != null) { + if (view != null && view.getParent() instanceof ViewGroup) { ((ViewGroup) view.getParent()).removeView(view); } - if (frameLayout != null && frameLayout.getParent() != null) { - ((ViewGroup) frameLayout.getParent()).removeView(frameLayout); - } - view.removeOnLayoutChangeListener(this); - - splashImageView = null; - frameLayout = null; view = null; JmeAndroidSystem.setView(null); - super.onDestroyView(); } - /** - * Called by the system when the application is being destroyed. In this case, - * the jME application is actually closed as well. This method is not called - * during device configuration changes or when the application is put in the - * background. - */ @Override public void onDestroy() { logger.fine("onDestroy"); @@ -385,186 +185,63 @@ public void onDestroy() { } @Override - public void onDetach() { - logger.fine("onDetach"); - super.onDetach(); - } - - - /** - * Called when an error has occurred. By default, will show an error message - * to the user and print the exception/error to the log. - */ - @Override - public void handleError(final String errorMsg, final Throwable t) { + public void handleError(final String errorMsg, final Throwable throwable) { String stackTrace = ""; String title = "Error"; - if (t != null) { - // Convert exception to string - StringWriter sw = new StringWriter(100); - t.printStackTrace(new PrintWriter(sw)); - stackTrace = sw.toString(); - title = t.toString(); + if (throwable != null) { + StringWriter writer = new StringWriter(100); + throwable.printStackTrace(new PrintWriter(writer)); + stackTrace = writer.toString(); + title = throwable.toString(); } final String finalTitle = title; - final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") + final String finalMessage = (errorMsg != null ? errorMsg : "Uncaught Exception") + "\n" + stackTrace; - logger.log(Level.SEVERE, finalMsg); + logger.log(Level.SEVERE, finalMessage); - getActivity().runOnUiThread(new Runnable() { + requireActivity().runOnUiThread(new Runnable() { @Override public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(finalTitle); - builder.setPositiveButton("Kill", AndroidHarnessFragment.this); - builder.setMessage(finalMsg); - - AlertDialog dialog = builder.create(); + AlertDialog dialog = new AlertDialog.Builder(requireActivity()) + .setTitle(finalTitle) + .setMessage(finalMessage) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, (d, w) -> { + if (app != null) { + app.stop(true); + } + requireActivity().finish(); + }) + .setNegativeButton("Kill", (d, w) -> { + android.os.Process.killProcess(android.os.Process.myPid()); + }) + .create(); dialog.show(); } }); } - /** - * Called by the android alert dialog, terminate the activity and OpenGL - * rendering - * - * @param dialog ignored - * @param whichButton the button index - */ - @Override - public void onClick(DialogInterface dialog, int whichButton) { - if (whichButton != -2) { - if (app != null) { - app.stop(true); - } - app = null; - getActivity().finish(); - } - } - - /** - * Gets called by the InputManager on all touch/drag/scale events - */ - @Override - public void onTouch(String name, TouchEvent evt, float tpf) { - if (name.equals(ESCAPE_EVENT)) { - switch (evt.getType()) { - case KEY_UP: - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(exitDialogTitle); - builder.setPositiveButton("Yes", AndroidHarnessFragment.this); - builder.setNegativeButton("No", AndroidHarnessFragment.this); - builder.setMessage(exitDialogMessage); - - AlertDialog dialog = builder.create(); - dialog.show(); - } - }); - break; - default: - break; - } - } - } - - public void createLayout() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER); - - if (frameLayout != null && frameLayout.getParent() != null) { - ((ViewGroup) frameLayout.getParent()).removeView(frameLayout); - } - frameLayout = new FrameLayout(getActivity()); - - if (view.getParent() != null) { - ((ViewGroup) view.getParent()).removeView(view); - } - frameLayout.addView(view); - - if (splashPicID != 0) { - splashImageView = new ImageView(getActivity()); - - Drawable drawable = getResources().getDrawable(splashPicID); - if (drawable instanceof NinePatchDrawable) { - splashImageView.setBackgroundDrawable(drawable); - } else { - splashImageView.setImageResource(splashPicID); - } - - if (splashImageView.getParent() != null) { - ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); - } - frameLayout.addView(splashImageView, lp); - - logger.fine("Splash Screen Created"); - } else { - logger.fine("Splash Screen Skipped."); - } - } - - public void removeSplashScreen() { - logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); - if (splashPicID != 0) { - if (frameLayout != null) { - if (splashImageView != null) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - splashImageView.setVisibility(View.INVISIBLE); - frameLayout.removeView(splashImageView); - } - }); - } else { - logger.fine("splashImageView is null"); - } - } else { - logger.fine("frameLayout is null"); - } - } - } - - /** - * Removes the standard Android log handler due to an issue with not logging - * entries lower than INFO level and adds a handler that produces - * JME formatted log messages. - */ protected void initializeLogHandler() { - Logger log = LogManager.getLogManager().getLogger(""); - for (Handler handler : log.getHandlers()) { - if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) { + Logger rootLogger = LogManager.getLogManager().getLogger(""); + for (Handler handler : rootLogger.getHandlers()) { + if (rootLogger.getLevel() != null + && rootLogger.getLevel().intValue() <= Level.FINE.intValue()) { Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName()); } - log.removeHandler(handler); + rootLogger.removeHandler(handler); } + Handler handler = new AndroidLogHandler(); - log.addHandler(handler); handler.setLevel(Level.ALL); + rootLogger.addHandler(handler); } @Override public void initialize() { app.initialize(); - if (handleExitHook) { - // remove existing mapping from SimpleApplication that stops the app - // when the esc key is pressed (esc key = android back key) so that - // AndroidHarness can produce the exit app dialog box. - if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) { - app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT); - } - - app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK)); - app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT}); - } } @Override @@ -580,13 +257,6 @@ public void rescale(float x, float y) { @Override public void update() { app.update(); - // call to remove the splash screen, if present. - // call after app.update() to make sure no gap between - // splash screen going away and app display being shown. - if (firstDrawFrame) { - removeSplashScreen(); - firstDrawFrame = false; - } } @Override @@ -600,7 +270,7 @@ public void destroy() { app.destroy(); } if (finishOnAppStop) { - getActivity().finish(); + requireActivity().finish(); } } @@ -612,24 +282,16 @@ public void gainFocus() { } if (app != null) { - //resume the audio AudioRenderer audioRenderer = app.getAudioRenderer(); if (audioRenderer != null) { audioRenderer.resumeAll(); } - //resume the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.resumeSensors(); - } - } + + JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; + if (joyInput instanceof AndroidSensorJoyInput) { + ((AndroidSensorJoyInput) joyInput).resumeSensors(); } - } - if (app != null) { app.gainFocus(); } } @@ -646,71 +308,15 @@ public void loseFocus() { } if (app != null) { - //pause the audio AudioRenderer audioRenderer = app.getAudioRenderer(); if (audioRenderer != null) { audioRenderer.pauseAll(); } - //pause the sensors (aka joysticks) - if (app.getContext() != null) { - JoyInput joyInput = app.getContext().getJoyInput(); - if (joyInput != null) { - if (joyInput instanceof AndroidSensorJoyInput) { - AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; - androidJoyInput.pauseSensors(); - } - } - } - } - } - - @Override - public void onLayoutChange(View v, - int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - - if (v.equals(view)) { -// logger.log(Level.INFO, "surfaceview layout changed. left: {0}, top: {1}, right: {2}, bottom: {3}", -// new Object[]{left, top, right, bottom}); - - if (v.equals(view) && maxResolutionDimension > 0) { - int newWidth = right-left; - int newHeight = bottom-top; - - if (viewWidth != newWidth || viewHeight != newHeight) { - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "SurfaceView layout changed: old width: {0}, old height: {1}, new width: {2}, new height: {3}", - new Object[]{viewWidth, viewHeight, newWidth, newHeight}); - } - viewWidth = newWidth; - viewHeight = newHeight; - - int fixedSizeWidth = viewWidth; - int fixedSizeHeight = viewHeight; - if (viewWidth > viewHeight && viewWidth > maxResolutionDimension) { - // landscape - fixedSizeWidth = maxResolutionDimension; - fixedSizeHeight = (int)(maxResolutionDimension * (viewHeight / (float)viewWidth)); - } else if (viewHeight > viewWidth && viewHeight > maxResolutionDimension) { - // portrait - fixedSizeWidth = (int)(maxResolutionDimension * (viewWidth / (float)viewHeight)); - fixedSizeHeight = maxResolutionDimension; - } else if (viewWidth == viewHeight && viewWidth > maxResolutionDimension) { - fixedSizeWidth = maxResolutionDimension; - fixedSizeHeight = maxResolutionDimension; - } - // set the surfaceview resolution if the size != current view size - if (fixedSizeWidth != viewWidth || fixedSizeHeight != viewHeight) { - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "setting surfaceview resolution to width: {0}, height: {1}", - new Object[]{fixedSizeWidth, fixedSizeHeight}); - } - view.getHolder().setFixedSize(fixedSizeWidth, fixedSizeHeight); - } - } + JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; + if (joyInput instanceof AndroidSensorJoyInput) { + ((AndroidSensorJoyInput) joyInput).pauseSensors(); } } } - } diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java index aff21a20ce..02d9110098 100644 --- a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java +++ b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java @@ -610,6 +610,19 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor GLES30.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + return GLES31.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + /* + * GLES 3.1 exposes shader storage block binding through GLSL layout(binding = N). + * Android's GLES31 Java bindings do not expose glShaderStorageBlockBinding. + */ + } + @Override public void glBindFramebufferEXT(int param1, int param2) { GLES20.glBindFramebuffer(param1, param2); diff --git a/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java b/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java index 8f023c6266..215107c6a9 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java +++ b/jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java @@ -1,6 +1,6 @@ package com.jme3.system.android; -import android.opengl.GLSurfaceView.EGLConfigChooser; +import android.opengl.GLSurfaceView; import com.jme3.renderer.android.RendererUtil; import com.jme3.system.AppSettings; import java.util.logging.Level; @@ -10,486 +10,282 @@ import javax.microedition.khronos.egl.EGLDisplay; /** - * AndroidConfigChooser is used to determine the best suited EGL Config + * AndroidConfigChooser is used to determine the best suited EGL Config. * * @author iwgeric */ -public class AndroidConfigChooser implements EGLConfigChooser { +public final class AndroidConfigChooser implements GLSurfaceView.EGLConfigChooser { private static final Logger logger = Logger.getLogger(AndroidConfigChooser.class.getName()); - protected AppSettings settings; - private final static int EGL_OPENGL_ES2_BIT = 4; - private final static int EGL_OPENGL_ES3_BIT = 0x40; + + private static final int EGL_OPENGL_ES3_BIT = 0x40; + private static final int EGL_WINDOW_BIT = 0x0004; + + private static final int REJECTED = Integer.MIN_VALUE / 2; + + private final AppSettings settings; public AndroidConfigChooser(AppSettings settings) { this.settings = settings; } - /** - * Gets called by the GLSurfaceView class to return the best config - */ @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { - logger.fine("GLSurfaceView asking for egl config"); - Config requestedConfig = getRequestedConfig(); + RequestedConfig requested = getRequestedConfig(); EGLConfig[] configs = getConfigs(egl, display); + EGLConfig chosenConfig = chooseBestConfig(egl, display, configs, requested); - // First try to find an exact match, but allowing a higher stencil - EGLConfig chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true); - if (chosenConfig == null && requestedConfig.d > 16) { - logger.log(Level.INFO, "EGL configuration not found, reducing depth"); - requestedConfig.d = 16; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true); - } - - if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher RGB"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); - } - - if (chosenConfig == null && requestedConfig.a > 0) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); + if (chosenConfig == null && requested.samples > 0) { + logger.log(Level.INFO, "EGL configuration not found with requested samples, disabling MSAA"); + requested = requested.withSamples(0); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.s > 0) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher samples"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true); + if (chosenConfig == null && requested.alpha > 0) { + logger.log(Level.INFO, "EGL configuration not found with requested alpha, allowing opaque config"); + requested = requested.withAlpha(0); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.a > 0) { - logger.log(Level.INFO, "EGL configuration not found, reducing alpha"); - requestedConfig.a = 1; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); + if (chosenConfig == null && requested.depth > 16) { + logger.log(Level.INFO, "EGL configuration not found with requested depth, reducing depth to 16"); + requested = requested.withDepth(16); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } - if (chosenConfig == null && requestedConfig.s > 0) { - logger.log(Level.INFO, "EGL configuration not found, reducing samples"); - requestedConfig.s = 1; - if (requestedConfig.a > 0) { - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true); - } else { - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true); - } - } - - if (chosenConfig == null && requestedConfig.getBitsPerPixel() > 16) { - logger.log(Level.INFO, "EGL configuration not found, setting to RGB565"); - requestedConfig.r = 5; - requestedConfig.g = 6; - requestedConfig.b = 5; - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); - - if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha"); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true); - } + if (chosenConfig == null) { + logger.log(Level.INFO, "EGL configuration not found, using minimal GLES3 window config"); + requested = new RequestedConfig(0, 0, 0, 0, 16, 0, 0, false); + chosenConfig = chooseBestConfig(egl, display, configs, requested); } if (chosenConfig == null) { - logger.log(Level.INFO, "EGL configuration not found, looking for best config with >= 16 bit Depth"); - // failsafe: pick the best config with depth >= 16 - requestedConfig = new Config(0, 0, 0, 0, 16, 0, 0); - chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true); + throw new IllegalStateException("No suitable GLES3 EGLConfig found"); } - if (chosenConfig != null) { - logger.fine("GLSurfaceView asks for egl config, returning: "); - logEGLConfig(chosenConfig, display, egl, Level.FINE); - - storeSelectedConfig(egl, display, chosenConfig); - return chosenConfig; - } else { - logger.severe("No EGL Config found"); - return null; + storeSelectedConfig(egl, display, chosenConfig); + if (logger.isLoggable(Level.INFO)) { + logEGLConfig(chosenConfig, display, egl, Level.INFO); } + return chosenConfig; } - private Config getRequestedConfig() { - int r, g, b; - if (settings.getBitsPerPixel() == 24) { - r = g = b = 8; + private RequestedConfig getRequestedConfig() { + int bitsPerPixel = settings.getBitsPerPixel(); + boolean gamma = settings.isGammaCorrection(); + int red; + int green; + int blue; + + if (gamma || bitsPerPixel >= 24) { + red = 8; + green = 8; + blue = 8; + if (bitsPerPixel < 24) { + settings.setBitsPerPixel(24); + } } else { - if (settings.getBitsPerPixel() != 16) { - logger.log(Level.SEVERE, "Invalid bitsPerPixel setting: {0}, setting to RGB565 (16)", settings.getBitsPerPixel()); + red = 5; + green = 6; + blue = 5; + if (bitsPerPixel != 16) { + logger.log(Level.INFO, "Invalid bitsPerPixel setting: {0}, using RGB565", bitsPerPixel); settings.setBitsPerPixel(16); } - r = 5; - g = 6; - b = 5; - } - - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Requested Display Config:"); - logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}", - new Object[]{settings.getBitsPerPixel(), - settings.getAlphaBits(), settings.getDepthBits(), - settings.getSamples(), settings.getStencilBits()}); } - return new Config( - r, g, b, - settings.getAlphaBits(), - settings.getDepthBits(), - settings.getSamples(), - settings.getStencilBits()); + return new RequestedConfig( + red, + green, + blue, + Math.max(0, settings.getAlphaBits()), + Math.max(0, settings.getDepthBits()), + Math.max(0, settings.getStencilBits()), + Math.max(0, settings.getSamples()), + gamma); } - /** - * Query egl for the available configs - * @param egl - * @param display - * @return - */ private EGLConfig[] getConfigs(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + int[] configSpec = { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL10.EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL10.EGL_NONE + }; + + if (!egl.eglChooseConfig(display, configSpec, null, 0, numConfig)) { + RendererUtil.checkEGLError(egl); + throw new AssertionError("Unable to query GLES3 EGL configs"); + } - int[] num_config = new int[1]; - int[] configSpec = new int[]{ - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - EGL10.EGL_NONE}; - boolean gles3=true; - - // Try openGL ES 3 - try { - if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) { - RendererUtil.checkEGLError(egl); - gles3=false; - } - } catch (com.jme3.renderer.RendererException re) { - // it's just the device not supporting GLES3. Fallback to GLES2 - gles3=false; - } - - if(!gles3) - { - // Get back to openGL ES 2 - configSpec[1]=EGL_OPENGL_ES2_BIT; - if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) { - RendererUtil.checkEGLError(egl); - throw new AssertionError(); - } + int numConfigs = numConfig[0]; + if (numConfigs == 0) { + throw new IllegalStateException("No GLES3 window EGL configs found"); } - int numConfigs = num_config[0]; EGLConfig[] configs = new EGLConfig[numConfigs]; - if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) { + if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, numConfig)) { RendererUtil.checkEGLError(egl); - throw new AssertionError(); + throw new AssertionError("Unable to enumerate GLES3 EGL configs"); } - logger.fine("--------------Display Configurations---------------"); - for (EGLConfig eGLConfig : configs) { - logEGLConfig(eGLConfig, display, egl, Level.FINE); - logger.fine("----------------------------------------"); + if (logger.isLoggable(Level.FINE)) { + logger.fine("--------------Display Configurations---------------"); + for (EGLConfig config : configs) { + logEGLConfig(config, display, egl, Level.FINE); + logger.fine("----------------------------------------"); + } } - return configs; } - private EGLConfig chooseConfig( - EGL10 egl, EGLDisplay display, EGLConfig[] configs, Config requestedConfig, - boolean higherRGB, boolean higherAlpha, - boolean higherSamples, boolean higherStencil) { + private EGLConfig chooseBestConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs, RequestedConfig requested) { + EGLConfig bestConfig = null; + int bestScore = REJECTED; - EGLConfig keptConfig = null; - int kr = 0; - int kg = 0; - int kb = 0; - int ka = 0; - int kd = 0; - int ks = 0; - int kst = 0; - - - // first pass through config list. Try to find an exact match. for (EGLConfig config : configs) { - int r = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_RED_SIZE); - int g = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_GREEN_SIZE); - int b = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_BLUE_SIZE); - int a = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_ALPHA_SIZE); - int d = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_DEPTH_SIZE); - int s = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_SAMPLES); - int st = eglGetConfigAttribSafe(egl, display, config, - EGL10.EGL_STENCIL_SIZE); + int red = getAttrib(egl, display, config, EGL10.EGL_RED_SIZE); + int green = getAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE); + int blue = getAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE); + int alpha = getAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE); + int depth = getAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE); + int stencil = getAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE); + int sampleBuffers = getAttrib(egl, display, config, EGL10.EGL_SAMPLE_BUFFERS); + int samples = getAttrib(egl, display, config, EGL10.EGL_SAMPLES); + + int score = scoreConfig(requested, red, green, blue, alpha, depth, + stencil, sampleBuffers, samples); if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}", - new Object[]{r, g, b, a, d, s, st}); + logger.log(Level.FINE, + "Checking EGLConfig R{0} G{1} B{2} A{3} D{4} S{5} MSAA[{6},{7}] score {8}", + new Object[]{red, green, blue, alpha, depth, stencil, sampleBuffers, samples, score}); } - if (higherRGB && r < requestedConfig.r) { continue; } - if (!higherRGB && r != requestedConfig.r) { continue; } - - if (higherRGB && g < requestedConfig.g) { continue; } - if (!higherRGB && g != requestedConfig.g) { continue; } - - if (higherRGB && b < requestedConfig.b) { continue; } - if (!higherRGB && b != requestedConfig.b) { continue; } - - if (higherAlpha && a < requestedConfig.a) { continue; } - if (!higherAlpha && a != requestedConfig.a) { continue; } - - if (d < requestedConfig.d) { continue; } // always allow higher depth + if (score > bestScore) { + bestScore = score; + bestConfig = config; + } + } - if (higherSamples && s < requestedConfig.s) { continue; } - if (!higherSamples && s != requestedConfig.s) { continue; } + return bestScore == REJECTED ? null : bestConfig; + } - if (higherStencil && st < requestedConfig.st) { continue; } - if (!higherStencil && !inRange(st, 0, requestedConfig.st)) { continue; } + private int scoreConfig(RequestedConfig requested, int red, int green, int blue, + int alpha, int depth, int stencil, int sampleBuffers, int samples) { + if (requested.gamma && (red < 8 || green < 8 || blue < 8)) { + return REJECTED; + } + if (red < requested.red || green < requested.green || blue < requested.blue) { + return REJECTED; + } + if (alpha < requested.alpha || depth < requested.depth || stencil < requested.stencil) { + return REJECTED; + } + if (requested.samples > 0 && (sampleBuffers == 0 || samples < requested.samples)) { + return REJECTED; + } - //we keep the config if it is better - if ( r >= kr || g >= kg || b >= kb || a >= ka || - d >= kd || s >= ks || st >= kst ) { - kr = r; kg = g; kb = b; ka = a; - kd = d; ks = s; kst = st; - keptConfig = config; - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}", - new Object[]{r, g, b, a, d, s, st}); - } - } + int score = 0; + score += closenessScore(red, requested.red, 8); + score += closenessScore(green, requested.green, 8); + score += closenessScore(blue, requested.blue, 8); + score += requested.alpha > 0 ? closenessScore(alpha, requested.alpha, 8) : opaqueBonus(alpha); + score += closenessScore(depth, requested.depth, 32); + score += closenessScore(stencil, requested.stencil, 8); + if (requested.samples > 0) { + score += 100 + closenessScore(samples, requested.samples, 16); + } else { + score += samples == 0 ? 10 : -samples; } - if (keptConfig != null) { - return keptConfig; + return score; + } + + private int closenessScore(int actual, int requested, int maxUseful) { + if (requested <= 0) { + return 0; } + return maxUseful - Math.min(maxUseful, actual - requested); + } - //no match found - logger.log(Level.SEVERE, "No egl config match found"); - return null; + private int opaqueBonus(int alpha) { + return alpha == 0 ? 10 : -Math.min(alpha, 8); } - private static int eglGetConfigAttribSafe(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) { + private int getAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) { int[] value = new int[1]; if (!egl.eglGetConfigAttrib(display, config, attribute, value)) { RendererUtil.checkEGLError(egl); - throw new AssertionError(); + throw new AssertionError("Unable to query EGL attribute " + attribute); } return value[0]; } private void storeSelectedConfig(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { - int r = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_RED_SIZE); - int g = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE); - int b = eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE); - settings.setBitsPerPixel(r+g+b); - - settings.setAlphaBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE)); - settings.setDepthBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE)); - settings.setSamples( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_SAMPLES)); - settings.setStencilBits( - eglGetConfigAttribSafe(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE)); + int red = getAttrib(egl, display, eglConfig, EGL10.EGL_RED_SIZE); + int green = getAttrib(egl, display, eglConfig, EGL10.EGL_GREEN_SIZE); + int blue = getAttrib(egl, display, eglConfig, EGL10.EGL_BLUE_SIZE); + int samples = getAttrib(egl, display, eglConfig, EGL10.EGL_SAMPLE_BUFFERS) > 0 + ? getAttrib(egl, display, eglConfig, EGL10.EGL_SAMPLES) + : 0; + + settings.setBitsPerPixel(red + green + blue); + settings.setAlphaBits(getAttrib(egl, display, eglConfig, EGL10.EGL_ALPHA_SIZE)); + settings.setDepthBits(getAttrib(egl, display, eglConfig, EGL10.EGL_DEPTH_SIZE)); + settings.setStencilBits(getAttrib(egl, display, eglConfig, EGL10.EGL_STENCIL_SIZE)); + settings.setSamples(samples); } - /** - * log output with egl config details - * - * @param conf - * @param display - * @param egl - */ - private void logEGLConfig(EGLConfig conf, EGLDisplay display, EGL10 egl, Level level) { - - logger.log(level, "EGL_RED_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RED_SIZE)); - - logger.log(level, "EGL_GREEN_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_GREEN_SIZE)); - - logger.log(level, "EGL_BLUE_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_BLUE_SIZE)); - - logger.log(level, "EGL_ALPHA_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_ALPHA_SIZE)); - - logger.log(level, "EGL_DEPTH_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_DEPTH_SIZE)); - - logger.log(level, "EGL_STENCIL_SIZE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_STENCIL_SIZE)); - - logger.log(level, "EGL_RENDERABLE_TYPE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_RENDERABLE_TYPE)); - - logger.log(level, "EGL_SURFACE_TYPE = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SURFACE_TYPE)); - - logger.log(level, "EGL_SAMPLE_BUFFERS = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLE_BUFFERS)); - - logger.log(level, "EGL_SAMPLES = {0}", - eglGetConfigAttribSafe(egl, display, conf, EGL10.EGL_SAMPLES)); + private void logEGLConfig(EGLConfig config, EGLDisplay display, EGL10 egl, Level level) { + logger.log(level, + "EGLConfig chosen: R{0} G{1} B{2} A{3} D{4} S{5} MSAA[{6} buffers, {7} samples]", + new Object[]{ + getAttrib(egl, display, config, EGL10.EGL_RED_SIZE), + getAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE), + getAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE), + getAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE), + getAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE), + getAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE), + getAttrib(egl, display, config, EGL10.EGL_SAMPLE_BUFFERS), + getAttrib(egl, display, config, EGL10.EGL_SAMPLES) + }); } - private boolean inRange(int val, int min, int max) { - return min <= val && val <= max; - } + private static final class RequestedConfig { + private final int red; + private final int green; + private final int blue; + private final int alpha; + private final int depth; + private final int stencil; + private final int samples; + private final boolean gamma; + + private RequestedConfig(int red, int green, int blue, int alpha, + int depth, int stencil, int samples, boolean gamma) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + this.depth = depth; + this.stencil = stencil; + this.samples = samples; + this.gamma = gamma; + } - private class Config { - /** - * red, green, blue, alpha, depth, samples, stencil - */ - int r, g, b, a, d, s, st; - - private Config(int r, int g, int b, int a, int d, int s, int st) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - this.d = d; - this.s = s; - this.st = st; + private RequestedConfig withSamples(int samples) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); } - private int getBitsPerPixel() { - return r+g+b; + private RequestedConfig withAlpha(int alpha) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); } - } -//DON'T REMOVE THIS, USED FOR UNIT TESTING FAILING CONFIGURATION LISTS. -// private static class Config { -// -// int r, g, b, a, d, s, ms, ns; -// -// public Config(int r, int g, int b, int a, int d, int s, int ms, int ns) { -// this.r = r; -// this.g = g; -// this.b = b; -// this.a = a; -// this.d = d; -// this.s = s; -// this.ms = ms; -// this.ns = ns; -// } -// -// @Override -// public String toString() { -// return "Config{" + "r=" + r + ", g=" + g + ", b=" + b + ", a=" + a + ", d=" + d + ", s=" + s + ", ms=" + ms + ", ns=" + ns + '}'; -// } -// } -// -// public static Config chooseConfig(List configs, ConfigType configType, int mSamples) { -// -// Config keptConfig = null; -// int kd = 0; -// int knbMs = 0; -// -// -// // first pass through config list. Try to find an exact match. -// for (Config config : configs) { -//// logEGLConfig(config, display, egl); -// int r = config.r; -// int g = config.g; -// int b = config.b; -// int a = config.a; -// int d = config.d; -// int s = config.s; -// int isMs = config.ms; -// int nbMs = config.ns; -// -// if (inRange(r, configType.mr, configType.r) -// && inRange(g, configType.mg, configType.g) -// && inRange(b, configType.mb, configType.b) -// && inRange(a, configType.ma, configType.a) -// && inRange(d, configType.md, configType.d) -// && inRange(s, configType.ms, configType.s)) { -// if (mSamples == 0 && isMs != 0) { -// continue; -// } -// boolean keep = false; -// //we keep the config if the depth is better or if the AA setting is better -// if (d >= kd) { -// kd = d; -// keep = true; -// } else { -// keep = false; -// } -// -// if (mSamples != 0) { -// if (nbMs >= knbMs && nbMs <= mSamples) { -// knbMs = nbMs; -// keep = true; -// } else { -// keep = false; -// } -// } -// -// if (keep) { -// keptConfig = config; -// } -// } -// } -// -// if (keptConfig != null) { -// return keptConfig; -// } -// -// if (configType == ConfigType.BEST) { -// keptConfig = chooseConfig(configs, ConfigType.BEST_TRANSLUCENT, mSamples); -// -// if (keptConfig != null) { -// return keptConfig; -// } -// } -// -// if (configType == ConfigType.BEST_TRANSLUCENT) { -// keptConfig = chooseConfig(configs, ConfigType.FASTEST, mSamples); -// -// if (keptConfig != null) { -// return keptConfig; -// } -// } -// // failsafe. pick the 1st config. -// -// for (Config config : configs) { -// if (config.d >= 16) { -// return config; -// } -// } -// -// return null; -// } -// -// private static boolean inRange(int val, int min, int max) { -// return min <= val && val <= max; -// } -// -// public static void main(String... argv) { -// List confs = new ArrayList(); -// confs.add(new Config(5, 6, 5, 0, 0, 0, 0, 0)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 0, 0)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 0, 0)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 0, 0)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 0, 0)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 0, 0)); -// -// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 2)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 2)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 2)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 2)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 2)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 2)); -// -// confs.add(new Config(5, 6, 5, 0, 0, 0, 1, 4)); -// confs.add(new Config(5, 6, 5, 0, 16, 0, 1, 4)); -// confs.add(new Config(5, 6, 5, 0, 24, 8, 1, 4)); -// confs.add(new Config(8, 8, 8, 8, 0, 0, 1, 4)); -//// confs.add(new Config(8, 8, 8, 8, 16, 0, 1, 4)); -//// confs.add(new Config(8, 8, 8, 8, 24, 8, 1, 4)); -// -// Config chosen = chooseConfig(confs, ConfigType.BEST, 0); -// -// System.err.println(chosen); -// -// } + private RequestedConfig withDepth(int depth) { + return new RequestedConfig(red, green, blue, alpha, depth, stencil, samples, gamma); + } + } } diff --git a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java index 4acb4aa66f..a553afa25a 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java +++ b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java @@ -48,6 +48,8 @@ import android.view.ViewGroup.LayoutParams; import android.widget.EditText; import android.widget.FrameLayout; +import com.jme3.app.Application; +import com.jme3.asset.AssetManager; import com.jme3.input.*; import com.jme3.input.android.AndroidInputHandler; import com.jme3.input.android.AndroidInputHandler14; @@ -55,19 +57,33 @@ import com.jme3.input.android.AndroidInputHandler26; import com.jme3.input.controls.SoftTextDialogInputListener; import com.jme3.input.dummy.DummyKeyInput; +import com.jme3.material.Material; +import com.jme3.renderer.Caps; +import com.jme3.renderer.RenderManager; import com.jme3.renderer.android.AndroidGL; import com.jme3.renderer.opengl.*; import com.jme3.system.*; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.FrameBuffer.FrameBufferTarget; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; +import com.jme3.ui.Picture; import com.jme3.util.BufferAllocatorFactory; import com.jme3.util.PrimitiveAllocator; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.opengles.GL10; public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTextDialogInput { + private static final String BLIT_MATERIAL = "Common/MatDefs/Blit/Blit.j3md"; private static final Logger logger = Logger.getLogger(OGLESContext.class.getName()); private static final String SAFER_BUFFER_ALLOCATOR_CLASS = "com.jme3.util.SaferBufferAllocator"; protected final AtomicBoolean created = new AtomicBoolean(false); @@ -82,6 +98,13 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex protected AndroidInputHandler androidInput; protected long minFrameDuration = 0; // No FPS cap protected long lastUpdateTime = 0; + private Application application; + private Material blitMaterial; + private Picture blitGeometry; + private FrameBuffer linearFrameBuffer; + private Texture2D linearFrameBufferColorTexture; + private boolean linearFrameBufferDirty; + private boolean multisampleTextureWarningIssued; static { final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION; @@ -124,16 +147,8 @@ public Type getType() { public GLSurfaceView createView(Context context) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo info = am.getDeviceConfigurationInfo(); - // NOTE: We assume all ICS devices have OpenGL ES 2.0. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - // below 4.0, check OpenGL ES 2.0 support. - if (info.reqGlEsVersion < 0x20000) { - throw new UnsupportedOperationException( - "OpenGL ES 2.0 or better is not supported on this device" - ); - } - } else if (Build.VERSION.SDK_INT < 9) { - throw new UnsupportedOperationException("jME3 requires Android 2.3 or later"); + if (info.reqGlEsVersion < 0x30000) { + throw new UnsupportedOperationException("OpenGL ES 3.0 or better is not supported on this device"); } // Start to set up the view @@ -153,10 +168,8 @@ public GLSurfaceView createView(Context context) { androidInput.setView(view); androidInput.loadSettings(settings); - // setEGLContextClientVersion must be set before calling setRenderer - // this means it cannot be set in AndroidConfigChooser (too late) - // use proper openGL ES version - view.setEGLContextClientVersion(info.reqGlEsVersion >> 16); + // setEGLContextClientVersion must be set before calling setRenderer. + view.setEGLContextClientVersion(3); view.setFocusableInTouchMode(true); view.setFocusable(true); @@ -165,12 +178,7 @@ public GLSurfaceView createView(Context context) { //view.setClickable(true); // setFormat must be set before AndroidConfigChooser is called by the surfaceview. - // if setFormat is called after ConfigChooser is called, then execution - // stops at the setFormat call without a crash. - // We look at the user setting for alpha bits and set the surfaceview - // PixelFormat to either Opaque, Transparent, or Translucent. - // ConfigChooser will do its best to honor the alpha requested by the user - // For best rendering performance, use Opaque (alpha bits = 0). + // For best rendering performance and sRGB support, prefer Opaque (alpha bits = 0). int curAlphaBits = settings.getAlphaBits(); logger.log(Level.FINE, "curAlphaBits: {0}", curAlphaBits); if (curAlphaBits >= 8) { @@ -187,6 +195,7 @@ public GLSurfaceView createView(Context context) { AndroidConfigChooser configChooser = new AndroidConfigChooser(settings); view.setEGLConfigChooser(configChooser); + view.setEGLContextFactory(new Gles3ContextFactory()); view.setRenderer(this); // Attempt to preserve the EGL Context on app pause/resume. @@ -200,11 +209,63 @@ public GLSurfaceView createView(Context context) { return view; } + private static final class Gles3ContextFactory implements GLSurfaceView.EGLContextFactory { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final int EGL_CONTEXT_PRIORITY_LEVEL_IMG = 0x3100; + private static final int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101; + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + EGLContext context = createGles3Context(egl, display, config, true); + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + context = createGles3Context(egl, display, config, false); + } + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + throw new IllegalStateException("Unable to create an OpenGL ES 3 context"); + } + return context; + } + + private EGLContext createGles3Context(EGL10 egl, EGLDisplay display, + EGLConfig config, boolean preferHighPriority) { + boolean usePriority = preferHighPriority && hasExtension(egl, display, "EGL_IMG_context_priority"); + int[] attributes = usePriority + ? new int[]{ + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL10.EGL_NONE + } + : new int[]{ + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL10.EGL_NONE + }; + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attributes); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + } + + private static boolean hasExtension(EGL10 egl, EGLDisplay display, String extension) { + String extensions = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (extensions == null) { + return false; + } + // EGL extension list is space separated. Ensure we only match full + // extension names to avoid false positives when one name is a + // substring of another. + String padded = " " + extensions + " "; + return padded.contains(" " + extension + " "); + } + // renderer:initialize @Override public void onSurfaceCreated(GL10 gl, EGLConfig cfg) { if (created.get() && renderer != null) { renderer.resetGLObjects(); + destroyLinearFrameBuffer(); } else { if (!created.get()) { logger.fine("GL Surface created, initializing JME3 renderer"); @@ -253,6 +314,13 @@ public void uncaughtException(Thread thread, Throwable thrown) { renderer = new GLRenderer(gl, (GLExt) gl, (GLFbo) gl); renderer.initialize(); + boolean blitSrgbConversion = useBlitSrgbConversion(); + renderer.setMainFrameBufferSrgb(false); + renderer.setLinearizeSrgbImages(settings.isGammaCorrection()); + logger.log(Level.INFO, + "Android gamma correction: requested={0}, main framebuffer sRGB=false, blit sRGB={1}", + new Object[]{settings.isGammaCorrection(), blitSrgbConversion}); + JmeSystem.setSoftTextDialogInput(this); needClose.set(false); @@ -264,6 +332,7 @@ public void uncaughtException(Thread thread, Throwable thrown) { protected void deinitInThread() { if (renderable.get()) { created.set(false); + destroyLinearFrameBufferResources(); if (renderer != null) { renderer.cleanup(); } @@ -318,6 +387,9 @@ public SystemListener getSystemListener() { @Override public void setSystemListener(SystemListener listener) { this.listener = listener; + if (listener instanceof Application) { + application = (Application) listener; + } } @Override @@ -410,7 +482,9 @@ public void onDrawFrame(GL10 gl) { throw new IllegalStateException("onDrawFrame without create"); } - listener.update(); + if (!renderFrameWithBlitSrgbConversion()) { + listener.update(); + } if (autoFlush) { renderer.postFrame(); } @@ -469,6 +543,150 @@ protected void waitFor(boolean createdVal) { } } + private boolean useBlitSrgbConversion() { + return settings.isGammaCorrection() && application != null; + } + + private int getLinearFrameBufferSampleCount() { + int samples = Math.max(settings.getSamples(), 1); + if (samples > 1 && renderer != null && !renderer.getCaps().contains(Caps.TextureMultisample)) { + if (!multisampleTextureWarningIssued) { + logger.warning("sRGB blit conversion requires multisampled textures for MSAA. " + + "Falling back to a single-sample linear framebuffer."); + multisampleTextureWarningIssued = true; + } + return 1; + } + return samples; + } + + private void rebuildLinearFrameBufferIfNeeded() { + if (!useBlitSrgbConversion()) { + destroyLinearFrameBufferResources(); + return; + } + + int width = Math.max(settings.getWidth(), 1); + int height = Math.max(settings.getHeight(), 1); + int samples = getLinearFrameBufferSampleCount(); + + if (linearFrameBuffer != null && linearFrameBuffer.getWidth() == width + && linearFrameBuffer.getHeight() == height && linearFrameBuffer.getSamples() == samples) { + return; + } + + destroyLinearFrameBuffer(); + + FrameBuffer frameBuffer = new FrameBuffer(width, height, samples); + frameBuffer.setName("Android Linear Blit FrameBuffer"); + frameBuffer.setSrgb(false); + + Texture2D colorTexture = new Texture2D( + new Image(getLinearFrameBufferColorFormat(), width, height, null, ColorSpace.Linear)); + if (samples > 1) { + colorTexture.getImage().setMultiSamples(samples); + } + frameBuffer.addColorTarget(FrameBufferTarget.newTarget(colorTexture)); + + if (settings.getDepthBits() > 0 || settings.getStencilBits() > 0) { + frameBuffer.setDepthTarget(FrameBufferTarget + .newTarget(renderer.getBestDepthTargetFormat(false, false, settings.getStencilBits() > 0))); + } + + linearFrameBufferColorTexture = colorTexture; + linearFrameBuffer = frameBuffer; + linearFrameBufferDirty = true; + } + + private Format getLinearFrameBufferColorFormat() { + if (renderer != null && renderer.getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + logger.warning("RGBA16F color framebuffer is not supported. " + + "Falling back to RGBA8 for Android sRGB blit conversion."); + return Format.RGBA8; + } + + private boolean ensureBlitResources() { + if (!useBlitSrgbConversion()) { + return false; + } + + AssetManager assetManager = application.getAssetManager(); + RenderManager renderManager = application.getRenderManager(); + if (assetManager == null || renderManager == null) { + return false; + } + + if (blitMaterial == null) { + blitMaterial = new Material(assetManager, BLIT_MATERIAL); + blitMaterial.setBoolean("Srgb", true); + blitMaterial.getAdditionalRenderState().setDepthTest(false); + blitMaterial.getAdditionalRenderState().setDepthWrite(false); + } + + if (blitGeometry == null) { + blitGeometry = new Picture("Linear to sRGB Blit"); + blitGeometry.setWidth(1f); + blitGeometry.setHeight(1f); + blitGeometry.setMaterial(blitMaterial); + } + + if (linearFrameBufferDirty && linearFrameBufferColorTexture != null) { + blitMaterial.setTexture("Texture", linearFrameBufferColorTexture); + if (linearFrameBuffer != null && linearFrameBuffer.getSamples() > 1) { + blitMaterial.setInt("NumSamples", linearFrameBuffer.getSamples()); + } else { + blitMaterial.clearParam("NumSamples"); + } + linearFrameBufferDirty = false; + } + + return true; + } + + private void destroyLinearFrameBuffer() { + if (linearFrameBuffer != null) { + linearFrameBuffer.dispose(); + linearFrameBuffer = null; + } + if (linearFrameBufferColorTexture != null && linearFrameBufferColorTexture.getImage() != null) { + linearFrameBufferColorTexture.getImage().dispose(); + } + linearFrameBufferColorTexture = null; + linearFrameBufferDirty = true; + } + + private void destroyLinearFrameBufferResources() { + destroyLinearFrameBuffer(); + blitMaterial = null; + blitGeometry = null; + multisampleTextureWarningIssued = false; + } + + private boolean renderFrameWithBlitSrgbConversion() { + if (!useBlitSrgbConversion()) { + return false; + } + + rebuildLinearFrameBufferIfNeeded(); + if (linearFrameBuffer == null || !ensureBlitResources()) { + return false; + } + + renderer.setMainFrameBufferOverride(linearFrameBuffer); + try { + listener.update(); + } finally { + renderer.setMainFrameBufferOverride(null); + } + + renderer.setFrameBuffer(null); + blitGeometry.updateGeometricState(); + application.getRenderManager().renderGeometry(blitGeometry); + return true; + } + @Override public void requestDialog( final int id, diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java index 2f8ee78d9b..47b259a2dd 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java @@ -66,7 +66,7 @@ public class EnvironmentCamera extends BaseAppState { protected static Vector3f[] axisY = new Vector3f[6]; protected static Vector3f[] axisZ = new Vector3f[6]; - protected Image.Format imageFormat = Image.Format.RGB16F; + protected Image.Format imageFormat = null; public TextureCubeMap debugEnv; @@ -174,23 +174,24 @@ public Void call() throws Exception { public void render(final RenderManager renderManager) { if (isBusy()) { final SnapshotJob job = jobs.get(0); + final Image.Format format = getImageFormat(renderManager.getRenderer()); for (int i = 0; i < 6; i++) { viewports[i].clearScenes(); viewports[i].attachScene(job.scene); renderManager.renderViewPort(viewports[i], 0.16f); buffers[i] = BufferUtils.createByteBuffer( - size * size * imageFormat.getBitsPerPixel() / 8); + size * size * format.getBitsPerPixel() / 8); renderManager.getRenderer().readFrameBufferWithFormat( - framebuffers[i], buffers[i], imageFormat); - images[i] = new Image(imageFormat, size, size, buffers[i], + framebuffers[i], buffers[i], format); + images[i] = new Image(format, size, size, buffers[i], ColorSpace.Linear); MipMapGenerator.generateMipMaps(images[i]); } final TextureCubeMap map = EnvMapUtils.makeCubeMap(images[0], images[1], images[2], images[3], images[4], images[5], - imageFormat); + format); debugEnv = map; job.callback.done(map); map.getImage().dispose(); @@ -292,6 +293,8 @@ public boolean isBusy() { @Override protected void initialize(Application app) { this.backGroundColor = app.getViewPort().getBackgroundColor().clone(); + final Renderer renderer = app.getRenderManager().getRenderer(); + final Image.Format colorFormat = getImageFormat(renderer); final Camera[] cameras = new Camera[6]; final Texture2D[] textures = new Texture2D[6]; @@ -305,7 +308,7 @@ protected void initialize(Application app) { cameras[i] = createOffCamera(size, position, axisX[i], axisY[i], axisZ[i]); viewports[i] = createOffViewPort("EnvView" + i, cameras[i]); framebuffers[i] = createOffScreenFrameBuffer(size, viewports[i]); - textures[i] = new Texture2D(size, size, imageFormat); + textures[i] = new Texture2D(size, size, colorFormat); framebuffers[i].setColorTexture(textures[i]); } } @@ -325,13 +328,27 @@ protected void cleanup(Application app) { } } + protected Image.Format getImageFormat(Renderer renderer) { + if (this.imageFormat == null) { + this.imageFormat = renderer.getBestColorTargetFormat(true, false, false); + } + return this.imageFormat; + } + + protected Image.Format getDepthFormat(Renderer renderer) { + return renderer.getBestDepthTargetFormat(false, false, false); + } + /** * returns the images format used for the generated maps. * * @return the enum value */ public Image.Format getImageFormat() { - return imageFormat; + if (this.imageFormat == null && getApplication() != null) { + return getImageFormat(getApplication().getRenderManager().getRenderer()); + } + return this.imageFormat; } @Override @@ -384,9 +401,22 @@ protected ViewPort createOffViewPort(final String name, final Camera offCamera) * @return a new instance */ protected FrameBuffer createOffScreenFrameBuffer(int mapSize, ViewPort offView) { + Image.Format depthFormat = getDepthFormat(getApplication().getRenderManager().getRenderer()); + return createOffScreenFrameBuffer(mapSize, offView, depthFormat); + } + + /** + * create an off-screen framebuffer. + * + * @param mapSize the desired size (pixels per side) + * @param offView the off-screen viewport to be used (alias created) + * @param depthFormat the depth format to use + * @return a new instance + */ + protected FrameBuffer createOffScreenFrameBuffer(int mapSize, ViewPort offView, Image.Format depthFormat) { // create offscreen framebuffer final FrameBuffer offBuffer = new FrameBuffer(mapSize, mapSize, 1); - offBuffer.setDepthBuffer(Image.Format.Depth); + offBuffer.setDepthBuffer(depthFormat); offView.setOutputFrameBuffer(offBuffer); return offBuffer; } diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java index 6f07fd1c1c..82d45f32de 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -287,9 +287,8 @@ public void setAssetManager(AssetManager assetManager) { } void rebakeNow(RenderManager renderManager) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, - Format.Depth, - envMapSize, envMapSize); + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, null, + null, envMapSize, envMapSize); baker.setTexturePulling(isRequiredSavableResults()); baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter); diff --git a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java index 12ba5e99c1..8705265572 100644 --- a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java @@ -75,7 +75,7 @@ public class FastLightProbeFactory { * @return The baked LightProbe */ public static LightProbe makeProbe(RenderManager rm, AssetManager am, int size, Vector3f pos, float frustumNear, float frustumFar, Spatial scene) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, null, null, size, size); baker.setTexturePulling(true); diff --git a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java index 7becab82e8..f035f95267 100644 --- a/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java @@ -36,6 +36,7 @@ import com.jme3.environment.generation.*; import com.jme3.environment.util.EnvMapUtils; import com.jme3.light.LightProbe; +import com.jme3.renderer.Renderer; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.texture.TextureCubeMap; @@ -123,7 +124,8 @@ public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener listener) { final LightProbe probe = new LightProbe(); probe.setPosition(envCam.getPosition()); - probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat())); + Renderer renderer = envCam.getApplication().getRenderManager().getRenderer(); + probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat(renderer))); envCam.snapshot(scene, new JobProgressAdapter() { @Override @@ -168,7 +170,8 @@ public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCa probe.getPrefilteredEnvMap().getImage().dispose(); } - probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat())); + Renderer renderer = envCam.getApplication().getRenderManager().getRenderer(); + probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat(renderer))); envCam.snapshot(scene, new JobProgressAdapter() { diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java index 4f15dc71e0..0e1bd5a5b2 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java @@ -98,6 +98,7 @@ public abstract class GenericEnvBaker implements EnvBaker { protected TextureCubeMap envMap; protected Format depthFormat; + protected Format colorFormat; protected final RenderManager renderManager; protected final AssetManager assetManager; @@ -107,19 +108,34 @@ public abstract class GenericEnvBaker implements EnvBaker { protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) { this.depthFormat = depthFormat; + this.colorFormat = colorFormat; renderManager = rm; assetManager = am; cam = new Camera(128, 128); - envMap = new TextureCubeMap(env_size, env_size, colorFormat); + envMap = new TextureCubeMap(env_size, env_size, getColorFormat()); envMap.setMagFilter(MagFilter.Bilinear); envMap.setMinFilter(MinFilter.BilinearNoMipMaps); envMap.setWrap(WrapMode.EdgeClamp); envMap.getImage().setColorSpace(ColorSpace.Linear); } + protected Format getColorFormat() { + if (colorFormat == null) { + this.colorFormat = renderManager.getRenderer().getBestColorTargetFormat(true, false, false); + } + return colorFormat; + } + + protected Format getDepthFormat() { + if (depthFormat == null) { + this.depthFormat = renderManager.getRenderer().getBestDepthTargetFormat(false, false, false); + } + return depthFormat; + } + @Override public void setTexturePulling(boolean v) { texturePulling = v; @@ -170,7 +186,7 @@ public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, FrameBuffer envbakers[] = new FrameBuffer[6]; for (int i = 0; i < 6; i++) { envbakers[i] = new FrameBuffer(envMap.getImage().getWidth(), envMap.getImage().getHeight(), 1); - envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(depthFormat)); + envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(getDepthFormat())); envbakers[i].setSrgb(false); envbakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i])); } diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java index 022f4c349f..606a894699 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java @@ -85,7 +85,7 @@ public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBak public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) { super(rm, am, format, depthFormat, env_size); - specular = new TextureCubeMap(specular_size, specular_size, format); + specular = new TextureCubeMap(specular_size, specular_size, getColorFormat()); specular.setWrap(WrapMode.EdgeClamp); specular.setMagFilter(MagFilter.Bilinear); specular.setMinFilter(MinFilter.Trilinear); diff --git a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java index aec38731aa..25115963c8 100644 --- a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java +++ b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java @@ -207,16 +207,7 @@ public void initialize(RenderManager rm, ViewPort vp) { // Determine optimal framebuffer format based on renderer capabilities if (fbFormat == null) { - fbFormat = Format.RGB111110F; - if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) { - if (renderer.getCaps().contains(Caps.FloatColorBufferRGB)) { - fbFormat = Format.RGB16F; - } else if (renderer.getCaps().contains(Caps.FloatColorBufferRGBA)) { - fbFormat = Format.RGBA16F; - } else { - fbFormat = Format.RGB8; - } - } + fbFormat = renderer.getBestColorTargetFormat(true, false, false); } Camera cam = vp.getCamera(); diff --git a/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java b/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java index 857e78dbc3..fb205bad7b 100644 --- a/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java +++ b/jme3-core/src/main/java/com/jme3/post/HDRRenderer.java @@ -106,9 +106,9 @@ public HDRRenderer(AssetManager manager, Renderer renderer) { Collection caps = renderer.getCaps(); if (caps.contains(Caps.PackedFloatColorBuffer)) bufFormat = Format.RGB111110F; - else if (caps.contains(Caps.FloatColorBufferRGB)) + else if (caps.contains(Caps.HalfFloatColorBufferRGB)) bufFormat = Format.RGB16F; - else if (caps.contains(Caps.FloatColorBufferRGBA)) + else if (caps.contains(Caps.HalfFloatColorBufferRGBA)) bufFormat = Format.RGBA16F; else { enabled = false; diff --git a/jme3-core/src/main/java/com/jme3/renderer/Caps.java b/jme3-core/src/main/java/com/jme3/renderer/Caps.java index 647d44a9a6..5b111c811b 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Caps.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Caps.java @@ -223,28 +223,77 @@ public enum Caps { TextureBuffer, /** - * Supports floating point and half textures (Format.RGB16F). + * Supports 32-bit floating point textures. */ FloatTexture, /** - * Supports rendering on RGB floating point textures + * Supports 16-bit floating point textures. + */ + HalfFloatTexture, + + /** + * Supports linear filtering of floating point textures. + */ + FloatTextureFilter, + + /** + * Supports linear filtering of half floating point textures. + */ + HalfFloatTextureFilter, + + /** + * Supports rendering on RGB 32-bit floating point textures. */ FloatColorBufferRGB, /** - * Supports rendering on RGBA floating point textures + * Supports rendering on R 32-bit floating point textures. + */ + FloatColorBufferR, + + /** + * Supports rendering on RG 32-bit floating point textures. + */ + FloatColorBufferRG, + + /** + * Supports rendering on RGBA 32-bit floating point textures. */ FloatColorBufferRGBA, + /** + * Supports rendering on R 16-bit floating point textures. + */ + HalfFloatColorBufferR, + + /** + * Supports rendering on RG 16-bit floating point textures. + */ + HalfFloatColorBufferRG, + + /** + * Supports rendering on RGB 16-bit floating point textures. + */ + HalfFloatColorBufferRGB, + + /** + * Supports rendering on RGBA 16-bit floating point textures. + */ + HalfFloatColorBufferRGBA, + /** * Supports integer textures. */ IntegerTexture, /** - * Supports floating point FBO color buffers (Format.RGB16F). + * Supports full floating point FBO color buffers. + * + * @deprecated use the format-specific FloatColorBuffer* and + * HalfFloatColorBuffer* caps. */ + @Deprecated FloatColorBuffer, @@ -253,6 +302,11 @@ public enum Caps { */ FloatDepthBuffer, + /** + * Supports FBO with Depth32 image format. + */ + Depth32, + /** * Supports Format.RGB111110F for textures. */ @@ -311,6 +365,12 @@ public enum Caps { */ Srgb, + /** + * Supports enabling and disabling linear-to-sRGB conversion for framebuffer + * writes using GL_FRAMEBUFFER_SRGB. + */ + SrgbWriteControl, + /** * Supports blitting framebuffers. */ @@ -491,15 +551,34 @@ public static boolean supports(Collection caps, Texture tex) { return caps.contains(Caps.PackedDepthStencilBuffer); case Depth32F: return caps.contains(Caps.FloatDepthBuffer); + case Depth32: + return caps.contains(Caps.Depth32); + case Depth24: + return caps.contains(Caps.Depth24); case RGB16F_to_RGB111110F: + return caps.contains(Caps.HalfFloatTexture) && caps.contains(Caps.PackedFloatTexture); case RGB111110F: return caps.contains(Caps.PackedFloatTexture); case RGB16F_to_RGB9E5: + return caps.contains(Caps.HalfFloatTexture) && caps.contains(Caps.SharedExponentTexture); case RGB9E5: return caps.contains(Caps.SharedExponentTexture); + case Luminance16F: + case Luminance16FAlpha16F: + case RGB16F: + case RGBA16F: + case R16F: + case RG16F: + return caps.contains(Caps.HalfFloatTexture); + case Luminance32F: + case RGB32F: + case RGBA32F: + case R32F: + case RG32F: + return caps.contains(Caps.FloatTexture); default: if (fmt.isFloatingPont()) { - return caps.contains(Caps.FloatTexture); + return caps.contains(Caps.FloatTexture) || caps.contains(Caps.HalfFloatTexture); } return true; @@ -519,13 +598,32 @@ private static boolean supportsColorBuffer(Collection caps, RenderBuffer c switch (colorFmt) { case RGB111110F: return caps.contains(Caps.PackedFloatColorBuffer); + case Luminance16F: + case R16F: + return caps.contains(Caps.HalfFloatColorBufferR); + case Luminance32F: + case R32F: + return caps.contains(Caps.FloatColorBufferR); + case Luminance16FAlpha16F: + case RG16F: + return caps.contains(Caps.HalfFloatColorBufferRG); + case RG32F: + return caps.contains(Caps.FloatColorBufferRG); + case RGB16F: + return caps.contains(Caps.HalfFloatColorBufferRGB); + case RGB32F: + return caps.contains(Caps.FloatColorBufferRGB); + case RGBA16F: + return caps.contains(Caps.HalfFloatColorBufferRGBA); + case RGBA32F: + return caps.contains(Caps.FloatColorBufferRGBA); case RGB16F_to_RGB111110F: case RGB16F_to_RGB9E5: case RGB9E5: return false; default: if (colorFmt.isFloatingPont()) { - return caps.contains(Caps.FloatColorBuffer); + return false; } return true; @@ -565,6 +663,16 @@ public static boolean supports(Collection caps, FrameBuffer fb) { && !caps.contains(Caps.PackedDepthStencilBuffer)) { return false; } + + if (depthFmt == Format.Depth32 + && !caps.contains(Caps.Depth32)) { + return false; + } + + if (depthFmt == Format.Depth24 + && !caps.contains(Caps.Depth24)) { + return false; + } } } for (int i = 0; i < fb.getNumColorBuffers(); i++) { diff --git a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java index 00f2f8ac57..05f64fb339 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java @@ -42,6 +42,7 @@ import com.jme3.system.AppSettings; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; import com.jme3.texture.Texture; import com.jme3.texture.TextureImage; import com.jme3.util.NativeObject; @@ -422,13 +423,15 @@ public void setTexture(int unit, Texture tex) * As a shorthand, the user can set {@link AppSettings#setGammaCorrection(boolean)} to true * to toggle both {@link Renderer#setLinearizeSrgbImages(boolean)} and * {@link Renderer#setMainFrameBufferSrgb(boolean)} if the - * {@link Caps#Srgb} is supported by the GPU. + * {@link Caps#Srgb} and {@link Caps#SrgbWriteControl} capabilities are + * supported by the GPU. * * @param srgb true for sRGB colorspace, false for linear colorspace * @throws RendererException If the GPU hardware does not support sRGB. * * @see FrameBuffer#setSrgb(boolean) * @see Caps#Srgb + * @see Caps#SrgbWriteControl */ public void setMainFrameBufferSrgb(boolean srgb); @@ -560,4 +563,70 @@ public default void pushDebugGroup(String name) { * Registers a NativeObject to be cleaned up by this renderer. */ public void registerNativeObject(NativeObject nativeObject); + + public default Format getBestColorTargetFormat(boolean floatingPoint) { + return getBestColorTargetFormat(floatingPoint, true, false); + } + + public default Format getBestColorTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withAlpha) { + if (!floatingPoint) { + return Format.RGBA8; + } + + if (!highPrecision) { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } + } + + if (withAlpha) { + if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } else { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGB)) { + return Format.RGB16F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } + + return Format.RGBA8; + } + + public default Format getBestDepthTargetFormat() { + return getBestDepthTargetFormat(false, false, false); + } + + public default Format getBestDepthTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withStencil) { + if (withStencil) { + if (getCaps().contains(Caps.PackedDepthStencilBuffer)) { + return Format.Depth24Stencil8; + } + } else { + if (floatingPoint && getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + if (highPrecision) { + if (getCaps().contains(Caps.Depth32)) { + return Format.Depth32; + } + if (getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + } + if (getCaps().contains(Caps.Depth24)) { + return Format.Depth24; + } + } + + return Format.Depth; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java index 10c5c7ff88..2db094c437 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java @@ -293,6 +293,29 @@ public default void glUniformBlockBinding(int program, int uniformBlockIndex, in throw new UnsupportedOperationException("Uniform buffer objects are not supported"); } + /** + * Retrieves the index of a named program resource. + * + * @param program the name of a program object + * @param programInterface the program interface containing the resource + * @param name the name of the resource + * @return the resource index + */ + public default int glGetProgramResourceIndex(int program, int programInterface, String name) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + + /** + * Assigns a shader storage block to a binding point. + * + * @param program the name of a program object + * @param storageBlockIndex the index of the shader storage block within {@code program} + * @param storageBlockBinding the binding point to assign + */ + public default void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + public default void glPushDebugGroup(int source, int id, String message) { } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java index 8e65fbdef1..1d974fa9a4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java @@ -42,6 +42,9 @@ public final class GLImageFormat { public final int format; public final int dataType; public final boolean compressed; + public final boolean colorRenderable; + public final boolean depthRenderable; + public final boolean filterable; public final boolean swizzleRequired; /** @@ -51,11 +54,14 @@ public final class GLImageFormat { * @param format OpenGL format * @param dataType OpenGL datatype */ - public GLImageFormat(int internalFormat, int format, int dataType) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = false; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -66,12 +72,18 @@ public GLImageFormat(int internalFormat, int format, int dataType) { * @param format OpenGL format * @param dataType OpenGL datatype * @param compressed Format is compressed + * @param colorRenderable Format can be used as a color render target + * @param depthRenderable Format can be used as a depth render target + * @param filterable Format can be filtered */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -84,11 +96,47 @@ public GLImageFormat(int internalFormat, int format, int dataType, boolean compr * @param compressed Format is compressed * @param swizzleRequired Need to use texture swizzle to upload texture */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = swizzleRequired; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + GLImageFormat other = (GLImageFormat) obj; + return internalFormat == other.internalFormat + && format == other.format + && dataType == other.dataType + && compressed == other.compressed + && colorRenderable == other.colorRenderable + && depthRenderable == other.depthRenderable + && filterable == other.filterable + && swizzleRequired == other.swizzleRequired; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + this.internalFormat; + hash = 97 * hash + this.format; + hash = 97 * hash + this.dataType; + hash = 97 * hash + (this.compressed ? 1 : 0); + hash = 97 * hash + (this.colorRenderable ? 1 : 0); + hash = 97 * hash + (this.depthRenderable ? 1 : 0); + hash = 97 * hash + (this.filterable ? 1 : 0); + hash = 97 * hash + (this.swizzleRequired ? 1 : 0); + return hash; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java index c73943c54e..d7d2dd58c4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java @@ -44,50 +44,66 @@ public final class GLImageFormats { private GLImageFormats() { } - + private static void format(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, colorRenderable, depthRenderable, filterable); } - + private static void formatSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable + ) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, false, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgbSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatComp(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } - + private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } /** @@ -103,231 +119,277 @@ private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format fo public static GLImageFormat[][] getFormatsForCaps(EnumSet caps) { GLImageFormat[][] formatToGL = new GLImageFormat[2][Image.Format.values().length]; + boolean opengles = caps.contains(Caps.OpenGLES20); + boolean opengles3 = opengles && caps.contains(Caps.OpenGLES30); + boolean opengles2Only = opengles && !opengles3; + boolean webgl = caps.contains(Caps.WebGL); + boolean opengl = !opengles; + boolean coreProfile = caps.contains(Caps.CoreProfile); + boolean colorRenderableHalfFloatR = caps.contains(Caps.HalfFloatColorBufferR); + boolean colorRenderableHalfFloatRG = caps.contains(Caps.HalfFloatColorBufferRG); + boolean colorRenderableHalfFloatRGB = caps.contains(Caps.HalfFloatColorBufferRGB); + boolean colorRenderableHalfFloatRGBA = caps.contains(Caps.HalfFloatColorBufferRGBA); + boolean colorRenderableFloatR = caps.contains(Caps.FloatColorBufferR); + boolean colorRenderableFloatRG = caps.contains(Caps.FloatColorBufferRG); + boolean colorRenderableFloatRGB = caps.contains(Caps.FloatColorBufferRGB); + boolean colorRenderableFloatRGBA = caps.contains(Caps.FloatColorBufferRGBA); + boolean colorRenderablePackedFloat = caps.contains(Caps.PackedFloatColorBuffer); + boolean filterableHalfFloat = caps.contains(Caps.HalfFloatTextureFilter); + boolean filterableFloat = caps.contains(Caps.FloatTextureFilter); + int halfFloatFormat = GLExt.GL_HALF_FLOAT_ARB; - if (caps.contains(Caps.OpenGLES20)) { + if (opengles2Only) { halfFloatFormat = GLExt.GL_HALF_FLOAT_OES; } // Core Profile Formats (supported by both OpenGL Core 3.3 and OpenGL ES 3.0+) - if (caps.contains(Caps.CoreProfile)) { - formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat); - formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT); - formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); + if (coreProfile) { + formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); - formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); + formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); } - if (caps.contains(Caps.OpenGL20)||caps.contains(Caps.OpenGLES30)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.OpenGL20)||opengles3) { + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (opengles3 || webgl) { + format(formatToGL, Format.RGB565, GLES_30.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); + } else { + format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl || opengles3 || webgl, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - // Additional desktop-specific formats: - format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); + // Additional desktop-specific formats. + if (opengl) { + format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + } // sRGB formats if (caps.contains(Caps.Srgb)) { - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - if (!caps.contains(Caps.CoreProfile)) { - formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl, false, true); + formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, opengl, false, true); + + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (!coreProfile) { + formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + if (opengl) { + formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); } - formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); if (caps.contains(Caps.TextureCompressionS3TC)) { - formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } } } else if (caps.contains(Caps.Rgba8)) { // A more limited form of 32-bit RGBA. Only GL_RGBA8 is available. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); - formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (opengl) { + formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } } else { // Actually, the internal format isn't used for OpenGL ES 2! This is the same as the above. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); } - if (caps.contains(Caps.OpenGLES20)) { - format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); + if (opengles) { + format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); } - format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); + format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, true, false, true); - if (caps.contains(Caps.FloatTexture)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat); - format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT); - format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat); + if (caps.contains(Caps.HalfFloatTexture) || caps.contains(Caps.FloatTexture)) { + if (!coreProfile) { + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat, false, false, filterableHalfFloat); + format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat, false, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT, false, false, filterableFloat); + } + } + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT, colorRenderableFloatRG, false, filterableFloat); + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); } - format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat); - format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT); - format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); - format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT); - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); } if (caps.contains(Caps.PackedFloatTexture)) { - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } if (caps.contains(Caps.SharedExponentTexture)) { - format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT, false, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } // Supported in GLES30 core if (caps.contains(Caps.OpenGLES30)) { - format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV); - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV, true, false, true); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_ALPHA, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + } - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.Srgb)) { + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } - //Depending on the device could be better to use the previously defined extension based float textures instead of gles3.0 texture formats -// if (!caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); -// } - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); + // Depending on the device, the extension-based definitions above may have already defined these. + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); + } + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, true); } // Need to check whether Caps.DepthTexture is supported before using it for textures. // But for render buffers it's OK. - format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); if (caps.contains(Caps.WebGL)) { // NOTE: fallback to 24-bit depth as workaround for firefox bug in WebGL 2 where DEPTH_COMPONENT16 is not handled properly - format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } else if (caps.contains(Caps.OpenGLES20)) { // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth. - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); } else { - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE, false, true, false); + } + if (caps.contains(Caps.Depth24)) { + format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } - if (caps.contains(Caps.OpenGLES30) || caps.contains(Caps.OpenGL20) || caps.contains(Caps.Depth24)) { - format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.Depth32)) { + format(formatToGL, Format.Depth32, GL2.GL_DEPTH_COMPONENT32, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } if (caps.contains(Caps.FloatDepthBuffer)) { - format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT); + format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, false, true, false); } if (caps.contains(Caps.PackedDepthStencilBuffer)) { - format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT); + format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT, false, true, false); } // Compressed formats if (caps.contains(Caps.TextureCompressionS3TC)) { - formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)){ - formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE); - formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE); + if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)) { + formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE, false, false, true); + formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE, false, false, true); } if (caps.contains(Caps.TextureCompressionETC2)) { - formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); if (caps.contains(Caps.Srgb)) { - formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } } else if (caps.contains(Caps.TextureCompressionETC1)) { - formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { - formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); - formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { + formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); } // Integer formats - if(caps.contains(Caps.IntegerTexture)) { - format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT); - format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.IntegerTexture)) { + format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT); - format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE, false, false, false); + format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE, false, false, false); + format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT, false, false, false); + format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT, false, false, false); + format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT, false, false, false); + format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT, false, false, false); - format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); } return formatToGL; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 59927fc3aa..b7d11f9ba6 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -62,6 +62,7 @@ import com.jme3.texture.Texture.ShadowCompareMode; import com.jme3.texture.Texture.WrapAxis; import com.jme3.texture.TextureImage; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.LastTextureState; import com.jme3.util.BufferUtils; import com.jme3.util.ListMap; @@ -410,29 +411,35 @@ private void loadCapabilitiesCommon() { // == texture format extensions == - boolean hasFloatTexture; + boolean coreFloatTextures = caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) + || caps.contains(Caps.WebGL); + boolean arbFloatTextures = hasExtension("GL_ARB_texture_float"); + boolean hasFloatTexture = coreFloatTextures || arbFloatTextures || hasExtension("GL_OES_texture_float"); + boolean hasHalfFloatTexture = coreFloatTextures || hasExtension("GL_OES_texture_half_float") + || (arbFloatTextures && hasExtension("GL_ARB_half_float_pixel")); - hasFloatTexture = hasExtension("GL_OES_texture_half_float") && - hasExtension("GL_OES_texture_float"); + if (hasFloatTexture) { + caps.add(Caps.FloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = hasExtension("GL_ARB_texture_float") && - hasExtension("GL_ARB_half_float_pixel"); + if (hasHalfFloatTexture) { + caps.add(Caps.HalfFloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL); - } + if (hasFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_float_linear"))) { + caps.add(Caps.FloatTextureFilter); } - if (hasFloatTexture) { - caps.add(Caps.FloatTexture); + if (hasHalfFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_half_float_linear"))) { + caps.add(Caps.HalfFloatTextureFilter); } // integer texture format extensions - if(hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) - || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) + if (hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { caps.add(Caps.IntegerTexture); + } if (hasExtension("GL_OES_depth_texture") || hasExtension("WEBGL_depth_texture") || gl2 != null || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { @@ -444,6 +451,10 @@ private void loadCapabilitiesCommon() { caps.add(Caps.Depth24); } + if (caps.contains(Caps.OpenGL20) || hasExtension("GL_OES_depth32")) { + caps.add(Caps.Depth32); + } + if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) || hasExtension("GL_OES_rgb8_rgba8") || hasExtension("GL_ARM_rgba8") || @@ -452,20 +463,32 @@ private void loadCapabilitiesCommon() { } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) - || hasExtension("GL_OES_packed_depth_stencil")) { + || hasAnyExtension("GL_OES_packed_depth_stencil", "GL_EXT_packed_depth_stencil")) { caps.add(Caps.PackedDepthStencilBuffer); } - if (hasExtension("GL_ARB_color_buffer_float") && - hasExtension("GL_ARB_half_float_pixel") - ||caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL)) { - // XXX: Require both 16- and 32-bit float support for FloatColorBuffer. + boolean hasDesktopFloatColorBuffer = (hasExtension("GL_ARB_color_buffer_float") + && hasExtension("GL_ARB_texture_float") + && hasExtension("GL_ARB_half_float_pixel")) + || caps.contains(Caps.OpenGL30); + boolean hasExtFloatColorBuffer = hasExtension("GL_EXT_color_buffer_float"); + boolean hasExtHalfFloatColorBuffer = hasExtension("GL_EXT_color_buffer_half_float"); + + if (hasDesktopFloatColorBuffer || hasExtFloatColorBuffer) { caps.add(Caps.FloatColorBuffer); + caps.add(Caps.FloatColorBufferR); + caps.add(Caps.FloatColorBufferRG); caps.add(Caps.FloatColorBufferRGBA); - if (!caps.contains(Caps.OpenGLES30) && !caps.contains(Caps.WebGL)) { - caps.add(Caps.FloatColorBufferRGB); - } + caps.add(Caps.HalfFloatColorBufferR); + caps.add(Caps.HalfFloatColorBufferRG); + caps.add(Caps.HalfFloatColorBufferRGBA); + } else if (hasExtHalfFloatColorBuffer && hasHalfFloatTexture) { + caps.add(Caps.HalfFloatColorBufferRGBA); + } + + if (hasDesktopFloatColorBuffer) { + caps.add(Caps.FloatColorBufferRGB); + caps.add(Caps.HalfFloatColorBufferRGB); } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) @@ -473,15 +496,15 @@ private void loadCapabilitiesCommon() { caps.add(Caps.FloatDepthBuffer); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) + if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) + || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { - // Either GL3/GLES3 is available or both packed_float & half_float_pixel. caps.add(Caps.PackedFloatTexture); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30)) { + if ((hasExtension("GL_EXT_packed_float") && hasDesktopFloatColorBuffer) + || caps.contains(Caps.OpenGL30) || hasExtFloatColorBuffer) { caps.add(Caps.PackedFloatColorBuffer); } @@ -551,7 +574,12 @@ private void loadCapabilitiesCommon() { if (hasExtension("GL_EXT_texture_filter_anisotropic")) { caps.add(Caps.TextureFilterAnisotropic); - limits.put(Limits.TextureAnisotropy, getInteger(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT)); + floatBuf16.clear(); + gl.glGetFloat(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, floatBuf16); + limits.put(Limits.TextureAnisotropy, + Math.max(1, Math.round(floatBuf16.get(0)))); + } else { + limits.put(Limits.TextureAnisotropy, 1); } if (hasExtension("GL_EXT_framebuffer_object") @@ -619,6 +647,11 @@ private void loadCapabilitiesCommon() { || caps.contains(Caps.WebGL)) { caps.add(Caps.Srgb); } + if (hasExtension("GL_ARB_framebuffer_sRGB") + || caps.contains(Caps.OpenGL30) + || hasExtension("GL_EXT_sRGB_write_control")) { + caps.add(Caps.SrgbWriteControl); + } // Supports seamless cubemap if (hasExtension("GL_ARB_seamless_cube_map") || caps.contains(Caps.OpenGL32)) { @@ -647,7 +680,8 @@ private void loadCapabilitiesCommon() { caps.add(Caps.TesselationShader); } - if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) || caps.contains(Caps.OpenGLES31)) { + if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) + || caps.contains(Caps.OpenGLES31)) { caps.add(Caps.ShaderStorageBufferObject); limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE)); @@ -778,6 +812,29 @@ private void bindUniformBlock(int program, int uniformBlockIndex, int uniformBlo } } + private int getProgramResourceIndex(int program, int programInterface, String name) { + if (gl4 != null) { + return gl4.glGetProgramResourceIndex(program, programInterface, name); + } + return glext.glGetProgramResourceIndex(program, programInterface, name); + } + + private void bindShaderStorageBufferBase(int bindingPoint, int buffer) { + if (gl4 != null) { + gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } else { + glext.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } + } + + private void bindShaderStorageBlock(int program, int storageBlockIndex, int storageBlockBinding) { + if (gl4 != null) { + gl4.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } else { + glext.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + } + @SuppressWarnings("fallthrough") @Override public void initialize() { @@ -1583,11 +1640,11 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl if (bufferBlock.isUpdateNeeded() ) { int blockIndex = bufferBlock.getLocation(); if (blockIndex < 0) { - blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); + blockIndex = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); bufferBlock.setLocation(blockIndex); } if (bufferBlock.getLocation() != NativeObject.INVALID_ID) { - gl4.glShaderStorageBlockBinding(shaderId, bufferBlock.getLocation(), bindingPoint); + bindShaderStorageBlock(shaderId, bufferBlock.getLocation(), bindingPoint); } } break; @@ -2078,7 +2135,7 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { // Check NPOT requirements checkNonPowerOfTwo(tex); - updateTexImageData(image, tex.getType(), 0, false); + updateTexImageData(image, tex.getType(), 0, false, false); // NOTE: For depth textures, sets nearest/no-mips mode // Required to fix "framebuffer unsupported" @@ -2102,6 +2159,19 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { } public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { + Image.Format format = rb.getFormat(); + boolean depthTarget = rb.getSlot() == FrameBuffer.SLOT_DEPTH + || rb.getSlot() == FrameBuffer.SLOT_DEPTH_STENCIL; + boolean srgb = !depthTarget && fb.isSrgb(); + GLImageFormat glFormat = texUtil.getImageFormatWithError(format, srgb); + if (!depthTarget && !glFormat.colorRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not color-renderable and cannot be used as a color attachment."); + } else if (depthTarget && !glFormat.depthRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not depth-renderable and cannot be used as a depth attachment."); + } + boolean needAttach; if (rb.getTexture() == null) { // if it hasn't been created yet, then attach is required. @@ -2144,14 +2214,14 @@ private void toggleFramebufferSrgb(FrameBuffer fb) { boolean isSrgb = fb == null ? mainFrameBufferSrgb : fb.isSrgb(); if (isSrgb != context.srgbWriteEnabled) { - if (caps.contains(Caps.Srgb)) { + if (caps.contains(Caps.SrgbWriteControl) && caps.contains(Caps.Srgb)) { if (isSrgb) { gl.glEnable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } else { gl.glDisable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } - context.srgbWriteEnabled = isSrgb; } + context.srgbWriteEnabled = isSrgb; } } @@ -2299,11 +2369,17 @@ public void setFrameBuffer(FrameBuffer fb) { } // generate mipmaps for last FB if needed - if (context.boundFB != null && (context.boundFB.getMipMapsGenerationHint()!=null?context.boundFB.getMipMapsGenerationHint():generateMipmapsForFramebuffers)) { - for (int i = 0; i < context.boundFB.getNumColorBuffers(); i++) { - RenderBuffer rb = context.boundFB.getColorBuffer(i); + FrameBuffer boundFB = context.boundFB; + if (boundFB != null && (boundFB.getMipMapsGenerationHint() != null + ? boundFB.getMipMapsGenerationHint() + : generateMipmapsForFramebuffers)) { + for (int i = 0; i < boundFB.getNumColorBuffers(); i++) { + RenderBuffer rb = boundFB.getColorBuffer(i); Texture tex = rb.getTexture(); - if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + if (tex != null && tex.getMinFilter().usesMipMapLevels() + && isMipmapGenerationSupported(tex.getImage().getFormat(), + linearizeSrgbImages && boundFB.isSrgb() + ? ColorSpace.sRGB : ColorSpace.Linear)) { try { final int textureUnitIndex = 0; setTexture(textureUnitIndex, rb.getTexture()); @@ -2316,6 +2392,9 @@ public void setFrameBuffer(FrameBuffer fb) { int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); glfbo.glGenerateMipmapEXT(textureType); } + } else if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + logger.warning("Cannot generate mipmaps for framebuffer texture: " + tex + + " with image format: " + tex.getImage().getFormat()); } } } @@ -2519,7 +2598,11 @@ private void setupTextureParams(int unit, Texture tex) { boolean haveMips = true; if (image != null) { - haveMips = image.isGeneratedMipmapsRequired() || image.hasMipmaps(); + haveMips = image.hasMipmaps() + || image.isMipmapsGenerated() + || (image.isGeneratedMipmapsRequired() + && isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)); } LastTextureState curState = image.getLastTextureState(); @@ -2529,10 +2612,12 @@ private void setupTextureParams(int unit, Texture tex) { gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, convertMagFilter(tex.getMagFilter())); curState.magFilter = tex.getMagFilter(); } - if (curState.minFilter != tex.getMinFilter()) { + if (curState.minFilter != tex.getMinFilter() + || curState.minFilterMipmapsAvailable != haveMips) { bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, convertMinFilter(tex.getMinFilter(), haveMips)); curState.minFilter = tex.getMinFilter(); + curState.minFilterMipmapsAvailable = haveMips; } int desiredAnisoFilter = tex.getAnisotropicFilter() == 0 @@ -2703,7 +2788,13 @@ private void bindTextureOnly(int target, Image img, int unit) { * before being uploaded. */ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot) { + updateTexImageData(img, type, unit, scaleToPot, true); + } + + private void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot, + boolean allowCpuMipmapFallback) { int texId = img.getId(); + boolean textureWasUnuploaded = texId == -1; if (texId == -1) { // create texture gl.glGenTextures(intBuf1); @@ -2719,29 +2810,79 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s bindTextureAndUnit(target, img, unit); int imageSamples = img.getMultiSamples(); + boolean sourceMipmapsUsable = img.hasMipmaps() && !scaleToPot; + boolean needsMipmaps = !sourceMipmapsUsable && img.isGeneratedMipmapsRequired(); + boolean hwMipmapSupported = needsMipmaps && isMipmapGenerationSupported(img.getFormat(), + linearizeSrgbImages ? img.getColorSpace() : ColorSpace.Linear); + Image imageForUpload = img; + boolean cpuMipmapsGenerated = false; if (imageSamples <= 1) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) { - // Image does not have mipmaps, but they are required. - // Generate from base level. + boolean cpuMipmapFallbackFailed = false; + if (needsMipmaps) { + /* + * Some formats cannot use glGenerateMipmap because they are not both + * renderable and filterable. On a first upload, fall back to a CPU-built + * mip chain when the image data is suitable. If NPOT scaling is also + * required, build the CPU mips from the resized upload image. + */ + boolean needsCpuMipmapFallback = !hwMipmapSupported + && allowCpuMipmapFallback + && textureWasUnuploaded + && MipMapGenerator.canGenerateMipmaps(img); + if (needsCpuMipmapFallback) { + try { + Image cpuMipmapUploadImage = cloneImageForUpload(img, scaleToPot); + if (cpuMipmapUploadImage != null) { + MipMapGenerator.generateMipMaps(cpuMipmapUploadImage, linearizeSrgbImages, + img.getColorSpace() == ColorSpace.sRGB); + imageForUpload = cpuMipmapUploadImage; + cpuMipmapsGenerated = true; + scaleToPot = false; + img.setMipmapsGenerated(true); + } + } catch (RuntimeException exception) { + cpuMipmapFallbackFailed = true; + logger.log(Level.WARNING, + "Texture " + img + " requires mipmaps, but hardware mipmap generation is not supported" + + " and CPU mipmap generation failed. Mipmaps will not be generated.", + exception); + } + } - if (!caps.contains(Caps.FrameBuffer) && gl2 != null) { + /* + * Old desktop GL without FBO support can auto-generate mipmaps during + * texture upload. Newer paths generate explicitly after upload below. + */ + if (hwMipmapSupported && !caps.contains(Caps.FrameBuffer) && gl2 != null) { gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE); img.setMipmapsGenerated(true); - } else { - // For OpenGL3 and up. - // We'll generate mipmaps via glGenerateMipmapEXT (see below) } - } else if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30)) { - if (img.hasMipmaps()) { - // Image already has mipmaps, set the max level based on the - // number of mipmaps we have. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1); - } else { - // Image does not have mipmaps, and they are not required. - // Specify that the texture has no mipmaps. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0); + + if (!hwMipmapSupported + && !sourceMipmapsUsable + && !cpuMipmapsGenerated + && !cpuMipmapFallbackFailed) { + logger.log(Level.WARNING, "Texture " + img + " requires mipmaps, but hardware mipmaps generation is not supported. Mipmaps will not be generated."); } } + + /* + * Clamp the mip range to the levels actually uploaded. This is still + * needed when mipmaps are not requested, otherwise GL may sample + * missing levels left from a previous texture state. When hardware + * mipmap generation is pending, reopen the full generated range in + * case an earlier upload clamped this texture to the base level. + */ + boolean canSetTextureMaxLevel = caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30); + boolean hasUploadMipmaps = sourceMipmapsUsable || cpuMipmapsGenerated; + int uploadWidth = scaleToPot ? FastMath.nearestPowerOfTwo(img.getWidth()) : imageForUpload.getWidth(); + int uploadHeight = scaleToPot ? FastMath.nearestPowerOfTwo(img.getHeight()) : imageForUpload.getHeight(); + int maxLevel = textureMaxLevelForUpload(canSetTextureMaxLevel, needsMipmaps, hwMipmapSupported, + hasUploadMipmaps, cpuMipmapsGenerated ? imageForUpload.getMipMapSizes() : img.getMipMapSizes(), + generatedMipMaxLevel(uploadWidth, uploadHeight, imageForUpload.getDepth())); + if (maxLevel >= 0) { + gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, maxLevel); + } } else { // Check if graphics card doesn't support multisample textures if (!caps.contains(Caps.TextureMultisample)) { @@ -2782,11 +2923,8 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s } } - Image imageForUpload; if (scaleToPot) { imageForUpload = MipMapGenerator.resizeToPowerOf2(img); - } else { - imageForUpload = img; } if (target == GL.GL_TEXTURE_CUBE_MAP) { List data = imageForUpload.getData(); @@ -2821,16 +2959,80 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s img.setMultiSamples(imageSamples); } - if (caps.contains(Caps.FrameBuffer) || gl2 == null) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired() && img.getData(0) != null) { - glfbo.glGenerateMipmapEXT(target); - img.setMipmapsGenerated(true); - } + if (needsMipmaps && hwMipmapSupported + && (caps.contains(Caps.FrameBuffer) || gl2 == null) + && img.getData(0) != null + && !img.isMipmapsGenerated()) { + glfbo.glGenerateMipmapEXT(target); + img.setMipmapsGenerated(true); } img.clearUpdateNeeded(); } + private boolean isMipmapGenerationSupported(Image.Format format, ColorSpace colorSpace) { + GLImageFormat gf = texUtil.getImageFormat(format, colorSpace == ColorSpace.sRGB); + return gf != null && gf.colorRenderable && gf.filterable; + } + + static int textureMaxLevelForUpload(boolean canSetTextureMaxLevel, + boolean needsMipmaps, + boolean hwMipmapSupported, + boolean hasUploadMipmaps, + int[] uploadMipMapSizes, + int generatedMipMaxLevel) { + if (!canSetTextureMaxLevel) { + return -1; + } + if (needsMipmaps && hwMipmapSupported) { + return generatedMipMaxLevel; + } + if (!hasUploadMipmaps) { + return 0; + } + return uploadMipMapSizes.length - 1; + } + + static int generatedMipMaxLevel(int width, int height, int depth) { + int maxDimension = Math.max(Math.max(width, height), Math.max(1, depth)); + int maxLevel = 0; + while (maxDimension > 1) { + maxDimension >>= 1; + maxLevel++; + } + return maxLevel; + } + + private Image cloneImageForUpload(Image image, boolean scaleToPot) { + if (scaleToPot) { + return MipMapGenerator.resizeToPowerOf2(image); + } + + ArrayList data = new ArrayList<>(image.getData().size()); + for (ByteBuffer buffer : image.getData()) { + if (buffer == null) { + return null; + } + data.add(buffer.duplicate()); + } + return new Image(image.getFormat(), image.getWidth(), image.getHeight(), image.getDepth(), + data, null, image.getColorSpace()); + } + + private boolean needsGeneratedMipmaps(Image image) { + if (!image.isGeneratedMipmapsRequired() || image.isMipmapsGenerated()) { + return false; + } + + if (isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)) { + return true; + } + + return image.getId() == -1 + && MipMapGenerator.canGenerateMipmaps(image); + } + @Override public void setTexture(int unit, Texture tex) throws TextureUnitException { if (unit < 0 || unit >= RenderContext.maxTextureUnits) { @@ -2838,7 +3040,7 @@ public void setTexture(int unit, Texture tex) throws TextureUnitException { } Image image = tex.getImage(); - if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated())) { + if (image.isUpdateNeeded() || needsGeneratedMipmaps(image)) { // Check NPOT requirements boolean scaleToPot = false; @@ -2907,7 +3109,7 @@ public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferOb updateShaderStorageBufferObjectData(bufferObject); } if (context.boundBO[bindingPoint] == null || context.boundBO[bindingPoint].get() != bufferObject) { - gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, bufferObject.getId()); + bindShaderStorageBufferBase(bindingPoint, bufferObject.getId()); bufferObject.setBinding(bindingPoint); context.boundBO[bindingPoint] = bufferObject.getWeakRef(); } @@ -3157,14 +3359,14 @@ private void updateBufferData(int type, BufferObject bo) { BufferRegion reg; while ((reg = it.next()) != null) { - gl3.glBindBuffer(type, bufferId); + gl.glBindBuffer(type, bufferId); if (reg.isFullBufferRegion()) { ByteBuffer bbf = bo.getData(); if (logger.isLoggable(java.util.logging.Level.FINER)) { logger.log(java.util.logging.Level.FINER, "Update full buffer {0} with {1} bytes", new Object[] { bo, bbf.remaining() }); } gl.glBufferData(type, bbf, usage); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); break; } else { @@ -3172,7 +3374,7 @@ private void updateBufferData(int type, BufferObject bo) { logger.log(java.util.logging.Level.FINER, "Update region {0} of {1}", new Object[] { reg, bo }); } gl.glBufferSubData(type, reg.getStart(), reg.getData()); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); } } @@ -3593,7 +3795,7 @@ public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceDat @Override public void setMainFrameBufferSrgb(boolean enableSrgb) { // Gamma correction - if (!caps.contains(Caps.Srgb) && enableSrgb) { + if ((!caps.contains(Caps.SrgbWriteControl) || !caps.contains(Caps.Srgb)) && enableSrgb) { // Not supported, sorry. logger.warning("sRGB framebuffer is not supported " + "by video hardware, but was requested."); @@ -3699,7 +3901,7 @@ public boolean isLinearizeSrgbImages() { */ @Override public boolean isMainFrameBufferSrgb() { - if (!caps.contains(Caps.Srgb)) { + if (!caps.contains(Caps.Srgb) || !caps.contains(Caps.SrgbWriteControl)) { return false; } else { return mainFrameBufferSrgb; diff --git a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java index f3cc721df9..8f696f4b49 100644 --- a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java +++ b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java @@ -849,8 +849,9 @@ public long getUniqueId() { * * The FrameBuffer must have an SRGB texture attached. * - * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} capability - * for this option to take any effect. + * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} and + * {@link Caps#SrgbWriteControl sRGB write control} capabilities for this + * option to take any effect. * * Rendering operations performed on this framebuffer shall undergo a linear * -> sRGB color space conversion when this flag is enabled. If diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3159f0856a..3181abd4f9 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -79,16 +79,16 @@ public enum Format { Reserved2(0), /** - * half-precision floating-point grayscale/luminance. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16F(16,true), /** * single-precision floating-point grayscale/luminance. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#FloatTexture}. */ Luminance32F(32,true), @@ -101,9 +101,9 @@ public enum Format { Reserved3(0), /** - * half-precision floating-point grayscale/luminance and alpha. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance and alpha. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16FAlpha16F(32,true), @@ -255,7 +255,7 @@ public enum Format { * but will be converted to {@link Format#RGB111110F} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#PackedFloatTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}. */ RGB16F_to_RGB111110F(48,true), @@ -271,7 +271,7 @@ public enum Format { * but will be converted to {@link Format#RGB9E5} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#SharedExponentTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}. */ RGB16F_to_RGB9E5(48,true), @@ -283,9 +283,9 @@ public enum Format { RGB9E5(32,true), /** - * half-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating point red, green, and blue. + * + * Requires {@link Caps#HalfFloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB16F(48,true), @@ -293,22 +293,22 @@ public enum Format { /** * half-precision floating point red, green, blue, and alpha. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RGBA16F(64,true), /** - * single-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, and blue. + * + * Requires {@link Caps#FloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB32F(96,true), /** - * single-precision floating point red, green, blue and alpha. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, blue and alpha. + * + * Requires {@link Caps#FloatTexture}. */ RGBA32F(128,true), @@ -507,21 +507,21 @@ public enum Format { /** * half-precision floating point red. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ R16F(16,true), /** - * single-precision floating point red. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. */ R32F(32,true), /** * half-precision floating point red and green. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RG16F(32,true), diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java index 0fe86270d8..e329f21a1f 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java @@ -172,11 +172,15 @@ public ImageCodec(int bpp, int flags, int maxAlpha, int maxRed, int maxGreen, in * @param format The format to lookup. * @return The codec capable of decoding it, or null if not found. */ - public static ImageCodec lookup(Format format) { - ImageCodec codec = params.get(format); - if (codec == null) { - throw new UnsupportedOperationException("The format " + format + " is not supported"); - } - return codec; - } -} + public static ImageCodec lookup(Format format) { + ImageCodec codec = params.get(format); + if (codec == null) { + throw new UnsupportedOperationException("The format " + format + " is not supported"); + } + return codec; + } + + static boolean isSupported(Format format) { + return params.containsKey(format); + } +} diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java index 1a75203225..18b21d364b 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java @@ -62,11 +62,22 @@ * * @author Kirill Vainer */ -public abstract class ImageRaster { - - /** - * Create new image reader / writer. - * +public abstract class ImageRaster { + + /** + * Tests whether {@link ImageRaster} can read and write pixels for the + * specified image format. + * + * @param format the image format to test + * @return true if ImageRaster supports the format + */ + public static boolean isSupported(Image.Format format) { + return ImageCodec.isSupported(format); + } + + /** + * Create new image reader / writer. + * * @param image The image to read / write to. * @param slice Which slice to use. Only applies to 3D images, 2D image * arrays or cubemaps. diff --git a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java index 7a08e1cdde..7c49782058 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java @@ -45,6 +45,7 @@ public final class LastTextureState { public Texture.WrapMode sWrap, tWrap, rWrap; public Texture.MagFilter magFilter; public Texture.MinFilter minFilter; + public boolean minFilterMipmapsAvailable; public int anisoFilter; public Texture.ShadowCompareMode shadowCompareMode; @@ -58,6 +59,7 @@ public void reset() { rWrap = null; magFilter = null; minFilter = null; + minFilterMipmapsAvailable = false; anisoFilter = 1; // The default in OpenGL is OFF, so we avoid setting this per texture diff --git a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java index 7073dfab52..9e8ac4bbd0 100644 --- a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java +++ b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,99 +34,605 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.ImageRaster; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Locale; -public class MipMapGenerator { +public final class MipMapGenerator { + + private static final float EPSILON_ALPHA = 1e-8f; private MipMapGenerator() { } + /** + * Scales the base level of a 2D image. + * + * The returned image keeps the same Image.Format and ColorSpace as the input. + * Pixel format conversion is delegated to ImageRaster. + * + * For normal color textures, this method filters in linear space. + */ public static Image scaleImage(Image inputImage, int outputWidth, int outputHeight) { - int size = outputWidth * outputHeight * inputImage.getFormat().getBitsPerPixel() / 8; - ByteBuffer buffer = BufferUtils.createByteBuffer(size); - Image outputImage = new Image(inputImage.getFormat(), - outputWidth, - outputHeight, - buffer, - inputImage.getColorSpace()); - - ImageRaster input = ImageRaster.create(inputImage, 0, 0, false); - ImageRaster output = ImageRaster.create(outputImage, 0, 0, false); - - float xRatio = ((float) (input.getWidth() - 1)) / output.getWidth(); - float yRatio = ((float) (input.getHeight() - 1)) / output.getHeight(); - - ColorRGBA outputColor = new ColorRGBA(0, 0, 0, 0); - ColorRGBA bottomLeft = new ColorRGBA(); - ColorRGBA bottomRight = new ColorRGBA(); - ColorRGBA topLeft = new ColorRGBA(); - ColorRGBA topRight = new ColorRGBA(); - - for (int y = 0; y < outputHeight; y++) { - for (int x = 0; x < outputWidth; x++) { - float x2f = x * xRatio; - float y2f = y * yRatio; - - int x2 = (int) x2f; - int y2 = (int) y2f; - - input.getPixel(x2, y2, bottomLeft); - input.getPixel(x2 + 1, y2, bottomRight); - input.getPixel(x2, y2 + 1, topLeft); - input.getPixel(x2 + 1, y2 + 1, topRight); - - outputColor.set(bottomLeft).addLocal(bottomRight) - .addLocal(topLeft).addLocal(topRight); - outputColor.multLocal(1f / 4f); - output.setPixel(x, y, outputColor); - } - } - return outputImage; + return scaleImage(inputImage, outputWidth, outputHeight, true, isSrgb(inputImage)); + } + + /** + * Scales the base level of a 2D image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha to reduce transparent-edge halos + */ + public static Image scaleImage(Image inputImage, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + return scaleLevel(inputImage, 0, outputWidth, outputHeight, convertToLinear, alphaWeighted); } - public static Image resizeToPowerOf2(Image original){ + public static Image resizeToPowerOf2(Image original) { int potWidth = FastMath.nearestPowerOfTwo(original.getWidth()); int potHeight = FastMath.nearestPowerOfTwo(original.getHeight()); return scaleImage(original, potWidth, potHeight); } - public static void generateMipMaps(Image image){ - int width = image.getWidth(); - int height = image.getHeight(); + /** + * Returns true if this image has CPU-side data in a format supported by this + * mipmap generator. + * + * This generator works on uncompressed, non-depth, byte-addressable texture + * formats supported by ImageRaster. + */ + public static boolean canGenerateMipmaps(Image image) { + if (image == null + || image.getWidth() < 1 + || image.getHeight() < 1 + || image.getDepth() > 1 + || image.getFormat().isCompressed() + || image.getFormat().isDepthFormat() + || image.getData() == null + || image.getData().isEmpty()) { + return false; + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + return false; + } + + int baseLevelSize; + try { + baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + } catch (RuntimeException exception) { + return false; + } + + for (ByteBuffer data : image.getData()) { + if (data == null || data.capacity() < baseLevelSize) { + return false; + } + } + + return ImageRaster.isSupported(image.getFormat()); + } + + /** + * Generates a complete mip chain for the image. + * + * Default behavior is intended for normal color/albedo textures: + * - filtering is done in linear space; + * - sRGB images with alpha use alpha-weighted RGB filtering. + * + * For normal maps, roughness, metallic, AO, height maps, or packed data maps, + * prefer generateMipMaps(image, true, false), assuming the image is not marked sRGB. + */ + public static void generateMipMaps(Image image) { + generateMipMaps(image, true, isSrgb(image)); + } + + /** + * Generates a complete mip chain for every data buffer/slice in the image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha + */ + public static void generateMipMaps(Image image, boolean convertToLinear, boolean alphaWeighted) { + validateImage(image); + + int baseWidth = image.getWidth(); + int baseHeight = image.getHeight(); + + ArrayList chains = new ArrayList<>(image.getData().size()); + int dataCount = image.getData().size(); + + for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { + chains.add(generateMipChainForSlice( + image, + dataIndex, + baseWidth, + baseHeight, + convertToLinear, + alphaWeighted + )); + } + + for (int dataIndex = 0; dataIndex < chains.size(); dataIndex++) { + image.setData(dataIndex, chains.get(dataIndex).combinedData); + } + + if (!chains.isEmpty()) { + image.setMipMapSizes(chains.get(0).mipSizes); + } + } + + private static MipChain generateMipChainForSlice(Image sourceImage, + int sourceSlice, + int baseWidth, + int baseHeight, + boolean convertToLinear, + boolean alphaWeighted) { + ArrayList levels = new ArrayList<>(); + + Image.Format format = sourceImage.getFormat(); + ColorSpace colorSpace = sourceImage.getColorSpace(); + + ByteBuffer baseLevel = copyBaseLevel( + sourceImage.getData(sourceSlice), + levelSize(format, baseWidth, baseHeight) + ); + + Image current = new Image(format, baseWidth, baseHeight, baseLevel, colorSpace); + levels.add(baseLevel); + + int width = baseWidth; + int height = baseHeight; + + while (width > 1 || height > 1) { + int nextWidth = Math.max(1, width / 2); + int nextHeight = Math.max(1, height / 2); + + Image next = scaleLevel( + current, + 0, + nextWidth, + nextHeight, + convertToLinear, + alphaWeighted + ); + + levels.add(next.getData(0)); + + current = next; + width = nextWidth; + height = nextHeight; + } - Image current = image; - ArrayList output = new ArrayList<>(); int totalSize = 0; + int[] mipSizes = new int[levels.size()]; + + for (int i = 0; i < levels.size(); i++) { + int size = levels.get(i).capacity(); + mipSizes[i] = size; + totalSize += size; + } - while (height >= 1 || width >= 1){ - output.add(current.getData(0)); - totalSize += current.getData(0).capacity(); + ByteBuffer combined = BufferUtils.createByteBuffer(totalSize); - if (height == 1 || width == 1) { - break; + for (ByteBuffer level : levels) { + ByteBuffer duplicate = level.duplicate(); + duplicate.clear(); + combined.put(duplicate); + } + + combined.flip(); + + return new MipChain(combined, mipSizes); + } + + private static Image scaleLevel(Image inputImage, + int inputSlice, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + if (outputWidth < 1 || outputHeight < 1) { + throw new IllegalArgumentException("Output size must be at least 1x1"); + } + + validateImage(inputImage); + + int outputSize = levelSize(inputImage.getFormat(), outputWidth, outputHeight); + ByteBuffer outputBuffer = BufferUtils.createByteBuffer(outputSize); + + Image outputImage = new Image( + inputImage.getFormat(), + outputWidth, + outputHeight, + outputBuffer, + inputImage.getColorSpace() + ); + + ImageRaster input = ImageRaster.create(inputImage, inputSlice, 0, convertToLinear); + ImageRaster output = ImageRaster.create(outputImage, 0, 0, convertToLinear); + + boolean downscale = outputWidth <= input.getWidth() && outputHeight <= input.getHeight(); + boolean clampOutput = !inputImage.getFormat().isFloatingPont(); + + if (downscale) { + areaResample(input, output, alphaWeighted, clampOutput); + } else { + bilinearResample(input, output, alphaWeighted, clampOutput); + } + + return outputImage; + } + + /** + * Area filter. + * + * This is the right default for mipmap generation because every destination + * pixel represents the average area of the corresponding source rectangle. + */ + private static void areaResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); + + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); + + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY0 = y * scaleY; + double sourceY1 = (y + 1) * scaleY; + + int yStart = Math.max(0, (int) Math.floor(sourceY0)); + int yEnd = Math.min(sourceHeight, Math.max(yStart + 1, (int) Math.ceil(sourceY1))); + + for (int x = 0; x < targetWidth; x++) { + double sourceX0 = x * scaleX; + double sourceX1 = (x + 1) * scaleX; + + int xStart = Math.max(0, (int) Math.floor(sourceX0)); + int xEnd = Math.min(sourceWidth, Math.max(xStart + 1, (int) Math.ceil(sourceX1))); + + accumulator.clear(); + + for (int sy = yStart; sy < yEnd; sy++) { + double overlapY0 = Math.max(sourceY0, sy); + double overlapY1 = Math.min(sourceY1, sy + 1.0); + float weightY = (float) Math.max(0.0, overlapY1 - overlapY0); + + if (weightY <= 0f) { + continue; + } + + for (int sx = xStart; sx < xEnd; sx++) { + double overlapX0 = Math.max(sourceX0, sx); + double overlapX1 = Math.min(sourceX1, sx + 1.0); + float weightX = (float) Math.max(0.0, overlapX1 - overlapX0); + + if (weightX <= 0f) { + continue; + } + + float weight = weightX * weightY; + + input.getPixel(sx, sy, sample); + accumulator.add(sample, weight, alphaWeighted, clampOutput); + } + } + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); } + } + } + + /** + * Bilinear filter. + * + * Used only when scaleImage() is asked to upscale. + * Mipmap generation itself normally uses areaResample(). + */ + private static void bilinearResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); - height /= 2; - width /= 2; + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); - current = scaleImage(current, width, height); + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY = (y + 0.5) * scaleY - 0.5; + + int y0 = (int) Math.floor(sourceY); + double ty = sourceY - y0; + + if (y0 < 0) { + y0 = 0; + ty = 0.0; + } + + int y1 = y0 + 1; + + if (y1 >= sourceHeight) { + y1 = sourceHeight - 1; + y0 = y1; + ty = 0.0; + } + + float wy0 = (float) (1.0 - ty); + float wy1 = (float) ty; + + for (int x = 0; x < targetWidth; x++) { + double sourceX = (x + 0.5) * scaleX - 0.5; + + int x0 = (int) Math.floor(sourceX); + double tx = sourceX - x0; + + if (x0 < 0) { + x0 = 0; + tx = 0.0; + } + + int x1 = x0 + 1; + + if (x1 >= sourceWidth) { + x1 = sourceWidth - 1; + x0 = x1; + tx = 0.0; + } + + float wx0 = (float) (1.0 - tx); + float wx1 = (float) tx; + + accumulator.clear(); + + input.getPixel(x0, y0, sample); + accumulator.add(sample, wx0 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x1, y0, sample); + accumulator.add(sample, wx1 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x0, y1, sample); + accumulator.add(sample, wx0 * wy1, alphaWeighted, clampOutput); + + input.getPixel(x1, y1, sample); + accumulator.add(sample, wx1 * wy1, alphaWeighted, clampOutput); + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); + } + } + } + + private static void validateImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null"); } - ByteBuffer combinedData = BufferUtils.createByteBuffer(totalSize); - int[] mipSizes = new int[output.size()]; - for (int i = 0; i < output.size(); i++){ - ByteBuffer data = output.get(i); - data.clear(); - combinedData.put(data); - mipSizes[i] = data.capacity(); + if (image.getWidth() < 1 || image.getHeight() < 1) { + throw new IllegalArgumentException("Image size must be at least 1x1"); } - combinedData.flip(); - // insert mip data into image - image.setData(0, combinedData); - image.setMipMapSizes(mipSizes); + if (image.getData() == null || image.getData().isEmpty()) { + throw new IllegalArgumentException("Image has no data buffers"); + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + throw new UnsupportedOperationException( + "CPU mipmap generation requires byte-addressable formats. Unsupported format: " + + image.getFormat() + + " with " + + bitsPerPixel + + " bits per pixel" + ); + } + + int baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + for (int dataIndex = 0; dataIndex < image.getData().size(); dataIndex++) { + ByteBuffer data = image.getData(dataIndex); + if (data == null) { + throw new IllegalArgumentException("Image data buffer " + dataIndex + " is null"); + } + if (data.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data buffer " + dataIndex + " is smaller than expected base level size. Data capacity=" + + data.capacity() + + ", expected=" + + baseLevelSize + ); + } + } + } + + private static int levelSize(Image.Format format, int width, int height) { + int bitsPerPixel = format.getBitsPerPixel(); + + long bits = (long) width * (long) height * (long) bitsPerPixel; + + if ((bits % 8L) != 0L) { + throw new UnsupportedOperationException( + "Image level is not byte-addressable: " + + width + + "x" + + height + + " " + + format + ); + } + + long bytes = bits / 8L; + + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Image level is too large: " + + width + + "x" + + height + + " " + + format + ); + } + + return (int) bytes; + } + + /** + * If the input image already has mipmaps, its ByteBuffer may contain all levels. + * For rebuilding mipmaps, we only want the base level. + */ + private static ByteBuffer copyBaseLevel(ByteBuffer source, int baseLevelSize) { + if (source == null) { + throw new IllegalArgumentException("Image data buffer is null"); + } + if (source.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data is smaller than expected base level size. Data capacity=" + + source.capacity() + + ", expected=" + + baseLevelSize + ); + } + + ByteBuffer duplicate = source.duplicate(); + duplicate.clear(); + duplicate.limit(baseLevelSize); + + ByteBuffer copy = BufferUtils.createByteBuffer(baseLevelSize); + copy.put(duplicate); + copy.flip(); + + return copy; + } + + private static boolean isSrgb(Image image) { + if (image.getColorSpace() == ColorSpace.sRGB) { + return true; + } + + String formatName = image.getFormat().name().toLowerCase(Locale.ROOT); + return formatName.contains("srgb"); + } + + private static final class MipChain { + final ByteBuffer combinedData; + final int[] mipSizes; + + MipChain(ByteBuffer combinedData, int[] mipSizes) { + this.combinedData = combinedData; + this.mipSizes = mipSizes; + } + } + + private static final class PixelAccumulator { + private float r; + private float g; + private float b; + private float a; + private float weight; + + void clear() { + r = 0f; + g = 0f; + b = 0f; + a = 0f; + weight = 0f; + } + + void add(ColorRGBA color, float sampleWeight, boolean alphaWeighted, boolean clampOutput) { + if (sampleWeight <= 0f) { + return; + } + + float alpha = clampOutput ? clamp01(color.a) : color.a; + + if (alphaWeighted) { + r += color.r * alpha * sampleWeight; + g += color.g * alpha * sampleWeight; + b += color.b * alpha * sampleWeight; + } else { + r += color.r * sampleWeight; + g += color.g * sampleWeight; + b += color.b * sampleWeight; + } + + a += alpha * sampleWeight; + weight += sampleWeight; + } + + void toColor(ColorRGBA store, boolean alphaWeighted, boolean clampOutput) { + if (weight <= 0f) { + store.set(0f, 0f, 0f, 0f); + return; + } + + float outA = a / weight; + + float outR; + float outG; + float outB; + + if (alphaWeighted) { + if (a > EPSILON_ALPHA) { + outR = r / a; + outG = g / a; + outB = b / a; + } else { + outR = 0f; + outG = 0f; + outB = 0f; + } + } else { + outR = r / weight; + outG = g / weight; + outB = b / weight; + } + + if (clampOutput) { + outR = clamp01(outR); + outG = clamp01(outG); + outB = clamp01(outB); + outA = clamp01(outA); + } + + store.set(outR, outG, outB, outA); + } + + private static float clamp01(float value) { + if (value <= 0f) { + return 0f; + } + + if (value >= 1f) { + return 1f; + } + + return value; + } } } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag new file mode 100644 index 0000000000..7ca7901b22 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag @@ -0,0 +1,21 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/ShaderLib/MultiSample.glsllib" + +uniform COLORTEXTURE m_Texture; +varying vec2 texCoord; + +vec3 linearToSrgb(vec3 color) { + vec3 linear = max(color, vec3(0.0)); + vec3 encodedLow = linear * 12.92; + vec3 encodedHigh = 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055; + return mix(encodedLow, encodedHigh, step(vec3(0.0031308), linear)); +} + +void main() { + vec4 color = getColor(m_Texture, texCoord); + #ifdef SRGB + gl_FragColor = vec4(linearToSrgb(color.rgb), color.a); + #else + gl_FragColor = color; + #endif +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md new file mode 100644 index 0000000000..2197032e08 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md @@ -0,0 +1,24 @@ +MaterialDef Blit { + + MaterialParameters { + Int BoundDrawBuffer + Int NumSamples + Boolean Srgb + Texture2D Texture + } + + Technique { + VertexShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.vert + FragmentShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.frag + + WorldParameters { + } + + Defines { + BOUND_DRAW_BUFFER : BoundDrawBuffer + RESOLVE_MS : NumSamples + SRGB: Srgb + } + } + +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert new file mode 100644 index 0000000000..5d55b1f5af --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert @@ -0,0 +1,11 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +attribute vec4 inPosition; +attribute vec2 inTexCoord; + +varying vec2 texCoord; + +void main() { + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); + texCoord = inTexCoord; +} \ No newline at end of file diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java new file mode 100644 index 0000000000..3338e6d4b4 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import com.jme3.renderer.Caps; +import com.jme3.texture.Image; +import java.util.EnumSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class GLImageFormatsTest { + + @Test + public void testGles3UsesCoreHalfFloatType() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.FloatTexture, Caps.HalfFloatTexture); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.R16F.ordinal()].dataType); + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.RGBA16F.ordinal()].dataType); + } + + @Test + public void testGles3DoesNotExposeDesktopByteOrderFormats() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.BGR8.ordinal()]); + assertNull(formats[0][Image.Format.ABGR8.ordinal()]); + assertNull(formats[0][Image.Format.ARGB8.ordinal()]); + assertNull(formats[0][Image.Format.BGRA8.ordinal()]); + assertNull(formats[1][Image.Format.BGR8.ordinal()]); + assertNull(formats[1][Image.Format.ABGR8.ordinal()]); + assertNull(formats[1][Image.Format.ARGB8.ordinal()]); + assertNull(formats[1][Image.Format.BGRA8.ordinal()]); + } + + @Test + public void testGles3LegacyAlphaUsesGlesInternalFormatWhenNoCoreProfile() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].internalFormat); + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].format); + } + + @Test + public void testGles3CoreFormatsRemainMapped() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb, Caps.FloatTexture, + Caps.IntegerTexture, Caps.PackedFloatTexture, + Caps.SharedExponentTexture, Caps.TextureCompressionETC2, + Caps.Depth24, Caps.FloatDepthBuffer, Caps.PackedDepthStencilBuffer); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.RGB10A2.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB111110F.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB9E5.ordinal()]); + assertNotNull(formats[0][Image.Format.RGBA8UI.ordinal()]); + assertNotNull(formats[0][Image.Format.ETC2.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + } + + @Test + public void testDepthFormatsFollowExplicitCaps() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth24); + caps.add(Caps.FloatDepthBuffer); + caps.add(Caps.PackedDepthStencilBuffer); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth32); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth32.ordinal()]); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java new file mode 100644 index 0000000000..26d9aba160 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GLRendererMipmapPolicyTest { + + @Test + public void testDoesNotClampWhenHardwareMipmapGenerationIsPending() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + true, + false, + null, + 7); + + assertEquals(7, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenNoMipmapsAreUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + false, + false, + false, + null, + 4); + + assertEquals(0, maxLevel); + } + + @Test + public void testUsesUploadedMipCountForExistingOrCpuMipmaps() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + true, + new int[] {64, 16, 4, 1}, + 7); + + assertEquals(3, maxLevel); + } + + @Test + public void testSkipsMaxLevelWhenCapabilityIsUnavailable() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + false, + false, + false, + true, + new int[] {64, 16}, + 7); + + assertEquals(-1, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenRequestedMipmapsCannotBeGeneratedOrUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + false, + null, + 7); + + assertEquals(0, maxLevel); + } + + @Test + public void testGeneratedMipMaxLevelUsesLargestUploadDimension() { + assertEquals(0, GLRenderer.generatedMipMaxLevel(1, 1, 1)); + assertEquals(2, GLRenderer.generatedMipMaxLevel(3, 5, 1)); + assertEquals(3, GLRenderer.generatedMipMaxLevel(4, 8, 1)); + assertEquals(4, GLRenderer.generatedMipMaxLevel(4, 8, 16)); + } +} diff --git a/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java new file mode 100644 index 0000000000..fc2a6765d8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.util; + +import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MipMapGeneratorTest { + + @Test + public void testGenerateMipMapsAfterResizeToPowerOf2() { + ByteBuffer data = BufferUtils.createByteBuffer(3 * 5 * 4); + for (int i = 0; i < data.capacity(); i++) { + data.put((byte) i); + } + data.flip(); + + Image image = new Image(Image.Format.RGBA8, 3, 5, data, ColorSpace.Linear); + Image resized = MipMapGenerator.resizeToPowerOf2(image); + + MipMapGenerator.generateMipMaps(resized, true, false); + + assertEquals(4, resized.getWidth()); + assertEquals(8, resized.getHeight()); + assertTrue(resized.hasMipmaps()); + assertNotNull(resized.getData(0)); + assertNotNull(resized.getMipMapSizes()); + assertTrue(resized.getMipMapSizes().length > 1); + } + + @Test + public void testGenerateMipMapsRejectsNullDataBuffer() { + ArrayList data = new ArrayList<>(); + data.add(null); + Image image = new Image(Image.Format.RGBA8, 2, 2, 1, data, null, ColorSpace.Linear); + + assertThrows(IllegalArgumentException.class, + () -> MipMapGenerator.generateMipMaps(image, true, false)); + } +} diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java index 9bbfb86f5c..44ded69fee 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java @@ -234,24 +234,18 @@ public void render(RenderManager rm) { */ private void setupMaterials(Application app) { AssetManager manager = app.getAssetManager(); - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(manager, ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(manager, ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(manager, ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(manager, ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(manager, ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(manager, ColorRGBA.Pink); + } + + private static Material createDebugMaterial(AssetManager manager, ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } private void updateRigidBodies() { diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java index 16bc38ac8c..7ea8f3ba0c 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java @@ -268,23 +268,17 @@ protected void setupDebugNode() { * Initialize all the DebugTools materials. */ protected void setupMaterials() { - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(ColorRGBA.Pink); + } + + private Material createDebugMaterial(ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } } diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java index 3e74f1ab3c..41c3b062d7 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java @@ -132,10 +132,12 @@ public static Mesh getDebugMesh(CollisionShape shape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConvexShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } else if (shape.getCShape() instanceof ConcaveShape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConcaveShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } return mesh; } @@ -157,10 +159,10 @@ private static FloatBuffer getVertices(ConcaveShape concaveShape) { /** * Processes the given convex shape to retrieve a correctly ordered FloatBuffer to - * construct the shape from with a TriMesh. + * construct the shape from with line segments. * * @param convexShape the shape to retrieve the vertices from. - * @return the vertices as a FloatBuffer, ordered as Triangles. + * @return the vertices as a FloatBuffer, ordered as line segments. */ private static FloatBuffer getVertices(ConvexShape convexShape) { // Check there is a hull shape to render @@ -180,8 +182,8 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { assert hull.numTriangles() > 0 : "Expecting the Hull shape to have triangles"; int numberOfTriangles = hull.numTriangles(); - // The number of bytes needed is: (floats in a vertex) * (vertices in a triangle) * (# of triangles) * (size of float in bytes) - final int numberOfFloats = 3 * 3 * numberOfTriangles; + // The number of floats needed is: (floats in a vertex) * (vertices in a triangle outline) * (# of triangles) + final int numberOfFloats = 3 * 6 * numberOfTriangles; FloatBuffer vertices = BufferUtils.createFloatBuffer(numberOfFloats); // Force the limit, set the cap - the largest number of floats we will use the buffer for @@ -199,15 +201,24 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { vertexB = hullVertices.get(hullIndices.get(index++)); vertexC = hullVertices.get(hullIndices.get(index++)); - // Put the vertices into the vertex buffer - vertices.put(vertexA.x).put(vertexA.y).put(vertexA.z); - vertices.put(vertexB.x).put(vertexB.y).put(vertexB.z); - vertices.put(vertexC.x).put(vertexC.y).put(vertexC.z); + putTriangleOutline(vertices, vertexA, vertexB, vertexC); } vertices.clear(); return vertices; } + + private static void putTriangleOutline(FloatBuffer vertices, Vector3f vertexA, + Vector3f vertexB, Vector3f vertexC) { + putLine(vertices, vertexA, vertexB); + putLine(vertices, vertexB, vertexC); + putLine(vertices, vertexC, vertexA); + } + + private static void putLine(FloatBuffer vertices, Vector3f start, Vector3f end) { + vertices.put(start.x).put(start.y).put(start.z); + vertices.put(end.x).put(end.y).put(end.z); + } } /** @@ -227,11 +238,13 @@ public BufferedTriangleCallback() { @Override public void processTriangle(Vector3f[] triangle, int partId, int triangleIndex) { - // Three sets of individual lines // The new Vector is needed as the given triangle reference is from a pool vertices.add(new Vector3f(triangle[0])); vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[2])); vertices.add(new Vector3f(triangle[2])); + vertices.add(new Vector3f(triangle[0])); } /** diff --git a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java index f51dbf56b3..0bbefd7cf5 100644 --- a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java +++ b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java @@ -40,10 +40,11 @@ import com.jme3.bullet.collision.shapes.CollisionShape; import com.jme3.bullet.collision.shapes.SphereCollisionShape; import com.jme3.bullet.control.BetterCharacterControl; -import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.KinematicRagdollControl; +import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.bullet.util.DebugShapeFactory; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.binary.BinaryExporter; @@ -171,6 +172,21 @@ public InputStream openStream() { Assertions.assertEquals(new Vector3f(0.26f, 0.27f, 0.28f), rbcCopy.getLinearVelocity()); } + /** + * Debug collision meshes should render as line primitives instead of + * relying on OpenGL polygon wireframe mode, which is unavailable in GLES. + */ + @Test + public void testDebugMeshesUseLines() { + CollisionShape shape = new BoxCollisionShape(Vector3f.UNIT_XYZ); + Mesh mesh = DebugShapeFactory.getDebugMesh(shape); + + Assertions.assertNotNull(mesh); + Assertions.assertEquals(Mesh.Mode.Lines, mesh.getMode()); + Assertions.assertTrue(mesh.getVertexCount() > 0); + Assertions.assertEquals(0, mesh.getVertexCount() % 6); + } + /** * Test case for JME issue #1004: RagdollUtils can't handle 16-bit bone indices. */ diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 56ef07ffab..4d132618cd 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -7,6 +7,8 @@ import java.nio.IntBuffer; import org.lwjgl.opengl.ARBDrawInstanced; import org.lwjgl.opengl.ARBInstancedArrays; +import org.lwjgl.opengl.ARBProgramInterfaceQuery; +import org.lwjgl.opengl.ARBShaderStorageBufferObject; import org.lwjgl.opengl.ARBSync; import org.lwjgl.opengl.ARBTextureMultisample; import org.lwjgl.opengl.ARBUniformBufferObject; @@ -87,6 +89,16 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(int condition, int flags) { return ARBSync.glFenceSync(condition, flags); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 74736f0666..83836aa3da 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -103,6 +103,16 @@ public void glUniformBlockBinding(final int program, final int uniformBlockIndex ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(final int condition, final int flags) { return ARBSync.glFenceSync(condition, flags);
As a shorthand, the user can set {@link AppSettings#setGammaCorrection(boolean)} to true * to toggle both {@link Renderer#setLinearizeSrgbImages(boolean)} and * {@link Renderer#setMainFrameBufferSrgb(boolean)} if the - * {@link Caps#Srgb} is supported by the GPU. + * {@link Caps#Srgb} and {@link Caps#SrgbWriteControl} capabilities are + * supported by the GPU. * * @param srgb true for sRGB colorspace, false for linear colorspace * @throws RendererException If the GPU hardware does not support sRGB. * * @see FrameBuffer#setSrgb(boolean) * @see Caps#Srgb + * @see Caps#SrgbWriteControl */ public void setMainFrameBufferSrgb(boolean srgb); @@ -560,4 +563,70 @@ public default void pushDebugGroup(String name) { * Registers a NativeObject to be cleaned up by this renderer. */ public void registerNativeObject(NativeObject nativeObject); + + public default Format getBestColorTargetFormat(boolean floatingPoint) { + return getBestColorTargetFormat(floatingPoint, true, false); + } + + public default Format getBestColorTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withAlpha) { + if (!floatingPoint) { + return Format.RGBA8; + } + + if (!highPrecision) { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } + } + + if (withAlpha) { + if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } else { + if (getCaps().contains(Caps.PackedFloatTexture) + && getCaps().contains(Caps.PackedFloatColorBuffer)) { + return Format.RGB111110F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGB)) { + return Format.RGB16F; + } else if (getCaps().contains(Caps.HalfFloatTexture) + && getCaps().contains(Caps.HalfFloatColorBufferRGBA)) { + return Format.RGBA16F; + } + } + + return Format.RGBA8; + } + + public default Format getBestDepthTargetFormat() { + return getBestDepthTargetFormat(false, false, false); + } + + public default Format getBestDepthTargetFormat(boolean floatingPoint, boolean highPrecision, boolean withStencil) { + if (withStencil) { + if (getCaps().contains(Caps.PackedDepthStencilBuffer)) { + return Format.Depth24Stencil8; + } + } else { + if (floatingPoint && getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + if (highPrecision) { + if (getCaps().contains(Caps.Depth32)) { + return Format.Depth32; + } + if (getCaps().contains(Caps.FloatDepthBuffer)) { + return Format.Depth32F; + } + } + if (getCaps().contains(Caps.Depth24)) { + return Format.Depth24; + } + } + + return Format.Depth; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java index 10c5c7ff88..2db094c437 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java @@ -293,6 +293,29 @@ public default void glUniformBlockBinding(int program, int uniformBlockIndex, in throw new UnsupportedOperationException("Uniform buffer objects are not supported"); } + /** + * Retrieves the index of a named program resource. + * + * @param program the name of a program object + * @param programInterface the program interface containing the resource + * @param name the name of the resource + * @return the resource index + */ + public default int glGetProgramResourceIndex(int program, int programInterface, String name) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + + /** + * Assigns a shader storage block to a binding point. + * + * @param program the name of a program object + * @param storageBlockIndex the index of the shader storage block within {@code program} + * @param storageBlockBinding the binding point to assign + */ + public default void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + public default void glPushDebugGroup(int source, int id, String message) { } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java index 8e65fbdef1..1d974fa9a4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormat.java @@ -42,6 +42,9 @@ public final class GLImageFormat { public final int format; public final int dataType; public final boolean compressed; + public final boolean colorRenderable; + public final boolean depthRenderable; + public final boolean filterable; public final boolean swizzleRequired; /** @@ -51,11 +54,14 @@ public final class GLImageFormat { * @param format OpenGL format * @param dataType OpenGL datatype */ - public GLImageFormat(int internalFormat, int format, int dataType) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = false; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -66,12 +72,18 @@ public GLImageFormat(int internalFormat, int format, int dataType) { * @param format OpenGL format * @param dataType OpenGL datatype * @param compressed Format is compressed + * @param colorRenderable Format can be used as a color render target + * @param depthRenderable Format can be used as a depth render target + * @param filterable Format can be filtered */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = false; } @@ -84,11 +96,47 @@ public GLImageFormat(int internalFormat, int format, int dataType, boolean compr * @param compressed Format is compressed * @param swizzleRequired Need to use texture swizzle to upload texture */ - public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired) { + public GLImageFormat(int internalFormat, int format, int dataType, boolean compressed, boolean swizzleRequired, boolean colorRenderable, boolean depthRenderable, boolean filterable) { this.internalFormat = internalFormat; this.format = format; this.dataType = dataType; this.compressed = compressed; + this.colorRenderable = colorRenderable; + this.depthRenderable = depthRenderable; + this.filterable = filterable; this.swizzleRequired = swizzleRequired; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + GLImageFormat other = (GLImageFormat) obj; + return internalFormat == other.internalFormat + && format == other.format + && dataType == other.dataType + && compressed == other.compressed + && colorRenderable == other.colorRenderable + && depthRenderable == other.depthRenderable + && filterable == other.filterable + && swizzleRequired == other.swizzleRequired; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + this.internalFormat; + hash = 97 * hash + this.format; + hash = 97 * hash + this.dataType; + hash = 97 * hash + (this.compressed ? 1 : 0); + hash = 97 * hash + (this.colorRenderable ? 1 : 0); + hash = 97 * hash + (this.depthRenderable ? 1 : 0); + hash = 97 * hash + (this.filterable ? 1 : 0); + hash = 97 * hash + (this.swizzleRequired ? 1 : 0); + return hash; + } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java index c73943c54e..d7d2dd58c4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java @@ -44,50 +44,66 @@ public final class GLImageFormats { private GLImageFormats() { } - + private static void format(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, colorRenderable, depthRenderable, filterable); } - + private static void formatSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable + ) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, false, colorRenderable, depthRenderable, filterable); } - + private static void formatSrgbSwiz(GLImageFormat[][] formatToGL, Image.Format format, int glInternalFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glInternalFormat, glFormat, glDataType, false, true, colorRenderable, depthRenderable, filterable); } - + private static void formatComp(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType){ - formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[0][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } - + private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format format, int glCompressedFormat, int glFormat, - int glDataType) - { - formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true); + int glDataType, + boolean colorRenderable, + boolean depthRenderable, + boolean filterable) { + formatToGL[1][format.ordinal()] = new GLImageFormat(glCompressedFormat, glFormat, glDataType, true, colorRenderable, depthRenderable, filterable); } /** @@ -103,231 +119,277 @@ private static void formatCompSrgb(GLImageFormat[][] formatToGL, Image.Format fo public static GLImageFormat[][] getFormatsForCaps(EnumSet caps) { GLImageFormat[][] formatToGL = new GLImageFormat[2][Image.Format.values().length]; + boolean opengles = caps.contains(Caps.OpenGLES20); + boolean opengles3 = opengles && caps.contains(Caps.OpenGLES30); + boolean opengles2Only = opengles && !opengles3; + boolean webgl = caps.contains(Caps.WebGL); + boolean opengl = !opengles; + boolean coreProfile = caps.contains(Caps.CoreProfile); + boolean colorRenderableHalfFloatR = caps.contains(Caps.HalfFloatColorBufferR); + boolean colorRenderableHalfFloatRG = caps.contains(Caps.HalfFloatColorBufferRG); + boolean colorRenderableHalfFloatRGB = caps.contains(Caps.HalfFloatColorBufferRGB); + boolean colorRenderableHalfFloatRGBA = caps.contains(Caps.HalfFloatColorBufferRGBA); + boolean colorRenderableFloatR = caps.contains(Caps.FloatColorBufferR); + boolean colorRenderableFloatRG = caps.contains(Caps.FloatColorBufferRG); + boolean colorRenderableFloatRGB = caps.contains(Caps.FloatColorBufferRGB); + boolean colorRenderableFloatRGBA = caps.contains(Caps.FloatColorBufferRGBA); + boolean colorRenderablePackedFloat = caps.contains(Caps.PackedFloatColorBuffer); + boolean filterableHalfFloat = caps.contains(Caps.HalfFloatTextureFilter); + boolean filterableFloat = caps.contains(Caps.FloatTextureFilter); + int halfFloatFormat = GLExt.GL_HALF_FLOAT_ARB; - if (caps.contains(Caps.OpenGLES20)) { + if (opengles2Only) { halfFloatFormat = GLExt.GL_HALF_FLOAT_OES; } // Core Profile Formats (supported by both OpenGL Core 3.3 and OpenGL ES 3.0+) - if (caps.contains(Caps.CoreProfile)) { - formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat); - formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT); - formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); + if (coreProfile) { + formatSwiz(formatToGL, Format.Alpha8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8, GL3.GL_R8, GL.GL_RED, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance8Alpha8, GL3.GL_RG8, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.Luminance16F, GL3.GL_R16F, GL.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + formatSwiz(formatToGL, Format.Luminance32F, GL3.GL_R32F, GL.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + formatSwiz(formatToGL, Format.Luminance16FAlpha16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); - formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE); - formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); + formatSrgbSwiz(formatToGL, Format.Luminance8, GLExt.GL_SRGB8_EXT, GL.GL_RED, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgbSwiz(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SRGB8_ALPHA8_EXT, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); } - if (caps.contains(Caps.OpenGL20)||caps.contains(Caps.OpenGLES30)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.OpenGL20)||opengles3) { + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8, GL2.GL_LUMINANCE8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl || opengles3 || webgl, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (opengles3 || webgl) { + format(formatToGL, Format.RGB565, GLES_30.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); + } else { + format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl || opengles3 || webgl, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB565, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - // Additional desktop-specific formats: - format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); + // Additional desktop-specific formats. + if (opengl) { + format(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + format(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + } // sRGB formats if (caps.contains(Caps.Srgb)) { - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); - formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - if (!caps.contains(Caps.CoreProfile)) { - formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.RGB565, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, opengl, false, true); + formatSrgb(formatToGL, Format.RGB5A1, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, opengl, false, true); + + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + if (!coreProfile) { + formatSrgb(formatToGL, Format.Luminance8, GLExt.GL_SLUMINANCE8_EXT, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, opengl, false, true); + formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, opengl, false, true); + } + if (opengl) { + formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8, true, false, true); + formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE, true, false, true); } - formatSrgb(formatToGL, Format.BGR8, GLExt.GL_SRGB8_EXT, GL2.GL_BGR, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.ABGR8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.ARGB8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL2.GL_UNSIGNED_INT_8_8_8_8); - formatSrgb(formatToGL, Format.BGRA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL2.GL_BGRA, GL.GL_UNSIGNED_BYTE); if (caps.contains(Caps.TextureCompressionS3TC)) { - formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } } } else if (caps.contains(Caps.Rgba8)) { // A more limited form of 32-bit RGBA. Only GL_RGBA8 is available. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GLExt.GL_RGBA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GLExt.GL_RGBA8, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL2.GL_RGB8, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); - formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (opengl) { + formatSwiz(formatToGL, Format.BGR8, GL2.GL_RGB8, GL2.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ARGB8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.BGRA8, GLExt.GL_RGBA8, GL2.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + formatSwiz(formatToGL, Format.ABGR8, GLExt.GL_RGBA8, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } } else { // Actually, the internal format isn't used for OpenGL ES 2! This is the same as the above. - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_RGBA4, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8, GL.GL_RGB565, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, true, false, true); } - format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB8, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, true, false, true); + format(formatToGL, Format.RGBA8, GL.GL_RGBA4, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); } - if (caps.contains(Caps.OpenGLES20)) { - format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5); + if (opengles) { + format(formatToGL, Format.RGB565, GL.GL_RGB565, GL.GL_RGB, GL.GL_UNSIGNED_SHORT_5_6_5, true, false, true); } - format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1); + format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1, true, false, true); - if (caps.contains(Caps.FloatTexture)) { - if (!caps.contains(Caps.CoreProfile)) { - format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat); - format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT); - format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat); + if (caps.contains(Caps.HalfFloatTexture) || caps.contains(Caps.FloatTexture)) { + if (!coreProfile) { + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.Luminance16F, GLExt.GL_LUMINANCE16F_ARB, GL.GL_LUMINANCE, halfFloatFormat, false, false, filterableHalfFloat); + format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat, false, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.Luminance32F, GLExt.GL_LUMINANCE32F_ARB, GL.GL_LUMINANCE, GL.GL_FLOAT, false, false, filterableFloat); + } + } + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat, colorRenderableHalfFloatR, false, filterableHalfFloat); + format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat, colorRenderableHalfFloatRG, false, filterableHalfFloat); + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT, colorRenderableFloatR, false, filterableFloat); + format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT, colorRenderableFloatRG, false, filterableFloat); + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); } - format(formatToGL, Format.R16F, GL3.GL_R16F, GL3.GL_RED, halfFloatFormat); - format(formatToGL, Format.R32F, GL3.GL_R32F, GL3.GL_RED, GL.GL_FLOAT); - format(formatToGL, Format.RG16F, GL3.GL_RG16F, GL3.GL_RG, halfFloatFormat); - format(formatToGL, Format.RG32F, GL3.GL_RG32F, GL3.GL_RG, GL.GL_FLOAT); - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); } if (caps.contains(Caps.PackedFloatTexture)) { - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } if (caps.contains(Caps.SharedExponentTexture)) { - format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT); - if (caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat); + format(formatToGL, Format.RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_5_9_9_9_REV_EXT, false, false, opengl || opengles3); + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F_to_RGB9E5, GLExt.GL_RGB9_E5_EXT, GL.GL_RGB, halfFloatFormat, false, false, opengl || opengles3); } } // Supported in GLES30 core if (caps.contains(Caps.OpenGLES30)) { - format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV); - format(formatToGL, Format.Alpha8, GL2.GL_ALPHA8, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.RGB10A2, GLES_30.GL_RGB10_A2, GL.GL_RGBA, GLES_30.GL_UNSIGNED_INT_2_10_10_10_REV, true, false, true); + if (!coreProfile) { + format(formatToGL, Format.Alpha8, GL.GL_ALPHA, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8, GL.GL_LUMINANCE, GL.GL_LUMINANCE, GL.GL_UNSIGNED_BYTE, false, false, true); + format(formatToGL, Format.Luminance8Alpha8, GL.GL_LUMINANCE_ALPHA, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE, false, false, true); + } - formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + if (caps.contains(Caps.Srgb)) { + formatSrgb(formatToGL, Format.RGB8, GLExt.GL_SRGB8_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatSrgb(formatToGL, Format.RGBA8, GLExt.GL_SRGB8_ALPHA8_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, true, false, true); + } - //Depending on the device could be better to use the previously defined extension based float textures instead of gles3.0 texture formats -// if (!caps.contains(Caps.FloatTexture)) { - format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT); - format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, GLExt.GL_HALF_FLOAT_ARB); - format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT); -// } - format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT); + // Depending on the device, the extension-based definitions above may have already defined these. + if (caps.contains(Caps.HalfFloatTexture)) { + format(formatToGL, Format.RGB16F, GLExt.GL_RGB16F_ARB, GL.GL_RGB, halfFloatFormat, colorRenderableHalfFloatRGB, false, filterableHalfFloat); + format(formatToGL, Format.RGBA16F, GLExt.GL_RGBA16F_ARB, GL.GL_RGBA, halfFloatFormat, colorRenderableHalfFloatRGBA, false, filterableHalfFloat); + } + if (caps.contains(Caps.FloatTexture)) { + format(formatToGL, Format.RGB32F, GLExt.GL_RGB32F_ARB, GL.GL_RGB, GL.GL_FLOAT, colorRenderableFloatRGB, false, filterableFloat); + format(formatToGL, Format.RGBA32F, GLExt.GL_RGBA32F_ARB, GL.GL_RGBA, GL.GL_FLOAT, colorRenderableFloatRGBA, false, filterableFloat); + } + format(formatToGL, Format.RGB111110F, GLExt.GL_R11F_G11F_B10F_EXT, GL.GL_RGB, GLExt.GL_UNSIGNED_INT_10F_11F_11F_REV_EXT, colorRenderablePackedFloat, false, true); } // Need to check whether Caps.DepthTexture is supported before using it for textures. // But for render buffers it's OK. - format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth16, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); if (caps.contains(Caps.WebGL)) { // NOTE: fallback to 24-bit depth as workaround for firefox bug in WebGL 2 where DEPTH_COMPONENT16 is not handled properly - format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.Depth, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } else if (caps.contains(Caps.OpenGLES20)) { // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth. - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, false, true, false); } else { - format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE); + format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE, false, true, false); + } + if (caps.contains(Caps.Depth24)) { + format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } - if (caps.contains(Caps.OpenGLES30) || caps.contains(Caps.OpenGL20) || caps.contains(Caps.Depth24)) { - format(formatToGL, Format.Depth24, GL2.GL_DEPTH_COMPONENT24, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.Depth32)) { + format(formatToGL, Format.Depth32, GL2.GL_DEPTH_COMPONENT32, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, false, true, false); } if (caps.contains(Caps.FloatDepthBuffer)) { - format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT); + format(formatToGL, Format.Depth32F, GLExt.GL_DEPTH_COMPONENT32F, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, false, true, false); } if (caps.contains(Caps.PackedDepthStencilBuffer)) { - format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT); + format(formatToGL, Format.Depth24Stencil8, GLExt.GL_DEPTH24_STENCIL8_EXT, GLExt.GL_DEPTH_STENCIL_EXT, GLExt.GL_UNSIGNED_INT_24_8_EXT, false, true, false); } // Compressed formats if (caps.contains(Caps.TextureCompressionS3TC)) { - formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.DXT1, GLExt.GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT1A, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT3, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.DXT5, GLExt.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)){ - formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE); - formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE); + if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.TextureCompressionRGTC)) { + formatComp(formatToGL, Format.RGTC2, GL3.GL_COMPRESSED_RG_RGTC2, GL3.GL_RG, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC2, GL3.GL_COMPRESSED_SIGNED_RG_RGTC2, GL3.GL_RG, GL.GL_BYTE, false, false, true); + formatComp(formatToGL, Format.RGTC1, GL3.GL_COMPRESSED_RED_RGTC1, GL3.GL_RED, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.SIGNED_RGTC1, GL3.GL_COMPRESSED_SIGNED_RED_RGTC1, GL3.GL_RED, GL.GL_BYTE, false, false, true); } if (caps.contains(Caps.TextureCompressionETC2)) { - formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_RGBA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_RGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); if (caps.contains(Caps.Srgb)) { - formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE); - formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatCompSrgb(formatToGL, Format.ETC2, GLExt.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC2_ALPHA1, GLExt.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, false, false, true); + formatCompSrgb(formatToGL, Format.ETC1, GLExt.GL_COMPRESSED_SRGB8_ETC2, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } } else if (caps.contains(Caps.TextureCompressionETC1)) { - formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); + formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); } - if(caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { - formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE); - formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); - formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.OpenGL42) || caps.contains(Caps.TextureCompressionBPTC)) { + formatComp(formatToGL, Format.BC6H_SF16, GLExt.GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC6H_UF16, GLExt.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM, GLExt.GL_COMPRESSED_RGBA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); + formatComp(formatToGL, Format.BC7_UNORM_SRGB, GLExt.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL.GL_RGBA, GL.GL_UNSIGNED_INT, false, false, true); } // Integer formats - if(caps.contains(Caps.IntegerTexture)) { - format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT); - format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT); + if (caps.contains(Caps.IntegerTexture)) { + format(formatToGL, Format.R8I, GL3.GL_R8I, GL3.GL_RED_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.R8UI, GL3.GL_R8UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.R16I, GL3.GL_R16I, GL3.GL_RED_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.R16UI, GL3.GL_R16UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.R32I, GL3.GL_R32I, GL3.GL_RED_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.R32UI, GL3.GL_R32UI, GL3.GL_RED_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT); - format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RG8I, GL3.GL_RG8I, GL3.GL_RG_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RG8UI, GL3.GL_RG8UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RG16I, GL3.GL_RG16I, GL3.GL_RG_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RG16UI, GL3.GL_RG16UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RG32I, GL3.GL_RG32I, GL3.GL_RG_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RG32UI, GL3.GL_RG32UI, GL3.GL_RG_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); - format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGB8I, GL3.GL_RGB8I, GL3.GL_RGB_INTEGER, GL.GL_BYTE, false, false, false); + format(formatToGL, Format.RGB8UI, GL3.GL_RGB8UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_BYTE, false, false, false); + format(formatToGL, Format.RGB16I, GL3.GL_RGB16I, GL3.GL_RGB_INTEGER, GL.GL_SHORT, false, false, false); + format(formatToGL, Format.RGB16UI, GL3.GL_RGB16UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_SHORT, false, false, false); + format(formatToGL, Format.RGB32I, GL3.GL_RGB32I, GL3.GL_RGB_INTEGER, GL.GL_INT, false, false, false); + format(formatToGL, Format.RGB32UI, GL3.GL_RGB32UI, GL3.GL_RGB_INTEGER, GL.GL_UNSIGNED_INT, false, false, false); - format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE); - format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE); - format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT); - format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT); - format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT); - format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT); + format(formatToGL, Format.RGBA8I, GL3.GL_RGBA8I, GL3.GL_RGBA_INTEGER, GL.GL_BYTE, true, false, false); + format(formatToGL, Format.RGBA8UI, GL3.GL_RGBA8UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_BYTE, true, false, false); + format(formatToGL, Format.RGBA16I, GL3.GL_RGBA16I, GL3.GL_RGBA_INTEGER, GL.GL_SHORT, true, false, false); + format(formatToGL, Format.RGBA16UI, GL3.GL_RGBA16UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_SHORT, true, false, false); + format(formatToGL, Format.RGBA32I, GL3.GL_RGBA32I, GL3.GL_RGBA_INTEGER, GL.GL_INT, true, false, false); + format(formatToGL, Format.RGBA32UI, GL3.GL_RGBA32UI, GL3.GL_RGBA_INTEGER, GL.GL_UNSIGNED_INT, true, false, false); } return formatToGL; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index 59927fc3aa..b7d11f9ba6 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -62,6 +62,7 @@ import com.jme3.texture.Texture.ShadowCompareMode; import com.jme3.texture.Texture.WrapAxis; import com.jme3.texture.TextureImage; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.LastTextureState; import com.jme3.util.BufferUtils; import com.jme3.util.ListMap; @@ -410,29 +411,35 @@ private void loadCapabilitiesCommon() { // == texture format extensions == - boolean hasFloatTexture; + boolean coreFloatTextures = caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) + || caps.contains(Caps.WebGL); + boolean arbFloatTextures = hasExtension("GL_ARB_texture_float"); + boolean hasFloatTexture = coreFloatTextures || arbFloatTextures || hasExtension("GL_OES_texture_float"); + boolean hasHalfFloatTexture = coreFloatTextures || hasExtension("GL_OES_texture_half_float") + || (arbFloatTextures && hasExtension("GL_ARB_half_float_pixel")); - hasFloatTexture = hasExtension("GL_OES_texture_half_float") && - hasExtension("GL_OES_texture_float"); + if (hasFloatTexture) { + caps.add(Caps.FloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = hasExtension("GL_ARB_texture_float") && - hasExtension("GL_ARB_half_float_pixel"); + if (hasHalfFloatTexture) { + caps.add(Caps.HalfFloatTexture); + } - if (!hasFloatTexture) { - hasFloatTexture = caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL); - } + if (hasFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_float_linear"))) { + caps.add(Caps.FloatTextureFilter); } - if (hasFloatTexture) { - caps.add(Caps.FloatTexture); + if (hasHalfFloatTexture && (caps.contains(Caps.OpenGL30) || hasExtension("GL_OES_texture_half_float_linear"))) { + caps.add(Caps.HalfFloatTextureFilter); } // integer texture format extensions - if(hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) - || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) + if (hasExtension("GL_EXT_texture_integer") || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { caps.add(Caps.IntegerTexture); + } if (hasExtension("GL_OES_depth_texture") || hasExtension("WEBGL_depth_texture") || gl2 != null || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { @@ -444,6 +451,10 @@ private void loadCapabilitiesCommon() { caps.add(Caps.Depth24); } + if (caps.contains(Caps.OpenGL20) || hasExtension("GL_OES_depth32")) { + caps.add(Caps.Depth32); + } + if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) || hasExtension("GL_OES_rgb8_rgba8") || hasExtension("GL_ARM_rgba8") || @@ -452,20 +463,32 @@ private void loadCapabilitiesCommon() { } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) - || hasExtension("GL_OES_packed_depth_stencil")) { + || hasAnyExtension("GL_OES_packed_depth_stencil", "GL_EXT_packed_depth_stencil")) { caps.add(Caps.PackedDepthStencilBuffer); } - if (hasExtension("GL_ARB_color_buffer_float") && - hasExtension("GL_ARB_half_float_pixel") - ||caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) - || caps.contains(Caps.WebGL)) { - // XXX: Require both 16- and 32-bit float support for FloatColorBuffer. + boolean hasDesktopFloatColorBuffer = (hasExtension("GL_ARB_color_buffer_float") + && hasExtension("GL_ARB_texture_float") + && hasExtension("GL_ARB_half_float_pixel")) + || caps.contains(Caps.OpenGL30); + boolean hasExtFloatColorBuffer = hasExtension("GL_EXT_color_buffer_float"); + boolean hasExtHalfFloatColorBuffer = hasExtension("GL_EXT_color_buffer_half_float"); + + if (hasDesktopFloatColorBuffer || hasExtFloatColorBuffer) { caps.add(Caps.FloatColorBuffer); + caps.add(Caps.FloatColorBufferR); + caps.add(Caps.FloatColorBufferRG); caps.add(Caps.FloatColorBufferRGBA); - if (!caps.contains(Caps.OpenGLES30) && !caps.contains(Caps.WebGL)) { - caps.add(Caps.FloatColorBufferRGB); - } + caps.add(Caps.HalfFloatColorBufferR); + caps.add(Caps.HalfFloatColorBufferRG); + caps.add(Caps.HalfFloatColorBufferRGBA); + } else if (hasExtHalfFloatColorBuffer && hasHalfFloatTexture) { + caps.add(Caps.HalfFloatColorBufferRGBA); + } + + if (hasDesktopFloatColorBuffer) { + caps.add(Caps.FloatColorBufferRGB); + caps.add(Caps.HalfFloatColorBufferRGB); } if (caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) @@ -473,15 +496,15 @@ private void loadCapabilitiesCommon() { caps.add(Caps.FloatDepthBuffer); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30) || caps.contains(Caps.OpenGLES30) + if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) + || caps.contains(Caps.OpenGL30) + || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL)) { - // Either GL3/GLES3 is available or both packed_float & half_float_pixel. caps.add(Caps.PackedFloatTexture); } - if ((hasExtension("GL_EXT_packed_float") && hasFloatTexture) || - caps.contains(Caps.OpenGL30)) { + if ((hasExtension("GL_EXT_packed_float") && hasDesktopFloatColorBuffer) + || caps.contains(Caps.OpenGL30) || hasExtFloatColorBuffer) { caps.add(Caps.PackedFloatColorBuffer); } @@ -551,7 +574,12 @@ private void loadCapabilitiesCommon() { if (hasExtension("GL_EXT_texture_filter_anisotropic")) { caps.add(Caps.TextureFilterAnisotropic); - limits.put(Limits.TextureAnisotropy, getInteger(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT)); + floatBuf16.clear(); + gl.glGetFloat(GLExt.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, floatBuf16); + limits.put(Limits.TextureAnisotropy, + Math.max(1, Math.round(floatBuf16.get(0)))); + } else { + limits.put(Limits.TextureAnisotropy, 1); } if (hasExtension("GL_EXT_framebuffer_object") @@ -619,6 +647,11 @@ private void loadCapabilitiesCommon() { || caps.contains(Caps.WebGL)) { caps.add(Caps.Srgb); } + if (hasExtension("GL_ARB_framebuffer_sRGB") + || caps.contains(Caps.OpenGL30) + || hasExtension("GL_EXT_sRGB_write_control")) { + caps.add(Caps.SrgbWriteControl); + } // Supports seamless cubemap if (hasExtension("GL_ARB_seamless_cube_map") || caps.contains(Caps.OpenGL32)) { @@ -647,7 +680,8 @@ private void loadCapabilitiesCommon() { caps.add(Caps.TesselationShader); } - if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) || caps.contains(Caps.OpenGLES31)) { + if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) + || caps.contains(Caps.OpenGLES31)) { caps.add(Caps.ShaderStorageBufferObject); limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE)); @@ -778,6 +812,29 @@ private void bindUniformBlock(int program, int uniformBlockIndex, int uniformBlo } } + private int getProgramResourceIndex(int program, int programInterface, String name) { + if (gl4 != null) { + return gl4.glGetProgramResourceIndex(program, programInterface, name); + } + return glext.glGetProgramResourceIndex(program, programInterface, name); + } + + private void bindShaderStorageBufferBase(int bindingPoint, int buffer) { + if (gl4 != null) { + gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } else { + glext.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, buffer); + } + } + + private void bindShaderStorageBlock(int program, int storageBlockIndex, int storageBlockBinding) { + if (gl4 != null) { + gl4.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } else { + glext.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + } + @SuppressWarnings("fallthrough") @Override public void initialize() { @@ -1583,11 +1640,11 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl if (bufferBlock.isUpdateNeeded() ) { int blockIndex = bufferBlock.getLocation(); if (blockIndex < 0) { - blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); + blockIndex = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); bufferBlock.setLocation(blockIndex); } if (bufferBlock.getLocation() != NativeObject.INVALID_ID) { - gl4.glShaderStorageBlockBinding(shaderId, bufferBlock.getLocation(), bindingPoint); + bindShaderStorageBlock(shaderId, bufferBlock.getLocation(), bindingPoint); } } break; @@ -2078,7 +2135,7 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { // Check NPOT requirements checkNonPowerOfTwo(tex); - updateTexImageData(image, tex.getType(), 0, false); + updateTexImageData(image, tex.getType(), 0, false, false); // NOTE: For depth textures, sets nearest/no-mips mode // Required to fix "framebuffer unsupported" @@ -2102,6 +2159,19 @@ public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) { } public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { + Image.Format format = rb.getFormat(); + boolean depthTarget = rb.getSlot() == FrameBuffer.SLOT_DEPTH + || rb.getSlot() == FrameBuffer.SLOT_DEPTH_STENCIL; + boolean srgb = !depthTarget && fb.isSrgb(); + GLImageFormat glFormat = texUtil.getImageFormatWithError(format, srgb); + if (!depthTarget && !glFormat.colorRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not color-renderable and cannot be used as a color attachment."); + } else if (depthTarget && !glFormat.depthRenderable) { + throw new RendererException("Framebuffer format " + format + + " is not depth-renderable and cannot be used as a depth attachment."); + } + boolean needAttach; if (rb.getTexture() == null) { // if it hasn't been created yet, then attach is required. @@ -2144,14 +2214,14 @@ private void toggleFramebufferSrgb(FrameBuffer fb) { boolean isSrgb = fb == null ? mainFrameBufferSrgb : fb.isSrgb(); if (isSrgb != context.srgbWriteEnabled) { - if (caps.contains(Caps.Srgb)) { + if (caps.contains(Caps.SrgbWriteControl) && caps.contains(Caps.Srgb)) { if (isSrgb) { gl.glEnable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } else { gl.glDisable(GLExt.GL_FRAMEBUFFER_SRGB_EXT); } - context.srgbWriteEnabled = isSrgb; } + context.srgbWriteEnabled = isSrgb; } } @@ -2299,11 +2369,17 @@ public void setFrameBuffer(FrameBuffer fb) { } // generate mipmaps for last FB if needed - if (context.boundFB != null && (context.boundFB.getMipMapsGenerationHint()!=null?context.boundFB.getMipMapsGenerationHint():generateMipmapsForFramebuffers)) { - for (int i = 0; i < context.boundFB.getNumColorBuffers(); i++) { - RenderBuffer rb = context.boundFB.getColorBuffer(i); + FrameBuffer boundFB = context.boundFB; + if (boundFB != null && (boundFB.getMipMapsGenerationHint() != null + ? boundFB.getMipMapsGenerationHint() + : generateMipmapsForFramebuffers)) { + for (int i = 0; i < boundFB.getNumColorBuffers(); i++) { + RenderBuffer rb = boundFB.getColorBuffer(i); Texture tex = rb.getTexture(); - if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + if (tex != null && tex.getMinFilter().usesMipMapLevels() + && isMipmapGenerationSupported(tex.getImage().getFormat(), + linearizeSrgbImages && boundFB.isSrgb() + ? ColorSpace.sRGB : ColorSpace.Linear)) { try { final int textureUnitIndex = 0; setTexture(textureUnitIndex, rb.getTexture()); @@ -2316,6 +2392,9 @@ public void setFrameBuffer(FrameBuffer fb) { int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); glfbo.glGenerateMipmapEXT(textureType); } + } else if (tex != null && tex.getMinFilter().usesMipMapLevels()) { + logger.warning("Cannot generate mipmaps for framebuffer texture: " + tex + + " with image format: " + tex.getImage().getFormat()); } } } @@ -2519,7 +2598,11 @@ private void setupTextureParams(int unit, Texture tex) { boolean haveMips = true; if (image != null) { - haveMips = image.isGeneratedMipmapsRequired() || image.hasMipmaps(); + haveMips = image.hasMipmaps() + || image.isMipmapsGenerated() + || (image.isGeneratedMipmapsRequired() + && isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)); } LastTextureState curState = image.getLastTextureState(); @@ -2529,10 +2612,12 @@ private void setupTextureParams(int unit, Texture tex) { gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, convertMagFilter(tex.getMagFilter())); curState.magFilter = tex.getMagFilter(); } - if (curState.minFilter != tex.getMinFilter()) { + if (curState.minFilter != tex.getMinFilter() + || curState.minFilterMipmapsAvailable != haveMips) { bindTextureAndUnit(target, image, unit); gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, convertMinFilter(tex.getMinFilter(), haveMips)); curState.minFilter = tex.getMinFilter(); + curState.minFilterMipmapsAvailable = haveMips; } int desiredAnisoFilter = tex.getAnisotropicFilter() == 0 @@ -2703,7 +2788,13 @@ private void bindTextureOnly(int target, Image img, int unit) { * before being uploaded. */ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot) { + updateTexImageData(img, type, unit, scaleToPot, true); + } + + private void updateTexImageData(Image img, Texture.Type type, int unit, boolean scaleToPot, + boolean allowCpuMipmapFallback) { int texId = img.getId(); + boolean textureWasUnuploaded = texId == -1; if (texId == -1) { // create texture gl.glGenTextures(intBuf1); @@ -2719,29 +2810,79 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s bindTextureAndUnit(target, img, unit); int imageSamples = img.getMultiSamples(); + boolean sourceMipmapsUsable = img.hasMipmaps() && !scaleToPot; + boolean needsMipmaps = !sourceMipmapsUsable && img.isGeneratedMipmapsRequired(); + boolean hwMipmapSupported = needsMipmaps && isMipmapGenerationSupported(img.getFormat(), + linearizeSrgbImages ? img.getColorSpace() : ColorSpace.Linear); + Image imageForUpload = img; + boolean cpuMipmapsGenerated = false; if (imageSamples <= 1) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired()) { - // Image does not have mipmaps, but they are required. - // Generate from base level. + boolean cpuMipmapFallbackFailed = false; + if (needsMipmaps) { + /* + * Some formats cannot use glGenerateMipmap because they are not both + * renderable and filterable. On a first upload, fall back to a CPU-built + * mip chain when the image data is suitable. If NPOT scaling is also + * required, build the CPU mips from the resized upload image. + */ + boolean needsCpuMipmapFallback = !hwMipmapSupported + && allowCpuMipmapFallback + && textureWasUnuploaded + && MipMapGenerator.canGenerateMipmaps(img); + if (needsCpuMipmapFallback) { + try { + Image cpuMipmapUploadImage = cloneImageForUpload(img, scaleToPot); + if (cpuMipmapUploadImage != null) { + MipMapGenerator.generateMipMaps(cpuMipmapUploadImage, linearizeSrgbImages, + img.getColorSpace() == ColorSpace.sRGB); + imageForUpload = cpuMipmapUploadImage; + cpuMipmapsGenerated = true; + scaleToPot = false; + img.setMipmapsGenerated(true); + } + } catch (RuntimeException exception) { + cpuMipmapFallbackFailed = true; + logger.log(Level.WARNING, + "Texture " + img + " requires mipmaps, but hardware mipmap generation is not supported" + + " and CPU mipmap generation failed. Mipmaps will not be generated.", + exception); + } + } - if (!caps.contains(Caps.FrameBuffer) && gl2 != null) { + /* + * Old desktop GL without FBO support can auto-generate mipmaps during + * texture upload. Newer paths generate explicitly after upload below. + */ + if (hwMipmapSupported && !caps.contains(Caps.FrameBuffer) && gl2 != null) { gl2.glTexParameteri(target, GL2.GL_GENERATE_MIPMAP, GL.GL_TRUE); img.setMipmapsGenerated(true); - } else { - // For OpenGL3 and up. - // We'll generate mipmaps via glGenerateMipmapEXT (see below) } - } else if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30)) { - if (img.hasMipmaps()) { - // Image already has mipmaps, set the max level based on the - // number of mipmaps we have. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, img.getMipMapSizes().length - 1); - } else { - // Image does not have mipmaps, and they are not required. - // Specify that the texture has no mipmaps. - gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, 0); + + if (!hwMipmapSupported + && !sourceMipmapsUsable + && !cpuMipmapsGenerated + && !cpuMipmapFallbackFailed) { + logger.log(Level.WARNING, "Texture " + img + " requires mipmaps, but hardware mipmaps generation is not supported. Mipmaps will not be generated."); } } + + /* + * Clamp the mip range to the levels actually uploaded. This is still + * needed when mipmaps are not requested, otherwise GL may sample + * missing levels left from a previous texture state. When hardware + * mipmap generation is pending, reopen the full generated range in + * case an earlier upload clamped this texture to the base level. + */ + boolean canSetTextureMaxLevel = caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30); + boolean hasUploadMipmaps = sourceMipmapsUsable || cpuMipmapsGenerated; + int uploadWidth = scaleToPot ? FastMath.nearestPowerOfTwo(img.getWidth()) : imageForUpload.getWidth(); + int uploadHeight = scaleToPot ? FastMath.nearestPowerOfTwo(img.getHeight()) : imageForUpload.getHeight(); + int maxLevel = textureMaxLevelForUpload(canSetTextureMaxLevel, needsMipmaps, hwMipmapSupported, + hasUploadMipmaps, cpuMipmapsGenerated ? imageForUpload.getMipMapSizes() : img.getMipMapSizes(), + generatedMipMaxLevel(uploadWidth, uploadHeight, imageForUpload.getDepth())); + if (maxLevel >= 0) { + gl.glTexParameteri(target, GL2.GL_TEXTURE_MAX_LEVEL, maxLevel); + } } else { // Check if graphics card doesn't support multisample textures if (!caps.contains(Caps.TextureMultisample)) { @@ -2782,11 +2923,8 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s } } - Image imageForUpload; if (scaleToPot) { imageForUpload = MipMapGenerator.resizeToPowerOf2(img); - } else { - imageForUpload = img; } if (target == GL.GL_TEXTURE_CUBE_MAP) { List data = imageForUpload.getData(); @@ -2821,16 +2959,80 @@ public void updateTexImageData(Image img, Texture.Type type, int unit, boolean s img.setMultiSamples(imageSamples); } - if (caps.contains(Caps.FrameBuffer) || gl2 == null) { - if (!img.hasMipmaps() && img.isGeneratedMipmapsRequired() && img.getData(0) != null) { - glfbo.glGenerateMipmapEXT(target); - img.setMipmapsGenerated(true); - } + if (needsMipmaps && hwMipmapSupported + && (caps.contains(Caps.FrameBuffer) || gl2 == null) + && img.getData(0) != null + && !img.isMipmapsGenerated()) { + glfbo.glGenerateMipmapEXT(target); + img.setMipmapsGenerated(true); } img.clearUpdateNeeded(); } + private boolean isMipmapGenerationSupported(Image.Format format, ColorSpace colorSpace) { + GLImageFormat gf = texUtil.getImageFormat(format, colorSpace == ColorSpace.sRGB); + return gf != null && gf.colorRenderable && gf.filterable; + } + + static int textureMaxLevelForUpload(boolean canSetTextureMaxLevel, + boolean needsMipmaps, + boolean hwMipmapSupported, + boolean hasUploadMipmaps, + int[] uploadMipMapSizes, + int generatedMipMaxLevel) { + if (!canSetTextureMaxLevel) { + return -1; + } + if (needsMipmaps && hwMipmapSupported) { + return generatedMipMaxLevel; + } + if (!hasUploadMipmaps) { + return 0; + } + return uploadMipMapSizes.length - 1; + } + + static int generatedMipMaxLevel(int width, int height, int depth) { + int maxDimension = Math.max(Math.max(width, height), Math.max(1, depth)); + int maxLevel = 0; + while (maxDimension > 1) { + maxDimension >>= 1; + maxLevel++; + } + return maxLevel; + } + + private Image cloneImageForUpload(Image image, boolean scaleToPot) { + if (scaleToPot) { + return MipMapGenerator.resizeToPowerOf2(image); + } + + ArrayList data = new ArrayList<>(image.getData().size()); + for (ByteBuffer buffer : image.getData()) { + if (buffer == null) { + return null; + } + data.add(buffer.duplicate()); + } + return new Image(image.getFormat(), image.getWidth(), image.getHeight(), image.getDepth(), + data, null, image.getColorSpace()); + } + + private boolean needsGeneratedMipmaps(Image image) { + if (!image.isGeneratedMipmapsRequired() || image.isMipmapsGenerated()) { + return false; + } + + if (isMipmapGenerationSupported(image.getFormat(), + linearizeSrgbImages ? image.getColorSpace() : ColorSpace.Linear)) { + return true; + } + + return image.getId() == -1 + && MipMapGenerator.canGenerateMipmaps(image); + } + @Override public void setTexture(int unit, Texture tex) throws TextureUnitException { if (unit < 0 || unit >= RenderContext.maxTextureUnits) { @@ -2838,7 +3040,7 @@ public void setTexture(int unit, Texture tex) throws TextureUnitException { } Image image = tex.getImage(); - if (image.isUpdateNeeded() || (image.isGeneratedMipmapsRequired() && !image.isMipmapsGenerated())) { + if (image.isUpdateNeeded() || needsGeneratedMipmaps(image)) { // Check NPOT requirements boolean scaleToPot = false; @@ -2907,7 +3109,7 @@ public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferOb updateShaderStorageBufferObjectData(bufferObject); } if (context.boundBO[bindingPoint] == null || context.boundBO[bindingPoint].get() != bufferObject) { - gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bindingPoint, bufferObject.getId()); + bindShaderStorageBufferBase(bindingPoint, bufferObject.getId()); bufferObject.setBinding(bindingPoint); context.boundBO[bindingPoint] = bufferObject.getWeakRef(); } @@ -3157,14 +3359,14 @@ private void updateBufferData(int type, BufferObject bo) { BufferRegion reg; while ((reg = it.next()) != null) { - gl3.glBindBuffer(type, bufferId); + gl.glBindBuffer(type, bufferId); if (reg.isFullBufferRegion()) { ByteBuffer bbf = bo.getData(); if (logger.isLoggable(java.util.logging.Level.FINER)) { logger.log(java.util.logging.Level.FINER, "Update full buffer {0} with {1} bytes", new Object[] { bo, bbf.remaining() }); } gl.glBufferData(type, bbf, usage); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); break; } else { @@ -3172,7 +3374,7 @@ private void updateBufferData(int type, BufferObject bo) { logger.log(java.util.logging.Level.FINER, "Update region {0} of {1}", new Object[] { reg, bo }); } gl.glBufferSubData(type, reg.getStart(), reg.getData()); - gl3.glBindBuffer(type, 0); + gl.glBindBuffer(type, 0); reg.clearDirty(); } } @@ -3593,7 +3795,7 @@ public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceDat @Override public void setMainFrameBufferSrgb(boolean enableSrgb) { // Gamma correction - if (!caps.contains(Caps.Srgb) && enableSrgb) { + if ((!caps.contains(Caps.SrgbWriteControl) || !caps.contains(Caps.Srgb)) && enableSrgb) { // Not supported, sorry. logger.warning("sRGB framebuffer is not supported " + "by video hardware, but was requested."); @@ -3699,7 +3901,7 @@ public boolean isLinearizeSrgbImages() { */ @Override public boolean isMainFrameBufferSrgb() { - if (!caps.contains(Caps.Srgb)) { + if (!caps.contains(Caps.Srgb) || !caps.contains(Caps.SrgbWriteControl)) { return false; } else { return mainFrameBufferSrgb; diff --git a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java index f3cc721df9..8f696f4b49 100644 --- a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java +++ b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java @@ -849,8 +849,9 @@ public long getUniqueId() { * * The FrameBuffer must have an SRGB texture attached. * - * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} capability - * for this option to take any effect. + * The Renderer must expose the {@link Caps#Srgb sRGB pipeline} and + * {@link Caps#SrgbWriteControl sRGB write control} capabilities for this + * option to take any effect. * * Rendering operations performed on this framebuffer shall undergo a linear * -> sRGB color space conversion when this flag is enabled. If diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3159f0856a..3181abd4f9 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -79,16 +79,16 @@ public enum Format { Reserved2(0), /** - * half-precision floating-point grayscale/luminance. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16F(16,true), /** * single-precision floating-point grayscale/luminance. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#FloatTexture}. */ Luminance32F(32,true), @@ -101,9 +101,9 @@ public enum Format { Reserved3(0), /** - * half-precision floating-point grayscale/luminance and alpha. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating-point grayscale/luminance and alpha. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16FAlpha16F(32,true), @@ -255,7 +255,7 @@ public enum Format { * but will be converted to {@link Format#RGB111110F} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#PackedFloatTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}. */ RGB16F_to_RGB111110F(48,true), @@ -271,7 +271,7 @@ public enum Format { * but will be converted to {@link Format#RGB9E5} when sent * to the video hardware. * - * Requires {@link Caps#FloatTexture} and {@link Caps#SharedExponentTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}. */ RGB16F_to_RGB9E5(48,true), @@ -283,9 +283,9 @@ public enum Format { RGB9E5(32,true), /** - * half-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * half-precision floating point red, green, and blue. + * + * Requires {@link Caps#HalfFloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB16F(48,true), @@ -293,22 +293,22 @@ public enum Format { /** * half-precision floating point red, green, blue, and alpha. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RGBA16F(64,true), /** - * single-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, and blue. + * + * Requires {@link Caps#FloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB32F(96,true), /** - * single-precision floating point red, green, blue and alpha. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, blue and alpha. + * + * Requires {@link Caps#FloatTexture}. */ RGBA32F(128,true), @@ -507,21 +507,21 @@ public enum Format { /** * half-precision floating point red. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ R16F(16,true), /** - * single-precision floating point red. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. */ R32F(32,true), /** * half-precision floating point red and green. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RG16F(32,true), diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java index 0fe86270d8..e329f21a1f 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageCodec.java @@ -172,11 +172,15 @@ public ImageCodec(int bpp, int flags, int maxAlpha, int maxRed, int maxGreen, in * @param format The format to lookup. * @return The codec capable of decoding it, or null if not found. */ - public static ImageCodec lookup(Format format) { - ImageCodec codec = params.get(format); - if (codec == null) { - throw new UnsupportedOperationException("The format " + format + " is not supported"); - } - return codec; - } -} + public static ImageCodec lookup(Format format) { + ImageCodec codec = params.get(format); + if (codec == null) { + throw new UnsupportedOperationException("The format " + format + " is not supported"); + } + return codec; + } + + static boolean isSupported(Format format) { + return params.containsKey(format); + } +} diff --git a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java index 1a75203225..18b21d364b 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/ImageRaster.java @@ -62,11 +62,22 @@ * * @author Kirill Vainer */ -public abstract class ImageRaster { - - /** - * Create new image reader / writer. - * +public abstract class ImageRaster { + + /** + * Tests whether {@link ImageRaster} can read and write pixels for the + * specified image format. + * + * @param format the image format to test + * @return true if ImageRaster supports the format + */ + public static boolean isSupported(Image.Format format) { + return ImageCodec.isSupported(format); + } + + /** + * Create new image reader / writer. + * * @param image The image to read / write to. * @param slice Which slice to use. Only applies to 3D images, 2D image * arrays or cubemaps. diff --git a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java index 7a08e1cdde..7c49782058 100644 --- a/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java +++ b/jme3-core/src/main/java/com/jme3/texture/image/LastTextureState.java @@ -45,6 +45,7 @@ public final class LastTextureState { public Texture.WrapMode sWrap, tWrap, rWrap; public Texture.MagFilter magFilter; public Texture.MinFilter minFilter; + public boolean minFilterMipmapsAvailable; public int anisoFilter; public Texture.ShadowCompareMode shadowCompareMode; @@ -58,6 +59,7 @@ public void reset() { rWrap = null; magFilter = null; minFilter = null; + minFilterMipmapsAvailable = false; anisoFilter = 1; // The default in OpenGL is OFF, so we avoid setting this per texture diff --git a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java index 7073dfab52..9e8ac4bbd0 100644 --- a/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java +++ b/jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,99 +34,605 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; import com.jme3.texture.image.ImageRaster; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Locale; -public class MipMapGenerator { +public final class MipMapGenerator { + + private static final float EPSILON_ALPHA = 1e-8f; private MipMapGenerator() { } + /** + * Scales the base level of a 2D image. + * + * The returned image keeps the same Image.Format and ColorSpace as the input. + * Pixel format conversion is delegated to ImageRaster. + * + * For normal color textures, this method filters in linear space. + */ public static Image scaleImage(Image inputImage, int outputWidth, int outputHeight) { - int size = outputWidth * outputHeight * inputImage.getFormat().getBitsPerPixel() / 8; - ByteBuffer buffer = BufferUtils.createByteBuffer(size); - Image outputImage = new Image(inputImage.getFormat(), - outputWidth, - outputHeight, - buffer, - inputImage.getColorSpace()); - - ImageRaster input = ImageRaster.create(inputImage, 0, 0, false); - ImageRaster output = ImageRaster.create(outputImage, 0, 0, false); - - float xRatio = ((float) (input.getWidth() - 1)) / output.getWidth(); - float yRatio = ((float) (input.getHeight() - 1)) / output.getHeight(); - - ColorRGBA outputColor = new ColorRGBA(0, 0, 0, 0); - ColorRGBA bottomLeft = new ColorRGBA(); - ColorRGBA bottomRight = new ColorRGBA(); - ColorRGBA topLeft = new ColorRGBA(); - ColorRGBA topRight = new ColorRGBA(); - - for (int y = 0; y < outputHeight; y++) { - for (int x = 0; x < outputWidth; x++) { - float x2f = x * xRatio; - float y2f = y * yRatio; - - int x2 = (int) x2f; - int y2 = (int) y2f; - - input.getPixel(x2, y2, bottomLeft); - input.getPixel(x2 + 1, y2, bottomRight); - input.getPixel(x2, y2 + 1, topLeft); - input.getPixel(x2 + 1, y2 + 1, topRight); - - outputColor.set(bottomLeft).addLocal(bottomRight) - .addLocal(topLeft).addLocal(topRight); - outputColor.multLocal(1f / 4f); - output.setPixel(x, y, outputColor); - } - } - return outputImage; + return scaleImage(inputImage, outputWidth, outputHeight, true, isSrgb(inputImage)); + } + + /** + * Scales the base level of a 2D image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha to reduce transparent-edge halos + */ + public static Image scaleImage(Image inputImage, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + return scaleLevel(inputImage, 0, outputWidth, outputHeight, convertToLinear, alphaWeighted); } - public static Image resizeToPowerOf2(Image original){ + public static Image resizeToPowerOf2(Image original) { int potWidth = FastMath.nearestPowerOfTwo(original.getWidth()); int potHeight = FastMath.nearestPowerOfTwo(original.getHeight()); return scaleImage(original, potWidth, potHeight); } - public static void generateMipMaps(Image image){ - int width = image.getWidth(); - int height = image.getHeight(); + /** + * Returns true if this image has CPU-side data in a format supported by this + * mipmap generator. + * + * This generator works on uncompressed, non-depth, byte-addressable texture + * formats supported by ImageRaster. + */ + public static boolean canGenerateMipmaps(Image image) { + if (image == null + || image.getWidth() < 1 + || image.getHeight() < 1 + || image.getDepth() > 1 + || image.getFormat().isCompressed() + || image.getFormat().isDepthFormat() + || image.getData() == null + || image.getData().isEmpty()) { + return false; + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + return false; + } + + int baseLevelSize; + try { + baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + } catch (RuntimeException exception) { + return false; + } + + for (ByteBuffer data : image.getData()) { + if (data == null || data.capacity() < baseLevelSize) { + return false; + } + } + + return ImageRaster.isSupported(image.getFormat()); + } + + /** + * Generates a complete mip chain for the image. + * + * Default behavior is intended for normal color/albedo textures: + * - filtering is done in linear space; + * - sRGB images with alpha use alpha-weighted RGB filtering. + * + * For normal maps, roughness, metallic, AO, height maps, or packed data maps, + * prefer generateMipMaps(image, true, false), assuming the image is not marked sRGB. + */ + public static void generateMipMaps(Image image) { + generateMipMaps(image, true, isSrgb(image)); + } + + /** + * Generates a complete mip chain for every data buffer/slice in the image. + * + * @param convertToLinear if true, ImageRaster exposes pixels to this code in linear space + * @param alphaWeighted if true, RGB is filtered weighted by alpha + */ + public static void generateMipMaps(Image image, boolean convertToLinear, boolean alphaWeighted) { + validateImage(image); + + int baseWidth = image.getWidth(); + int baseHeight = image.getHeight(); + + ArrayList chains = new ArrayList<>(image.getData().size()); + int dataCount = image.getData().size(); + + for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { + chains.add(generateMipChainForSlice( + image, + dataIndex, + baseWidth, + baseHeight, + convertToLinear, + alphaWeighted + )); + } + + for (int dataIndex = 0; dataIndex < chains.size(); dataIndex++) { + image.setData(dataIndex, chains.get(dataIndex).combinedData); + } + + if (!chains.isEmpty()) { + image.setMipMapSizes(chains.get(0).mipSizes); + } + } + + private static MipChain generateMipChainForSlice(Image sourceImage, + int sourceSlice, + int baseWidth, + int baseHeight, + boolean convertToLinear, + boolean alphaWeighted) { + ArrayList levels = new ArrayList<>(); + + Image.Format format = sourceImage.getFormat(); + ColorSpace colorSpace = sourceImage.getColorSpace(); + + ByteBuffer baseLevel = copyBaseLevel( + sourceImage.getData(sourceSlice), + levelSize(format, baseWidth, baseHeight) + ); + + Image current = new Image(format, baseWidth, baseHeight, baseLevel, colorSpace); + levels.add(baseLevel); + + int width = baseWidth; + int height = baseHeight; + + while (width > 1 || height > 1) { + int nextWidth = Math.max(1, width / 2); + int nextHeight = Math.max(1, height / 2); + + Image next = scaleLevel( + current, + 0, + nextWidth, + nextHeight, + convertToLinear, + alphaWeighted + ); + + levels.add(next.getData(0)); + + current = next; + width = nextWidth; + height = nextHeight; + } - Image current = image; - ArrayList output = new ArrayList<>(); int totalSize = 0; + int[] mipSizes = new int[levels.size()]; + + for (int i = 0; i < levels.size(); i++) { + int size = levels.get(i).capacity(); + mipSizes[i] = size; + totalSize += size; + } - while (height >= 1 || width >= 1){ - output.add(current.getData(0)); - totalSize += current.getData(0).capacity(); + ByteBuffer combined = BufferUtils.createByteBuffer(totalSize); - if (height == 1 || width == 1) { - break; + for (ByteBuffer level : levels) { + ByteBuffer duplicate = level.duplicate(); + duplicate.clear(); + combined.put(duplicate); + } + + combined.flip(); + + return new MipChain(combined, mipSizes); + } + + private static Image scaleLevel(Image inputImage, + int inputSlice, + int outputWidth, + int outputHeight, + boolean convertToLinear, + boolean alphaWeighted) { + if (outputWidth < 1 || outputHeight < 1) { + throw new IllegalArgumentException("Output size must be at least 1x1"); + } + + validateImage(inputImage); + + int outputSize = levelSize(inputImage.getFormat(), outputWidth, outputHeight); + ByteBuffer outputBuffer = BufferUtils.createByteBuffer(outputSize); + + Image outputImage = new Image( + inputImage.getFormat(), + outputWidth, + outputHeight, + outputBuffer, + inputImage.getColorSpace() + ); + + ImageRaster input = ImageRaster.create(inputImage, inputSlice, 0, convertToLinear); + ImageRaster output = ImageRaster.create(outputImage, 0, 0, convertToLinear); + + boolean downscale = outputWidth <= input.getWidth() && outputHeight <= input.getHeight(); + boolean clampOutput = !inputImage.getFormat().isFloatingPont(); + + if (downscale) { + areaResample(input, output, alphaWeighted, clampOutput); + } else { + bilinearResample(input, output, alphaWeighted, clampOutput); + } + + return outputImage; + } + + /** + * Area filter. + * + * This is the right default for mipmap generation because every destination + * pixel represents the average area of the corresponding source rectangle. + */ + private static void areaResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); + + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); + + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY0 = y * scaleY; + double sourceY1 = (y + 1) * scaleY; + + int yStart = Math.max(0, (int) Math.floor(sourceY0)); + int yEnd = Math.min(sourceHeight, Math.max(yStart + 1, (int) Math.ceil(sourceY1))); + + for (int x = 0; x < targetWidth; x++) { + double sourceX0 = x * scaleX; + double sourceX1 = (x + 1) * scaleX; + + int xStart = Math.max(0, (int) Math.floor(sourceX0)); + int xEnd = Math.min(sourceWidth, Math.max(xStart + 1, (int) Math.ceil(sourceX1))); + + accumulator.clear(); + + for (int sy = yStart; sy < yEnd; sy++) { + double overlapY0 = Math.max(sourceY0, sy); + double overlapY1 = Math.min(sourceY1, sy + 1.0); + float weightY = (float) Math.max(0.0, overlapY1 - overlapY0); + + if (weightY <= 0f) { + continue; + } + + for (int sx = xStart; sx < xEnd; sx++) { + double overlapX0 = Math.max(sourceX0, sx); + double overlapX1 = Math.min(sourceX1, sx + 1.0); + float weightX = (float) Math.max(0.0, overlapX1 - overlapX0); + + if (weightX <= 0f) { + continue; + } + + float weight = weightX * weightY; + + input.getPixel(sx, sy, sample); + accumulator.add(sample, weight, alphaWeighted, clampOutput); + } + } + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); } + } + } + + /** + * Bilinear filter. + * + * Used only when scaleImage() is asked to upscale. + * Mipmap generation itself normally uses areaResample(). + */ + private static void bilinearResample(ImageRaster input, + ImageRaster output, + boolean alphaWeighted, + boolean clampOutput) { + int sourceWidth = input.getWidth(); + int sourceHeight = input.getHeight(); - height /= 2; - width /= 2; + int targetWidth = output.getWidth(); + int targetHeight = output.getHeight(); - current = scaleImage(current, width, height); + double scaleX = (double) sourceWidth / (double) targetWidth; + double scaleY = (double) sourceHeight / (double) targetHeight; + + ColorRGBA sample = new ColorRGBA(); + ColorRGBA result = new ColorRGBA(); + PixelAccumulator accumulator = new PixelAccumulator(); + + for (int y = 0; y < targetHeight; y++) { + double sourceY = (y + 0.5) * scaleY - 0.5; + + int y0 = (int) Math.floor(sourceY); + double ty = sourceY - y0; + + if (y0 < 0) { + y0 = 0; + ty = 0.0; + } + + int y1 = y0 + 1; + + if (y1 >= sourceHeight) { + y1 = sourceHeight - 1; + y0 = y1; + ty = 0.0; + } + + float wy0 = (float) (1.0 - ty); + float wy1 = (float) ty; + + for (int x = 0; x < targetWidth; x++) { + double sourceX = (x + 0.5) * scaleX - 0.5; + + int x0 = (int) Math.floor(sourceX); + double tx = sourceX - x0; + + if (x0 < 0) { + x0 = 0; + tx = 0.0; + } + + int x1 = x0 + 1; + + if (x1 >= sourceWidth) { + x1 = sourceWidth - 1; + x0 = x1; + tx = 0.0; + } + + float wx0 = (float) (1.0 - tx); + float wx1 = (float) tx; + + accumulator.clear(); + + input.getPixel(x0, y0, sample); + accumulator.add(sample, wx0 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x1, y0, sample); + accumulator.add(sample, wx1 * wy0, alphaWeighted, clampOutput); + + input.getPixel(x0, y1, sample); + accumulator.add(sample, wx0 * wy1, alphaWeighted, clampOutput); + + input.getPixel(x1, y1, sample); + accumulator.add(sample, wx1 * wy1, alphaWeighted, clampOutput); + + accumulator.toColor(result, alphaWeighted, clampOutput); + output.setPixel(x, y, result); + } + } + } + + private static void validateImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null"); } - ByteBuffer combinedData = BufferUtils.createByteBuffer(totalSize); - int[] mipSizes = new int[output.size()]; - for (int i = 0; i < output.size(); i++){ - ByteBuffer data = output.get(i); - data.clear(); - combinedData.put(data); - mipSizes[i] = data.capacity(); + if (image.getWidth() < 1 || image.getHeight() < 1) { + throw new IllegalArgumentException("Image size must be at least 1x1"); } - combinedData.flip(); - // insert mip data into image - image.setData(0, combinedData); - image.setMipMapSizes(mipSizes); + if (image.getData() == null || image.getData().isEmpty()) { + throw new IllegalArgumentException("Image has no data buffers"); + } + + int bitsPerPixel = image.getFormat().getBitsPerPixel(); + + if (bitsPerPixel <= 0 || (bitsPerPixel % 8) != 0) { + throw new UnsupportedOperationException( + "CPU mipmap generation requires byte-addressable formats. Unsupported format: " + + image.getFormat() + + " with " + + bitsPerPixel + + " bits per pixel" + ); + } + + int baseLevelSize = levelSize(image.getFormat(), image.getWidth(), image.getHeight()); + for (int dataIndex = 0; dataIndex < image.getData().size(); dataIndex++) { + ByteBuffer data = image.getData(dataIndex); + if (data == null) { + throw new IllegalArgumentException("Image data buffer " + dataIndex + " is null"); + } + if (data.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data buffer " + dataIndex + " is smaller than expected base level size. Data capacity=" + + data.capacity() + + ", expected=" + + baseLevelSize + ); + } + } + } + + private static int levelSize(Image.Format format, int width, int height) { + int bitsPerPixel = format.getBitsPerPixel(); + + long bits = (long) width * (long) height * (long) bitsPerPixel; + + if ((bits % 8L) != 0L) { + throw new UnsupportedOperationException( + "Image level is not byte-addressable: " + + width + + "x" + + height + + " " + + format + ); + } + + long bytes = bits / 8L; + + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Image level is too large: " + + width + + "x" + + height + + " " + + format + ); + } + + return (int) bytes; + } + + /** + * If the input image already has mipmaps, its ByteBuffer may contain all levels. + * For rebuilding mipmaps, we only want the base level. + */ + private static ByteBuffer copyBaseLevel(ByteBuffer source, int baseLevelSize) { + if (source == null) { + throw new IllegalArgumentException("Image data buffer is null"); + } + if (source.capacity() < baseLevelSize) { + throw new IllegalArgumentException( + "Image data is smaller than expected base level size. Data capacity=" + + source.capacity() + + ", expected=" + + baseLevelSize + ); + } + + ByteBuffer duplicate = source.duplicate(); + duplicate.clear(); + duplicate.limit(baseLevelSize); + + ByteBuffer copy = BufferUtils.createByteBuffer(baseLevelSize); + copy.put(duplicate); + copy.flip(); + + return copy; + } + + private static boolean isSrgb(Image image) { + if (image.getColorSpace() == ColorSpace.sRGB) { + return true; + } + + String formatName = image.getFormat().name().toLowerCase(Locale.ROOT); + return formatName.contains("srgb"); + } + + private static final class MipChain { + final ByteBuffer combinedData; + final int[] mipSizes; + + MipChain(ByteBuffer combinedData, int[] mipSizes) { + this.combinedData = combinedData; + this.mipSizes = mipSizes; + } + } + + private static final class PixelAccumulator { + private float r; + private float g; + private float b; + private float a; + private float weight; + + void clear() { + r = 0f; + g = 0f; + b = 0f; + a = 0f; + weight = 0f; + } + + void add(ColorRGBA color, float sampleWeight, boolean alphaWeighted, boolean clampOutput) { + if (sampleWeight <= 0f) { + return; + } + + float alpha = clampOutput ? clamp01(color.a) : color.a; + + if (alphaWeighted) { + r += color.r * alpha * sampleWeight; + g += color.g * alpha * sampleWeight; + b += color.b * alpha * sampleWeight; + } else { + r += color.r * sampleWeight; + g += color.g * sampleWeight; + b += color.b * sampleWeight; + } + + a += alpha * sampleWeight; + weight += sampleWeight; + } + + void toColor(ColorRGBA store, boolean alphaWeighted, boolean clampOutput) { + if (weight <= 0f) { + store.set(0f, 0f, 0f, 0f); + return; + } + + float outA = a / weight; + + float outR; + float outG; + float outB; + + if (alphaWeighted) { + if (a > EPSILON_ALPHA) { + outR = r / a; + outG = g / a; + outB = b / a; + } else { + outR = 0f; + outG = 0f; + outB = 0f; + } + } else { + outR = r / weight; + outG = g / weight; + outB = b / weight; + } + + if (clampOutput) { + outR = clamp01(outR); + outG = clamp01(outG); + outB = clamp01(outB); + outA = clamp01(outA); + } + + store.set(outR, outG, outB, outA); + } + + private static float clamp01(float value) { + if (value <= 0f) { + return 0f; + } + + if (value >= 1f) { + return 1f; + } + + return value; + } } } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag new file mode 100644 index 0000000000..7ca7901b22 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.frag @@ -0,0 +1,21 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +#import "Common/ShaderLib/MultiSample.glsllib" + +uniform COLORTEXTURE m_Texture; +varying vec2 texCoord; + +vec3 linearToSrgb(vec3 color) { + vec3 linear = max(color, vec3(0.0)); + vec3 encodedLow = linear * 12.92; + vec3 encodedHigh = 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055; + return mix(encodedLow, encodedHigh, step(vec3(0.0031308), linear)); +} + +void main() { + vec4 color = getColor(m_Texture, texCoord); + #ifdef SRGB + gl_FragColor = vec4(linearToSrgb(color.rgb), color.a); + #else + gl_FragColor = color; + #endif +} \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md new file mode 100644 index 0000000000..2197032e08 --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.j3md @@ -0,0 +1,24 @@ +MaterialDef Blit { + + MaterialParameters { + Int BoundDrawBuffer + Int NumSamples + Boolean Srgb + Texture2D Texture + } + + Technique { + VertexShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.vert + FragmentShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Blit/Blit.frag + + WorldParameters { + } + + Defines { + BOUND_DRAW_BUFFER : BoundDrawBuffer + RESOLVE_MS : NumSamples + SRGB: Srgb + } + } + +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert new file mode 100644 index 0000000000..5d55b1f5af --- /dev/null +++ b/jme3-core/src/main/resources/Common/MatDefs/Blit/Blit.vert @@ -0,0 +1,11 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +attribute vec4 inPosition; +attribute vec2 inTexCoord; + +varying vec2 texCoord; + +void main() { + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); + texCoord = inTexCoord; +} \ No newline at end of file diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java new file mode 100644 index 0000000000..3338e6d4b4 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLImageFormatsTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import com.jme3.renderer.Caps; +import com.jme3.texture.Image; +import java.util.EnumSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class GLImageFormatsTest { + + @Test + public void testGles3UsesCoreHalfFloatType() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.FloatTexture, Caps.HalfFloatTexture); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.R16F.ordinal()].dataType); + assertEquals(GLExt.GL_HALF_FLOAT_ARB, + formats[0][Image.Format.RGBA16F.ordinal()].dataType); + } + + @Test + public void testGles3DoesNotExposeDesktopByteOrderFormats() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.BGR8.ordinal()]); + assertNull(formats[0][Image.Format.ABGR8.ordinal()]); + assertNull(formats[0][Image.Format.ARGB8.ordinal()]); + assertNull(formats[0][Image.Format.BGRA8.ordinal()]); + assertNull(formats[1][Image.Format.BGR8.ordinal()]); + assertNull(formats[1][Image.Format.ABGR8.ordinal()]); + assertNull(formats[1][Image.Format.ARGB8.ordinal()]); + assertNull(formats[1][Image.Format.BGRA8.ordinal()]); + } + + @Test + public void testGles3LegacyAlphaUsesGlesInternalFormatWhenNoCoreProfile() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].internalFormat); + assertEquals(GL.GL_ALPHA, + formats[0][Image.Format.Alpha8.ordinal()].format); + } + + @Test + public void testGles3CoreFormatsRemainMapped() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile, Caps.Srgb, Caps.FloatTexture, + Caps.IntegerTexture, Caps.PackedFloatTexture, + Caps.SharedExponentTexture, Caps.TextureCompressionETC2, + Caps.Depth24, Caps.FloatDepthBuffer, Caps.PackedDepthStencilBuffer); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.RGB10A2.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB111110F.ordinal()]); + assertNotNull(formats[0][Image.Format.RGB9E5.ordinal()]); + assertNotNull(formats[0][Image.Format.RGBA8UI.ordinal()]); + assertNotNull(formats[0][Image.Format.ETC2.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + } + + @Test + public void testDepthFormatsFollowExplicitCaps() { + EnumSet caps = EnumSet.of(Caps.OpenGLES20, Caps.OpenGLES30, + Caps.CoreProfile); + + GLImageFormat[][] formats = GLImageFormats.getFormatsForCaps(caps); + + assertNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth24); + caps.add(Caps.FloatDepthBuffer); + caps.add(Caps.PackedDepthStencilBuffer); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth24.ordinal()]); + assertNull(formats[0][Image.Format.Depth32.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth32F.ordinal()]); + assertNotNull(formats[0][Image.Format.Depth24Stencil8.ordinal()]); + + caps.add(Caps.Depth32); + formats = GLImageFormats.getFormatsForCaps(caps); + + assertNotNull(formats[0][Image.Format.Depth32.ordinal()]); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java new file mode 100644 index 0000000000..26d9aba160 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererMipmapPolicyTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GLRendererMipmapPolicyTest { + + @Test + public void testDoesNotClampWhenHardwareMipmapGenerationIsPending() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + true, + false, + null, + 7); + + assertEquals(7, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenNoMipmapsAreUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + false, + false, + false, + null, + 4); + + assertEquals(0, maxLevel); + } + + @Test + public void testUsesUploadedMipCountForExistingOrCpuMipmaps() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + true, + new int[] {64, 16, 4, 1}, + 7); + + assertEquals(3, maxLevel); + } + + @Test + public void testSkipsMaxLevelWhenCapabilityIsUnavailable() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + false, + false, + false, + true, + new int[] {64, 16}, + 7); + + assertEquals(-1, maxLevel); + } + + @Test + public void testClampsToBaseLevelWhenRequestedMipmapsCannotBeGeneratedOrUploaded() { + int maxLevel = GLRenderer.textureMaxLevelForUpload( + true, + true, + false, + false, + null, + 7); + + assertEquals(0, maxLevel); + } + + @Test + public void testGeneratedMipMaxLevelUsesLargestUploadDimension() { + assertEquals(0, GLRenderer.generatedMipMaxLevel(1, 1, 1)); + assertEquals(2, GLRenderer.generatedMipMaxLevel(3, 5, 1)); + assertEquals(3, GLRenderer.generatedMipMaxLevel(4, 8, 1)); + assertEquals(4, GLRenderer.generatedMipMaxLevel(4, 8, 16)); + } +} diff --git a/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java new file mode 100644 index 0000000000..fc2a6765d8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/util/MipMapGeneratorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.util; + +import com.jme3.texture.Image; +import com.jme3.texture.image.ColorSpace; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MipMapGeneratorTest { + + @Test + public void testGenerateMipMapsAfterResizeToPowerOf2() { + ByteBuffer data = BufferUtils.createByteBuffer(3 * 5 * 4); + for (int i = 0; i < data.capacity(); i++) { + data.put((byte) i); + } + data.flip(); + + Image image = new Image(Image.Format.RGBA8, 3, 5, data, ColorSpace.Linear); + Image resized = MipMapGenerator.resizeToPowerOf2(image); + + MipMapGenerator.generateMipMaps(resized, true, false); + + assertEquals(4, resized.getWidth()); + assertEquals(8, resized.getHeight()); + assertTrue(resized.hasMipmaps()); + assertNotNull(resized.getData(0)); + assertNotNull(resized.getMipMapSizes()); + assertTrue(resized.getMipMapSizes().length > 1); + } + + @Test + public void testGenerateMipMapsRejectsNullDataBuffer() { + ArrayList data = new ArrayList<>(); + data.add(null); + Image image = new Image(Image.Format.RGBA8, 2, 2, 1, data, null, ColorSpace.Linear); + + assertThrows(IllegalArgumentException.class, + () -> MipMapGenerator.generateMipMaps(image, true, false)); + } +} diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java index 9bbfb86f5c..44ded69fee 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/BulletDebugAppState.java @@ -234,24 +234,18 @@ public void render(RenderManager rm) { */ private void setupMaterials(Application app) { AssetManager manager = app.getAssetManager(); - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(manager, ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(manager, ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(manager, ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(manager, ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(manager, ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(manager, ColorRGBA.Pink); + } + + private static Material createDebugMaterial(AssetManager manager, ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } private void updateRigidBodies() { diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java index 16bc38ac8c..7ea8f3ba0c 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/debug/DebugTools.java @@ -268,23 +268,17 @@ protected void setupDebugNode() { * Initialize all the DebugTools materials. */ protected void setupMaterials() { - DEBUG_BLUE = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_BLUE.getAdditionalRenderState().setWireframe(true); - DEBUG_BLUE.setColor("Color", ColorRGBA.Blue); - DEBUG_GREEN = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_GREEN.getAdditionalRenderState().setWireframe(true); - DEBUG_GREEN.setColor("Color", ColorRGBA.Green); - DEBUG_RED = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_RED.getAdditionalRenderState().setWireframe(true); - DEBUG_RED.setColor("Color", ColorRGBA.Red); - DEBUG_YELLOW = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_YELLOW.getAdditionalRenderState().setWireframe(true); - DEBUG_YELLOW.setColor("Color", ColorRGBA.Yellow); - DEBUG_MAGENTA = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_MAGENTA.getAdditionalRenderState().setWireframe(true); - DEBUG_MAGENTA.setColor("Color", ColorRGBA.Magenta); - DEBUG_PINK = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); - DEBUG_PINK.getAdditionalRenderState().setWireframe(true); - DEBUG_PINK.setColor("Color", ColorRGBA.Pink); + DEBUG_BLUE = createDebugMaterial(ColorRGBA.Blue); + DEBUG_GREEN = createDebugMaterial(ColorRGBA.Green); + DEBUG_RED = createDebugMaterial(ColorRGBA.Red); + DEBUG_YELLOW = createDebugMaterial(ColorRGBA.Yellow); + DEBUG_MAGENTA = createDebugMaterial(ColorRGBA.Magenta); + DEBUG_PINK = createDebugMaterial(ColorRGBA.Pink); + } + + private Material createDebugMaterial(ColorRGBA color) { + Material material = new Material(manager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; } } diff --git a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java index 3e74f1ab3c..41c3b062d7 100644 --- a/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java +++ b/jme3-jbullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java @@ -132,10 +132,12 @@ public static Mesh getDebugMesh(CollisionShape shape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConvexShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } else if (shape.getCShape() instanceof ConcaveShape) { mesh = new Mesh(); mesh.setBuffer(Type.Position, 3, getVertices((ConcaveShape) shape.getCShape())); mesh.getFloatBuffer(Type.Position).clear(); + mesh.setMode(Mesh.Mode.Lines); } return mesh; } @@ -157,10 +159,10 @@ private static FloatBuffer getVertices(ConcaveShape concaveShape) { /** * Processes the given convex shape to retrieve a correctly ordered FloatBuffer to - * construct the shape from with a TriMesh. + * construct the shape from with line segments. * * @param convexShape the shape to retrieve the vertices from. - * @return the vertices as a FloatBuffer, ordered as Triangles. + * @return the vertices as a FloatBuffer, ordered as line segments. */ private static FloatBuffer getVertices(ConvexShape convexShape) { // Check there is a hull shape to render @@ -180,8 +182,8 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { assert hull.numTriangles() > 0 : "Expecting the Hull shape to have triangles"; int numberOfTriangles = hull.numTriangles(); - // The number of bytes needed is: (floats in a vertex) * (vertices in a triangle) * (# of triangles) * (size of float in bytes) - final int numberOfFloats = 3 * 3 * numberOfTriangles; + // The number of floats needed is: (floats in a vertex) * (vertices in a triangle outline) * (# of triangles) + final int numberOfFloats = 3 * 6 * numberOfTriangles; FloatBuffer vertices = BufferUtils.createFloatBuffer(numberOfFloats); // Force the limit, set the cap - the largest number of floats we will use the buffer for @@ -199,15 +201,24 @@ private static FloatBuffer getVertices(ConvexShape convexShape) { vertexB = hullVertices.get(hullIndices.get(index++)); vertexC = hullVertices.get(hullIndices.get(index++)); - // Put the vertices into the vertex buffer - vertices.put(vertexA.x).put(vertexA.y).put(vertexA.z); - vertices.put(vertexB.x).put(vertexB.y).put(vertexB.z); - vertices.put(vertexC.x).put(vertexC.y).put(vertexC.z); + putTriangleOutline(vertices, vertexA, vertexB, vertexC); } vertices.clear(); return vertices; } + + private static void putTriangleOutline(FloatBuffer vertices, Vector3f vertexA, + Vector3f vertexB, Vector3f vertexC) { + putLine(vertices, vertexA, vertexB); + putLine(vertices, vertexB, vertexC); + putLine(vertices, vertexC, vertexA); + } + + private static void putLine(FloatBuffer vertices, Vector3f start, Vector3f end) { + vertices.put(start.x).put(start.y).put(start.z); + vertices.put(end.x).put(end.y).put(end.z); + } } /** @@ -227,11 +238,13 @@ public BufferedTriangleCallback() { @Override public void processTriangle(Vector3f[] triangle, int partId, int triangleIndex) { - // Three sets of individual lines // The new Vector is needed as the given triangle reference is from a pool vertices.add(new Vector3f(triangle[0])); vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[1])); + vertices.add(new Vector3f(triangle[2])); vertices.add(new Vector3f(triangle[2])); + vertices.add(new Vector3f(triangle[0])); } /** diff --git a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java index f51dbf56b3..0bbefd7cf5 100644 --- a/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java +++ b/jme3-jbullet/src/test/java/com/jme3/jbullet/test/PreventBulletIssueRegressions.java @@ -40,10 +40,11 @@ import com.jme3.bullet.collision.shapes.CollisionShape; import com.jme3.bullet.collision.shapes.SphereCollisionShape; import com.jme3.bullet.control.BetterCharacterControl; -import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.KinematicRagdollControl; +import com.jme3.bullet.control.GhostControl; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.bullet.util.DebugShapeFactory; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.binary.BinaryExporter; @@ -171,6 +172,21 @@ public InputStream openStream() { Assertions.assertEquals(new Vector3f(0.26f, 0.27f, 0.28f), rbcCopy.getLinearVelocity()); } + /** + * Debug collision meshes should render as line primitives instead of + * relying on OpenGL polygon wireframe mode, which is unavailable in GLES. + */ + @Test + public void testDebugMeshesUseLines() { + CollisionShape shape = new BoxCollisionShape(Vector3f.UNIT_XYZ); + Mesh mesh = DebugShapeFactory.getDebugMesh(shape); + + Assertions.assertNotNull(mesh); + Assertions.assertEquals(Mesh.Mode.Lines, mesh.getMode()); + Assertions.assertTrue(mesh.getVertexCount() > 0); + Assertions.assertEquals(0, mesh.getVertexCount() % 6); + } + /** * Test case for JME issue #1004: RagdollUtils can't handle 16-bit bone indices. */ diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 56ef07ffab..4d132618cd 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -7,6 +7,8 @@ import java.nio.IntBuffer; import org.lwjgl.opengl.ARBDrawInstanced; import org.lwjgl.opengl.ARBInstancedArrays; +import org.lwjgl.opengl.ARBProgramInterfaceQuery; +import org.lwjgl.opengl.ARBShaderStorageBufferObject; import org.lwjgl.opengl.ARBSync; import org.lwjgl.opengl.ARBTextureMultisample; import org.lwjgl.opengl.ARBUniformBufferObject; @@ -87,6 +89,16 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(int program, int programInterface, String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(int condition, int flags) { return ARBSync.glFenceSync(condition, flags); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 74736f0666..83836aa3da 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -103,6 +103,16 @@ public void glUniformBlockBinding(final int program, final int uniformBlockIndex ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { + return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); + } + + @Override + public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { + ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); + } + @Override public Object glFenceSync(final int condition, final int flags) { return ARBSync.glFenceSync(condition, flags);