Compare commits

..
Author SHA1 Message Date
Run Yu 0ad90d139d wgpu: code clean up based on feedback 2026-01-28 21:35:45 -05:00
Run Yu cf694a3334 wgpu: add TextureUsage::SAMPLEABLE to the depth texture in test_Scissor,
which is needed to draw the triangle if the test is enabled.
2026-01-23 15:17:12 -05:00
Run Yu 863379b5b4 wgpu: fix the readPixels function so test_Scissor can draw the triangle 2026-01-23 15:17:12 -05:00
212 changed files with 3155 additions and 12913 deletions
+4 -3
View File
@@ -163,7 +163,9 @@ jobs:
mv out/filamat-android-release.aar out/filamat-${TAG}-android.aar
mv out/gltfio-android-release.aar out/gltfio-${TAG}-android.aar
mv out/filament-utils-android-release.aar out/filament-utils-${TAG}-android.aar
mv out/filament-android-release-linux.tgz out/filament-${TAG}-android-native.tgz
cd out/android-release/filament
tar -czf ../../filament-${TAG}-android-native.tgz .
cd ../../..
- name: Sign sample-gltf-viewer
run: |
echo "${APK_KEYSTORE_BASE64}" > filament.jks.base64
@@ -243,8 +245,7 @@ jobs:
env:
TAG: ${{ steps.git_ref.outputs.tag }}
run: |
@REMARK 'call' is required to ensure control returns to this script after the batch file finishes.
call build\windows\build-github.bat release
build\windows\build-github.bat release
echo on
move out\filament-windows.tgz out\filament-%TAG%-windows.tgz
shell: cmd
+19 -3
View File
@@ -49,6 +49,8 @@ option(FILAMENT_ENABLE_COVERAGE "Enable LLVM code coverage" OFF)
option(FILAMENT_ENABLE_FEATURE_LEVEL_0 "Enable Feature Level 0" ON)
option(FILAMENT_ENABLE_MULTIVIEW "Enable multiview for Filament" OFF)
option(FILAMENT_SUPPORTS_OSMESA "Enable OSMesa (headless GL context) for Filament" OFF)
option(FILAMENT_ENABLE_FGVIEWER "Enable the frame graph viewer" OFF)
@@ -605,6 +607,23 @@ else()
option(FILAMENT_DISABLE_MATOPT "Disable material optimizations" ON)
endif()
# This only affects the prebuilt shader files in gltfio and samples, not filament library.
# The value can be either "instanced", "multiview", or "none"
set(FILAMENT_SAMPLES_STEREO_TYPE "none" CACHE STRING
"Stereoscopic type that shader files in gltfio and samples are built for."
)
string(TOLOWER "${FILAMENT_SAMPLES_STEREO_TYPE}" FILAMENT_SAMPLES_STEREO_TYPE)
if (NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "instanced"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "none")
message(FATAL_ERROR "Invalid stereo type: \"${FILAMENT_SAMPLES_STEREO_TYPE}\" choose either \"instanced\", \"multiview\", or \"none\" ")
endif ()
# Compiling samples for multiview implies enabling multiview feature as well.
if (FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview")
set(FILAMENT_ENABLE_MULTIVIEW ON)
endif ()
# Define backend flag for debug only
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT FILAMENT_BACKEND_DEBUG_FLAG STREQUAL "")
add_definitions(-DFILAMENT_BACKEND_DEBUG_FLAG=${FILAMENT_BACKEND_DEBUG_FLAG})
@@ -858,7 +877,6 @@ add_subdirectory(${LIBRARIES}/gltfio)
add_subdirectory(${LIBRARIES}/ibl)
add_subdirectory(${LIBRARIES}/iblprefilter)
add_subdirectory(${LIBRARIES}/image)
add_subdirectory(${LIBRARIES}/imagediff)
add_subdirectory(${LIBRARIES}/ktxreader)
add_subdirectory(${LIBRARIES}/math)
add_subdirectory(${LIBRARIES}/mathio)
@@ -885,8 +903,6 @@ add_subdirectory(${EXTERNAL}/getopt)
add_subdirectory(${EXTERNAL}/perfetto/tnt)
add_subdirectory(${EXTERNAL}/basisu/tnt)
# imageio-lite is needed for viewer
add_subdirectory(${LIBRARIES}/imageio-lite)
# Note that this has to be placed after mikktspace in order for combine_static_libs to work.
add_subdirectory(${LIBRARIES}/geometry)
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.2'
implementation 'com.google.android.filament:filament-android:1.68.5'
}
```
@@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.69.2'
pod 'Filament', '~> 1.68.5'
```
## Documentation
-8
View File
@@ -7,14 +7,6 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.69.2
- engine: fix shader compilation failure in TAA material
- engine: fix stereo & parallel shader compilation
## v1.69.1
## v1.69.0
- engine: Support custom attributes morphing, and allow for omitting position and/or normal data. [⚠️ **Recompile Materials**]
@@ -18,7 +18,6 @@
#include <filament/Camera.h>
#include <utils/Entity.h>
#include <math/mat4.h>
@@ -41,13 +40,6 @@ Java_com_google_android_filament_Camera_nSetProjectionFov(JNIEnv*, jclass ,
camera->setProjection(fovInDegrees, aspect, near, far, (Camera::Fov) fov);
}
extern "C" JNIEXPORT jdouble JNICALL
Java_com_google_android_filament_Camera_nGetFieldOfViewInDegrees(JNIEnv*, jclass,
jlong nativeCamera, jint direction) {
Camera *camera = (Camera *) nativeCamera;
return camera->getFieldOfViewInDegrees((Camera::Fov) direction);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nSetLensProjection(JNIEnv*, jclass,
jlong nativeCamera, jdouble focalLength, jdouble aspect, jdouble near, jdouble far) {
@@ -70,21 +62,6 @@ Java_com_google_android_filament_Camera_nSetCustomProjection(JNIEnv *env, jclass
env->ReleaseDoubleArrayElements(inProjectionForCulling_, inProjectionForCulling, JNI_ABORT);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nSetCustomEyeProjection(JNIEnv *env, jclass,
jlong nativeCamera, jdoubleArray inProjection_, jint count, jdoubleArray inProjectionForCulling_,
jdouble near, jdouble far) {
Camera *camera = (Camera *) nativeCamera;
jdouble *inProjection = env->GetDoubleArrayElements(inProjection_, NULL);
jdouble *inProjectionForCulling = env->GetDoubleArrayElements(inProjectionForCulling_, NULL);
camera->setCustomEyeProjection(
reinterpret_cast<const filament::math::mat4 *>(inProjection), (size_t) count,
*reinterpret_cast<const filament::math::mat4 *>(inProjectionForCulling),
near, far);
env->ReleaseDoubleArrayElements(inProjection_, inProjection, JNI_ABORT);
env->ReleaseDoubleArrayElements(inProjectionForCulling_, inProjectionForCulling, JNI_ABORT);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nSetScaling(JNIEnv* env, jclass,
jlong nativeCamera, jdouble x, jdouble y) {
@@ -99,17 +76,6 @@ Java_com_google_android_filament_Camera_nSetShift(JNIEnv* env, jclass,
camera->setShift({(double)x, (double)y});
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nGetShift(JNIEnv* env, jclass,
jlong nativeCamera, jdoubleArray out_) {
Camera *camera = (Camera *) nativeCamera;
jdouble *out = env->GetDoubleArrayElements(out_, NULL);
filament::math::double2 s = camera->getShift();
out[0] = s.x;
out[1] = s.y;
env->ReleaseDoubleArrayElements(out_, out, 0);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nLookAt(JNIEnv*, jclass, jlong nativeCamera,
jdouble eye_x, jdouble eye_y, jdouble eye_z, jdouble center_x, jdouble center_y,
@@ -149,15 +115,6 @@ Java_com_google_android_filament_Camera_nSetModelMatrixFp64(JNIEnv *env, jclass,
env->ReleaseDoubleArrayElements(in_, in, JNI_ABORT);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nSetEyeModelMatrix(JNIEnv *env, jclass,
jlong nativeCamera, jint eyeId, jdoubleArray model_) {
Camera* camera = (Camera *) nativeCamera;
jdouble *model = env->GetDoubleArrayElements(model_, NULL);
camera->setEyeModelMatrix((uint8_t)eyeId, *reinterpret_cast<const filament::math::mat4*>(model));
env->ReleaseDoubleArrayElements(model_, model, JNI_ABORT);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Camera_nGetProjectionMatrix(JNIEnv *env, jclass,
jlong nativeCamera, jdoubleArray out_) {
@@ -323,5 +280,3 @@ Java_com_google_android_filament_Camera_nComputeEffectiveFov(JNIEnv*, jclass,
jdouble fovInDegrees, jdouble focusDistance) {
return Camera::computeEffectiveFov(fovInDegrees, focusDistance);
}
@@ -96,14 +96,6 @@ Java_com_google_android_filament_Material_nGetBlendingMode(JNIEnv*, jclass,
return (jint) material->getBlendingMode();
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_google_android_filament_Material_nGetTransparencyMode(JNIEnv*, jclass,
jlong nativeMaterial) {
Material* material = (Material*) nativeMaterial;
return (jint) material->getTransparencyMode();
}
extern "C"
JNIEXPORT jint JNICALL
@@ -564,19 +564,3 @@ Java_com_google_android_filament_MaterialInstance_nGetDepthFunc(JNIEnv* env, jcl
MaterialInstance* instance = (MaterialInstance*)nativeMaterialInstance;
return (jint)instance->getDepthFunc();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetTransparencyMode(JNIEnv*, jclass,
jlong nativeMaterialInstance, jint mode) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
instance->setTransparencyMode((MaterialInstance::TransparencyMode) mode);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_google_android_filament_MaterialInstance_nGetTransparencyMode(JNIEnv*, jclass,
jlong nativeMaterialInstance) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
return (jint) instance->getTransparencyMode();
}
@@ -366,13 +366,6 @@ Java_com_google_android_filament_RenderableManager_nSetPriority(JNIEnv*, jclass,
rm->setPriority((RenderableManager::Instance) i, (uint8_t) priority);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_RenderableManager_nGetPriority(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jint) rm->getPriority((RenderableManager::Instance) i);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_RenderableManager_nSetChannel(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jint channel) {
@@ -380,13 +373,6 @@ Java_com_google_android_filament_RenderableManager_nSetChannel(JNIEnv*, jclass,
rm->setChannel((RenderableManager::Instance) i, (uint8_t) channel);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_RenderableManager_nGetChannel(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jint) rm->getChannel((RenderableManager::Instance) i);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_RenderableManager_nSetCulling(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jboolean enabled) {
@@ -394,13 +380,6 @@ Java_com_google_android_filament_RenderableManager_nSetCulling(JNIEnv*, jclass,
rm->setCulling((RenderableManager::Instance) i, enabled);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_RenderableManager_nIsCullingEnabled(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jboolean) rm->isCullingEnabled((RenderableManager::Instance) i);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_RenderableManager_nSetFogEnabled(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jboolean enabled) {
@@ -450,13 +429,6 @@ Java_com_google_android_filament_RenderableManager_nIsShadowReceiver(JNIEnv*, jc
return (jboolean) rm->isShadowReceiver((RenderableManager::Instance) i);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_RenderableManager_nIsScreenSpaceContactShadowsEnabled(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jboolean) rm->isScreenSpaceContactShadowsEnabled((RenderableManager::Instance) i);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_RenderableManager_nGetAxisAlignedBoundingBox(JNIEnv* env,
jclass, jlong nativeRenderableManager, jint i, jfloatArray center_,
@@ -528,13 +500,6 @@ Java_com_google_android_filament_RenderableManager_nSetBlendOrderAt(JNIEnv*, jcl
(uint16_t) blendOrder);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_RenderableManager_nGetBlendOrderAt(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jint primitiveIndex) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jint) rm->getBlendOrderAt((RenderableManager::Instance) i, (size_t) primitiveIndex);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_RenderableManager_nSetGlobalBlendOrderEnabledAt(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jint primitiveIndex, jboolean enabled) {
@@ -543,13 +508,6 @@ Java_com_google_android_filament_RenderableManager_nSetGlobalBlendOrderEnabledAt
(bool) enabled);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_RenderableManager_nIsGlobalBlendOrderEnabledAt(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jint primitiveIndex) {
RenderableManager *rm = (RenderableManager *) nativeRenderableManager;
return (jboolean) rm->isGlobalBlendOrderEnabledAt((RenderableManager::Instance) i, (size_t) primitiveIndex);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_RenderableManager_nGetEnabledAttributesAt(JNIEnv*, jclass,
jlong nativeRenderableManager, jint i, jint primitiveIndex) {
@@ -173,13 +173,6 @@ Java_com_google_android_filament_Texture_nBuilderSwizzle(JNIEnv *, jclass ,
(Texture::Swizzle)r, (Texture::Swizzle)g, (Texture::Swizzle)b, (Texture::Swizzle)a);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Texture_nBuilderSamples(JNIEnv*, jclass,
jlong nativeBuilder, jint samples) {
Texture::Builder *builder = (Texture::Builder *) nativeBuilder;
builder->samples((uint8_t) samples);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_Texture_nBuilderImportTexture(JNIEnv*, jclass, jlong nativeBuilder, jlong id) {
@@ -76,12 +76,6 @@ Java_com_google_android_filament_View_nSetVisibleLayers(JNIEnv*, jclass, jlong n
view->setVisibleLayers((uint8_t) select, (uint8_t) value);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_View_nGetVisibleLayers(JNIEnv*, jclass, jlong nativeView) {
View* view = (View*) nativeView;
return view->getVisibleLayers();
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nSetShadowingEnabled(JNIEnv*, jclass, jlong nativeView, jboolean enabled) {
View* view = (View*) nativeView;
@@ -446,18 +440,6 @@ Java_com_google_android_filament_View_nIsShadowingEnabled(JNIEnv *, jclass, jlon
return (jboolean)view->isShadowingEnabled();
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nSetFrustumCullingEnabled(JNIEnv*, jclass, jlong nativeView, jboolean enabled) {
View* view = (View*) nativeView;
view->setFrustumCullingEnabled(enabled);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_View_nIsFrustumCullingEnabled(JNIEnv*, jclass, jlong nativeView) {
View* view = (View*) nativeView;
return (jboolean)view->isFrustumCullingEnabled();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nSetScreenSpaceRefractionEnabled(JNIEnv *, jclass,
@@ -136,13 +136,4 @@ final class Asserts {
throw new ArrayIndexOutOfBoundsException("Array length must be at least 4");
}
}
@NonNull @Size(min = 2)
static double[] assertDouble2(@Nullable double[] out) {
if (out == null) out = new double[2];
else if (out.length < 2) {
throw new ArrayIndexOutOfBoundsException("Array length must be at least 2");
}
return out;
}
}
@@ -343,27 +343,6 @@ public class Camera {
nSetScaling(getNativeObject(), xscaling, yscaling);
}
/**
* Sets a custom projection matrix for each eye.
*
* @param inProjection An array of projection matrices, one for each eye.
* Must have at least 16 * count elements.
* @param count Number of eyes to set.
* @param inProjectionForCulling Custom projection matrix for culling, must encompass all eyes.
* @param near Distance to the near plane.
* @param far Distance to the far plane.
*/
public void setCustomEyeProjection(
@NonNull double[] inProjection, int count,
@NonNull @Size(min = 16) double[] inProjectionForCulling,
double near, double far) {
Asserts.assertMat4dIn(inProjectionForCulling);
if (inProjection.length < 16 * count) {
throw new IllegalArgumentException("inProjection array too small for the given count");
}
nSetCustomEyeProjection(getNativeObject(), inProjection, count, inProjectionForCulling, near, far);
}
/**
* Sets an additional matrix that scales the projection matrix.
*
@@ -420,31 +399,6 @@ public class Camera {
nSetShift(getNativeObject(), xshift, yshift);
}
/**
* Returns the shift amount used to translate the projection matrix.
*
* @param out A 2-double array where the shift will be stored, or null.
* @return A 2-double array containing the x and y shift.
*/
@NonNull @Size(min = 2)
public double[] getShift(@Nullable @Size(min = 2) double[] out) {
out = Asserts.assertDouble2(out);
nGetShift(getNativeObject(), out);
return out;
}
/**
* Returns the camera's field of view in degrees.
*
* @param direction The direction of the FOV (VERTICAL or HORIZONTAL).
* @return The field of view in degrees.
*/
public double getFieldOfViewInDegrees(@NonNull Fov direction) {
return nGetFieldOfViewInDegrees(getNativeObject(), direction.ordinal());
}
/**
* Sets the camera's model matrix.
* <p>
@@ -791,17 +745,6 @@ public class Camera {
return mEntity;
}
/**
* Sets the model matrix for a specific eye.
*
* @param eyeId The index of the eye.
* @param model The model matrix for the eye.
*/
public void setEyeModelMatrix(int eyeId, @NonNull @Size(min = 16) double[] model) {
Asserts.assertMat4dIn(model);
nSetEyeModelMatrix(getNativeObject(), eyeId, model);
}
/**
* Helper to compute the effective focal length taking into account the focus distance
*
@@ -841,13 +784,8 @@ public class Camera {
private static native void nSetCustomProjection(long nativeCamera, double[] inProjection, double[] inProjectionForCulling, double near, double far);
private static native void nSetScaling(long nativeCamera, double x, double y);
private static native void nSetShift(long nativeCamera, double x, double y);
private static native void nGetShift(long nativeCamera, double[] out);
private static native void nSetModelMatrix(long nativeCamera, float[] in);
private static native void nSetModelMatrixFp64(long nativeCamera, double[] in);
private static native void nSetEyeModelMatrix(long nativeCamera, int eyeId, double[] model);
private static native void nSetCustomEyeProjection(long nativeCamera, double[] inProjection, int count, double[] inProjectionForCulling, double near, double far);
private static native double nGetFieldOfViewInDegrees(long nativeCamera, int direction);
private static native void nLookAt(long nativeCamera, double eyeX, double eyeY, double eyeZ, double centerX, double centerY, double centerZ, double upX, double upY, double upZ);
private static native double nGetNear(long nativeCamera);
private static native double nGetCullingFar(long nativeCamera);
@@ -54,7 +54,6 @@ public class Material {
static final CullingMode[] sCullingModeValues = CullingMode.values();
static final VertexBuffer.VertexAttribute[] sVertexAttributeValues =
VertexBuffer.VertexAttribute.values();
static final TransparencyMode[] sTransparencyModeValues = TransparencyMode.values();
}
private long mNativeObject;
@@ -161,31 +160,6 @@ public class Material {
SCREEN,
}
/**
* How transparent objects are handled
*
* @see
* <a href="https://google.github.io/filament/Materials.html#materialdefinitions/materialblock/blendingandtransparency:transparencymode">
* Blending and transparency: transparencyMode</a>
*/
public enum TransparencyMode {
/** The transparent object is drawn honoring the raster state. */
DEFAULT,
/**
* The transparent object is first drawn in the depth buffer,
* then in the color buffer, honoring the culling mode, but ignoring the depth test function.
*/
TWO_PASSES_ONE_SIDE,
/**
* The transparent object is drawn twice in the color buffer,
* first with back faces only, then with front faces; the culling
* mode is ignored. Can be combined with two-sided lighting.
*/
TWO_PASSES_TWO_SIDES
}
/**
* Supported refraction modes
*
@@ -613,18 +587,6 @@ public class Material {
return EnumCache.sBlendingModeValues[nGetBlendingMode(getNativeObject())];
}
/**
* Returns the transparency mode of this material.
* This value only makes sense when the blending mode is transparent or fade.
*
* @see
* <a href="https://google.github.io/filament/Materials.html#materialdefinitions/materialblock/blendingandtransparency:transparencymode">
* Blending and transparency: transparencyMode</a>
*/
public TransparencyMode getTransparencyMode() {
return EnumCache.sTransparencyModeValues[nGetTransparencyMode(getNativeObject())];
}
/**
* Returns the refraction mode of this material.
*
@@ -1168,7 +1130,6 @@ public class Material {
private static native int nGetShading(long nativeMaterial);
private static native int nGetInterpolation(long nativeMaterial);
private static native int nGetBlendingMode(long nativeMaterial);
private static native int nGetTransparencyMode(long nativeMaterial);
private static native int nGetVertexDomain(long nativeMaterial);
private static native int nGetCullingMode(long nativeMaterial);
private static native boolean nIsColorWriteEnabled(long nativeMaterial);
@@ -537,14 +537,6 @@ public class MaterialInstance {
nSetDoubleSided(getNativeObject(), doubleSided);
}
/**
* Sets the transparency mode for this material instance.
* @see Material.TransparencyMode
*/
public void setTransparencyMode(@NonNull Material.TransparencyMode mode) {
nSetTransparencyMode(getNativeObject(), mode.ordinal());
}
/**
* Returns whether double-sided lighting is enabled when the parent Material has double-sided
* capability.
@@ -553,14 +545,6 @@ public class MaterialInstance {
return nIsDoubleSided(getNativeObject());
}
/**
* Returns the transparency mode.
*/
@NonNull
public Material.TransparencyMode getTransparencyMode() {
return Material.EnumCache.sTransparencyModeValues[nGetTransparencyMode(getNativeObject())];
}
/**
* Overrides the default triangle culling state that was set on the material.
*
@@ -998,6 +982,4 @@ public class MaterialInstance {
private static native boolean nIsStencilWriteEnabled(long nativeMaterialInstance);
private static native boolean nIsDepthCullingEnabled(long nativeMaterialInstance);
private static native int nGetDepthFunc(long nativeMaterialInstance);
private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode);
private static native int nGetTransparencyMode(long nativeMaterialInstance);
}
@@ -346,8 +346,8 @@ public class RenderableManager {
*
* @return Builder reference for chaining calls.
*
* @see Builder#blendOrder()
* @see Builder#priority()
* @see Builder::blendOrder()
* @see Builder::priority()
* @see RenderableManager::setBlendOrderAt()
*/
@NonNull
@@ -725,10 +725,6 @@ public class RenderableManager {
nSetPriority(mNativeObject, i, priority);
}
public int getPriority(@EntityInstance int i) {
return nGetPriority(mNativeObject, i);
}
/**
* Changes the channel of a renderable
*
@@ -738,10 +734,6 @@ public class RenderableManager {
nSetChannel(mNativeObject, i, channel);
}
public int getChannel(@EntityInstance int i) {
return nGetChannel(mNativeObject, i);
}
/**
* Changes whether or not frustum culling is on.
*
@@ -751,10 +743,6 @@ public class RenderableManager {
nSetCulling(mNativeObject, i, enabled);
}
public boolean isCullingEnabled(@EntityInstance int i) {
return nIsCullingEnabled(mNativeObject, i);
}
/**
* Changes whether or not the large-scale fog is applied to this renderable
* @see Builder#fog
@@ -824,10 +812,6 @@ public class RenderableManager {
nSetScreenSpaceContactShadows(mNativeObject, i, enabled);
}
public boolean isScreenSpaceContactShadowsEnabled(@EntityInstance int i) {
return nIsScreenSpaceContactShadowsEnabled(mNativeObject, i);
}
/**
* Checks if the renderable can cast shadows.
*
@@ -948,10 +932,6 @@ public class RenderableManager {
nSetBlendOrderAt(mNativeObject, instance, primitiveIndex, blendOrder);
}
public int getBlendOrderAt(@EntityInstance int instance, @IntRange(from = 0) int primitiveIndex) {
return nGetBlendOrderAt(mNativeObject, instance, primitiveIndex);
}
/**
* Changes whether the blend order is global or local to this Renderable (by default).
*
@@ -966,10 +946,6 @@ public class RenderableManager {
nSetGlobalBlendOrderEnabledAt(mNativeObject, instance, primitiveIndex, enabled);
}
public boolean isGlobalBlendOrderEnabledAt(@EntityInstance int instance, @IntRange(from = 0) int primitiveIndex) {
return nIsGlobalBlendOrderEnabledAt(mNativeObject, instance, primitiveIndex);
}
/**
* Retrieves the set of enabled attribute slots in the given primitive's VertexBuffer.
*/
@@ -1037,11 +1013,8 @@ public class RenderableManager {
private static native void nSetAxisAlignedBoundingBox(long nativeRenderableManager, int i, float cx, float cy, float cz, float ex, float ey, float ez);
private static native void nSetLayerMask(long nativeRenderableManager, int i, int select, int value);
private static native void nSetPriority(long nativeRenderableManager, int i, int priority);
private static native int nGetPriority(long nativeRenderableManager, int i);
private static native void nSetChannel(long nativeRenderableManager, int i, int channel);
private static native int nGetChannel(long nativeRenderableManager, int i);
private static native void nSetCulling(long nativeRenderableManager, int i, boolean enabled);
private static native boolean nIsCullingEnabled(long nativeRenderableManager, int i);
private static native void nSetFogEnabled(long nativeRenderableManager, int i, boolean enabled);
private static native boolean nGetFogEnabled(long nativeRenderableManager, int i);
private static native void nSetLightChannel(long nativeRenderableManager, int i, int channel, boolean enable);
@@ -1049,7 +1022,6 @@ public class RenderableManager {
private static native void nSetCastShadows(long nativeRenderableManager, int i, boolean enabled);
private static native void nSetReceiveShadows(long nativeRenderableManager, int i, boolean enabled);
private static native void nSetScreenSpaceContactShadows(long nativeRenderableManager, int i, boolean enabled);
private static native boolean nIsScreenSpaceContactShadowsEnabled(long nativeRenderableManager, int i);
private static native boolean nIsShadowCaster(long nativeRenderableManager, int i);
private static native boolean nIsShadowReceiver(long nativeRenderableManager, int i);
private static native void nGetAxisAlignedBoundingBox(long nativeRenderableManager, int i, float[] center, float[] halfExtent);
@@ -1060,8 +1032,6 @@ public class RenderableManager {
private static native long nGetMaterialInstanceAt(long nativeRenderableManager, int i, int primitiveIndex);
private static native void nSetGeometryAt(long nativeRenderableManager, int i, int primitiveIndex, int primitiveType, long nativeVertexBuffer, long nativeIndexBuffer, int offset, int count);
private static native void nSetBlendOrderAt(long nativeRenderableManager, int i, int primitiveIndex, int blendOrder);
private static native int nGetBlendOrderAt(long nativeRenderableManager, int i, int primitiveIndex);
private static native void nSetGlobalBlendOrderEnabledAt(long nativeRenderableManager, int i, int primitiveIndex, boolean enabled);
private static native boolean nIsGlobalBlendOrderEnabledAt(long nativeRenderableManager, int i, int primitiveIndex);
private static native int nGetEnabledAttributesAt(long nativeRenderableManager, int i, int primitiveIndex);
}
@@ -795,17 +795,6 @@ public class Texture {
return this;
}
/**
* Specifies the number of samples for multisample anti-aliasing.
* @param samples number of samples, must be at least 1. Default is 1.
* @return This Builder, for chaining calls.
*/
@NonNull
public Builder samples(@IntRange(from = 1) int samples) {
nBuilderSamples(mNativeBuilder, samples);
return this;
}
/**
* Specifies the texture's internal format.
* <p>The internal format specifies how texels are stored (which may be different from how
@@ -1381,7 +1370,6 @@ public class Texture {
private static native void nBuilderFormat(long nativeBuilder, int format);
private static native void nBuilderUsage(long nativeBuilder, int flags);
private static native void nBuilderSwizzle(long nativeBuilder, int r, int g, int b, int a);
private static native void nBuilderSamples(long nativeBuilder, int samples);
private static native void nBuilderImportTexture(long nativeBuilder, long id);
private static native void nBuilderExternal(long nativeBuilder);
private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
@@ -350,26 +350,6 @@ public class View {
nSetVisibleLayers(getNativeObject(), select & 0xFF, values & 0xFF);
}
/**
* Returns the visible layers.
*
* @return a bitmask specifying which layer is visible.
*/
public int getVisibleLayers() {
return nGetVisibleLayers(getNativeObject());
}
/**
* Enables or disables a specific layer.
*
* @param layer Index of the layer to enable or disable, must be between 0 and 7.
* @param enabled True to enable the layer, false to disable it.
*/
public void setLayerEnabled(@IntRange(from = 0, to = 7) int layer, boolean enabled) {
int mask = 1 << layer;
setVisibleLayers(mask, enabled ? mask : 0);
}
/**
* Enables or disables shadow mapping. Enabled by default.
*
@@ -388,22 +368,6 @@ public class View {
return nIsShadowingEnabled(getNativeObject());
}
/**
* Enables or disables frustum culling. Enabled by default.
*
* @param enabled true enables frustum culling, false disables it.
*/
public void setFrustumCullingEnabled(boolean enabled) {
nSetFrustumCullingEnabled(getNativeObject(), enabled);
}
/**
* @return whether frustum culling is enabled
*/
public boolean isFrustumCullingEnabled() {
return nIsFrustumCullingEnabled(getNativeObject());
}
/**
* Enables or disables screen space refraction. Enabled by default.
*
@@ -1358,9 +1322,6 @@ public class View {
private static native boolean nHasCamera(long nativeView);
private static native void nSetViewport(long nativeView, int left, int bottom, int width, int height);
private static native void nSetVisibleLayers(long nativeView, int select, int value);
private static native int nGetVisibleLayers(long nativeView);
private static native void nSetFrustumCullingEnabled(long nativeView, boolean enabled);
private static native boolean nIsFrustumCullingEnabled(long nativeView);
private static native void nSetShadowingEnabled(long nativeView, boolean enabled);
private static native void nSetRenderTarget(long nativeView, long nativeRenderTarget);
private static native void nSetSampleCount(long nativeView, int count);
@@ -1445,59 +1406,65 @@ public class View {
* by lowering the resolution of a View, or to increase the quality when the
* rendering is faster than the target frame rate.
*
* <p>This structure can be used to specify the minimum scale factor used when
* This structure can be used to specify the minimum scale factor used when
* lowering the resolution of a View, and the maximum scale factor used when
* increasing the resolution for higher quality rendering. The scale factors
* can be controlled on each X and Y axis independently. By default, all scale
* factors are set to 1.0.</p>
* factors are set to 1.0.
*
* <ul>
* <li>enabled: enable or disables dynamic resolution on a View</li>
* enabled: enable or disables dynamic resolution on a View
*
* <li>homogeneousScaling: by default the system scales the major axis first. Set this to true
* to force homogeneous scaling.</li>
* homogeneousScaling: by default the system scales the major axis first. Set this to true
* to force homogeneous scaling.
*
* <li>minScale: the minimum scale in X and Y this View should use</li>
* minScale: the minimum scale in X and Y this View should use
*
* <li>maxScale: the maximum scale in X and Y this View should use</li>
* maxScale: the maximum scale in X and Y this View should use
*
* <li>quality: upscaling quality.
* LOW: 1 bilinear tap, Medium: 4 bilinear taps, High: 9 bilinear taps (tent)</li>
* </ul>
* quality: upscaling quality.
* LOW: 1 bilinear tap, Medium: 4 bilinear taps, High: 9 bilinear taps (tent)
*
* <p>Note:
* \note
* Dynamic resolution is only supported on platforms where the time to render
* a frame can be measured accurately. On platforms where this is not supported,
* Dynamic Resolution can't be enabled unless <code>minScale == maxScale</code>.</p>
* Dynamic Resolution can't be enabled unless minScale == maxScale.
*
* @see Renderer.FrameRateOptions
* @see Renderer::FrameRateOptions
*
*/
public static class DynamicResolutionOptions {
/** minimum scale factors in x and y */
/**
* minimum scale factors in x and y
*/
public float minScale = 0.5f;
/** maximum scale factors in x and y */
/**
* maximum scale factors in x and y
*/
public float maxScale = 1.0f;
/** sharpness when QualityLevel::MEDIUM or higher is used [0 (disabled), 1 (sharpest)] */
/**
* sharpness when QualityLevel::MEDIUM or higher is used [0 (disabled), 1 (sharpest)]
*/
public float sharpness = 0.9f;
/** enable or disable dynamic resolution */
/**
* enable or disable dynamic resolution
*/
public boolean enabled = false;
/** set to true to force homogeneous scaling */
/**
* set to true to force homogeneous scaling
*/
public boolean homogeneousScaling = false;
/**
* Upscaling quality
* <ul>
* <li>LOW: bilinear filtered blit. Fastest, poor quality</li>
* <li>MEDIUM: Qualcomm Snapdragon Game Super Resolution (SGSR) 1.0</li>
* <li>HIGH: AMD FidelityFX FSR1 w/ mobile optimizations</li>
* <li>ULTRA: AMD FidelityFX FSR1</li>
* </ul>
* LOW: bilinear filtered blit. Fastest, poor quality
* MEDIUM: Qualcomm Snapdragon Game Super Resolution (SGSR) 1.0
* HIGH: AMD FidelityFX FSR1 w/ mobile optimizations
* ULTRA: AMD FidelityFX FSR1
* FSR1 and SGSR require a well anti-aliased (MSAA or TAA), noise free scene.
* Avoid FXAA and dithering.
*
* <p>The default upscaling quality is set to LOW.</p>
* The default upscaling quality is set to LOW.
*
* <p>caveat: currently, <code>quality</code> is always set to LOW if the View is TRANSLUCENT.</p>
* caveat: currently, 'quality' is always set to LOW if the View is TRANSLUCENT.
*/
@NonNull
public QualityLevel quality = QualityLevel.LOW;
@@ -1506,98 +1473,134 @@ public class View {
/**
* Options to control the bloom effect
*
* <ul>
* <li>enabled: Enable or disable the bloom post-processing effect. Disabled by default.</li>
* enabled: Enable or disable the bloom post-processing effect. Disabled by default.
*
* <li>levels: Number of successive blurs to achieve the blur effect, the minimum is 3 and the
* levels: Number of successive blurs to achieve the blur effect, the minimum is 3 and the
* maximum is 12. This value together with resolution influences the spread of the
* blur effect. This value can be silently reduced to accommodate the original
* image size.</li>
* image size.
*
* <li>resolution: Resolution of bloom's minor axis. The minimum value is 2^levels and the
* resolution: Resolution of bloom's minor axis. The minimum value is 2^levels and the
* the maximum is lower of the original resolution and 4096. This parameter is
* silently clamped to the minimum and maximum.
* It is highly recommended that this value be smaller than the target resolution
* after dynamic resolution is applied (horizontally and vertically).</li>
* after dynamic resolution is applied (horizontally and vertically).
*
* <li>strength: how much of the bloom is added to the original image. Between 0 and 1.</li>
* strength: how much of the bloom is added to the original image. Between 0 and 1.
*
* <li>blendMode: Whether the bloom effect is purely additive (false) or mixed with the original
* image (true).</li>
* blendMode: Whether the bloom effect is purely additive (false) or mixed with the original
* image (true).
*
* <li>threshold: When enabled, a threshold at 1.0 is applied on the source image, this is
* useful for artistic reasons and is usually needed when a dirt texture is used.</li>
* threshold: When enabled, a threshold at 1.0 is applied on the source image, this is
* useful for artistic reasons and is usually needed when a dirt texture is used.
*
* <li>dirt: A dirt/scratch/smudges texture (that can be RGB), which gets added to the
* dirt: A dirt/scratch/smudges texture (that can be RGB), which gets added to the
* bloom effect. Smudges are visible where bloom occurs. Threshold must be
* enabled for the dirt effect to work properly.</li>
* enabled for the dirt effect to work properly.
*
* <li>dirtStrength: Strength of the dirt texture.</li>
* </ul>
* dirtStrength: Strength of the dirt texture.
*/
public static class BloomOptions {
public enum BlendMode {
/** Bloom is modulated by the strength parameter and added to the scene */
/**
* Bloom is modulated by the strength parameter and added to the scene
*/
ADD,
/** Bloom is interpolated with the scene using the strength parameter */
/**
* Bloom is interpolated with the scene using the strength parameter
*/
INTERPOLATE,
}
/** user provided dirt texture */
/**
* user provided dirt texture
*/
@Nullable
public Texture dirt = null;
/** strength of the dirt texture */
/**
* strength of the dirt texture
*/
public float dirtStrength = 0.2f;
/** bloom's strength between 0.0 and 1.0 */
/**
* bloom's strength between 0.0 and 1.0
*/
public float strength = 0.10f;
/** resolution of vertical axis (2^levels to 2048) */
/**
* resolution of vertical axis (2^levels to 2048)
*/
public int resolution = 384;
/** number of blur levels (1 to 11) */
/**
* number of blur levels (1 to 11)
*/
public int levels = 6;
/** how the bloom effect is applied */
/**
* how the bloom effect is applied
*/
@NonNull
public BloomOptions.BlendMode blendMode = BloomOptions.BlendMode.ADD;
/** whether to threshold the source */
/**
* whether to threshold the source
*/
public boolean threshold = true;
/** enable or disable bloom */
/**
* enable or disable bloom
*/
public boolean enabled = false;
/** limit highlights to this value before bloom [10, +inf] */
/**
* limit highlights to this value before bloom [10, +inf]
*/
public float highlight = 1000.0f;
/**
* Bloom quality level.
* <ul>
* <li>LOW (default): use a more optimized down-sampling filter, however there can be artifacts
* with dynamic resolution, this can be alleviated by using the homogenous mode.</li>
* <li>MEDIUM: Good balance between quality and performance.</li>
* <li>HIGH: In this mode the bloom resolution is automatically increased to avoid artifacts.
* LOW (default): use a more optimized down-sampling filter, however there can be artifacts
* with dynamic resolution, this can be alleviated by using the homogenous mode.
* MEDIUM: Good balance between quality and performance.
* HIGH: In this mode the bloom resolution is automatically increased to avoid artifacts.
* This mode can be significantly slower on mobile, especially at high resolution.
* This mode greatly improves the anamorphic bloom.</li>
* </ul>
* This mode greatly improves the anamorphic bloom.
*/
@NonNull
public QualityLevel quality = QualityLevel.LOW;
/** enable screen-space lens flare */
/**
* enable screen-space lens flare
*/
public boolean lensFlare = false;
/** enable starburst effect on lens flare */
/**
* enable starburst effect on lens flare
*/
public boolean starburst = true;
/** amount of chromatic aberration */
/**
* amount of chromatic aberration
*/
public float chromaticAberration = 0.005f;
/** number of flare "ghosts" */
/**
* number of flare "ghosts"
*/
public int ghostCount = 4;
/** spacing of the ghost in screen units [0, 1[ */
/**
* spacing of the ghost in screen units [0, 1[
*/
public float ghostSpacing = 0.6f;
/** hdr threshold for the ghosts */
/**
* hdr threshold for the ghosts
*/
public float ghostThreshold = 10.0f;
/** thickness of halo in vertical screen units, 0 to disable */
/**
* thickness of halo in vertical screen units, 0 to disable
*/
public float haloThickness = 0.1f;
/** radius of halo in vertical screen units [0, 0.5] */
/**
* radius of halo in vertical screen units [0, 0.5]
*/
public float haloRadius = 0.4f;
/** hdr threshold for the halo */
/**
* hdr threshold for the halo
*/
public float haloThreshold = 10.0f;
}
/**
* Options to control large-scale fog in the scene. Materials can enable the <code>linearFog</code> property,
* Options to control large-scale fog in the scene. Materials can enable the `linearFog` property,
* which uses a simplified, linear equation for fog calculation; in this mode, the heightFalloff
* is ignored as well as the mipmap selection in IBL or skyColor mode.
*/
@@ -1611,12 +1614,12 @@ public class View {
* This can be used to exclude the skybox, which is desirable if it already contains clouds or
* fog. The default value is +infinity which applies the fog to everything.
*
* <p>Note: The SkyBox is typically at a distance of 1e19 in world space (depending on the near
* plane distance and projection used though).</p>
* Note: The SkyBox is typically at a distance of 1e19 in world space (depending on the near
* plane distance and projection used though).
*/
public float cutOffDistance = Float.POSITIVE_INFINITY;
/**
* fog's maximum opacity between 0 and 1. Ignored in <code>linearFog</code> mode.
* fog's maximum opacity between 0 and 1. Ignored in `linearFog` mode.
*/
public float maximumOpacity = 1.0f;
/**
@@ -1628,11 +1631,11 @@ public class View {
* It can be expressed as 1/H, where H is the altitude change in world units [m] that causes a
* factor 2.78 (e) change in fog density.
*
* <p>A falloff of 0 means the fog density is constant everywhere and may result is slightly
* faster computations.</p>
* A falloff of 0 means the fog density is constant everywhere and may result is slightly
* faster computations.
*
* <p>In <code>linearFog</code> mode, only use to compute the slope of the linear equation. Completely
* ignored if set to 0.</p>
* In `linearFog` mode, only use to compute the slope of the linear equation. Completely
* ignored if set to 0.
*/
public float heightFalloff = 1.0f;
/**
@@ -1642,11 +1645,11 @@ public class View {
* above one are allowed but could create a non energy-conservative fog (this is dependant
* on the IBL's intensity as well).
*
* <p>We assume that our fog has no absorption and therefore all the light it scatters out
* We assume that our fog has no absorption and therefore all the light it scatters out
* becomes ambient light in-scattering and has lost all directionality, i.e.: scattering is
* isotropic. This somewhat simulates Rayleigh scattering.</p>
* isotropic. This somewhat simulates Rayleigh scattering.
*
* <p>This value is used as a tint instead, when fogColorFromIbl is enabled.</p>
* This value is used as a tint instead, when fogColorFromIbl is enabled.
*
* @see #fogColorFromIbl
*/
@@ -1657,20 +1660,20 @@ public class View {
* light is absorbed and out-scattered per unit of distance. Each unit of extinction reduces
* the incoming light to 37% of its original value.
*
* <p>Note: The extinction factor is related to the fog density, it's usually some constant K times
* Note: The extinction factor is related to the fog density, it's usually some constant K times
* the density at sea level (more specifically at fog height). The constant K depends on
* the composition of the fog/atmosphere.</p>
* the composition of the fog/atmosphere.
*
* <p>For historical reason this parameter is called <code>density</code>.</p>
* For historical reason this parameter is called `density`.
*
* <p>In <code>linearFog</code> mode this is the slope of the linear equation if heightFalloff is set to 0.
* In `linearFog` mode this is the slope of the linear equation if heightFalloff is set to 0.
* Otherwise, heightFalloff affects the slope calculation such that it matches the slope of
* the standard equation at the camera height.</p>
* the standard equation at the camera height.
*/
public float density = 0.1f;
/**
* Distance in world units [m] from the camera where the Sun in-scattering starts.
* Ignored in <code>linearFog</code> mode.
* Ignored in `linearFog` mode.
*/
public float inScatteringStart = 0.0f;
/**
@@ -1678,16 +1681,16 @@ public class View {
* is scattered (by the fog) towards the camera.
* Size of the Sun in-scattering (>0 to activate). Good values are >> 1 (e.g. ~10 - 100).
* Smaller values result is a larger scattering size.
* Ignored in <code>linearFog</code> mode.
* Ignored in `linearFog` mode.
*/
public float inScatteringSize = -1.0f;
/**
* The fog color will be sampled from the IBL in the view direction and tinted by <code>color</code>.
* The fog color will be sampled from the IBL in the view direction and tinted by `color`.
* Depending on the scene this can produce very convincing results.
*
* <p>This simulates a more anisotropic phase-function.</p>
* This simulates a more anisotropic phase-function.
*
* <p><code>fogColorFromIbl</code> is ignored when skyTexture is specified.</p>
* `fogColorFromIbl` is ignored when skyTexture is specified.
*
* @see #skyColor
*/
@@ -1700,11 +1703,11 @@ public class View {
* level with a strong gaussian filter or even an irradiance filter and then generate mip
* levels as usual. How blurred the base level is somewhat of an artistic decision.
*
* <p>This simulates a more anisotropic phase-function.</p>
* This simulates a more anisotropic phase-function.
*
* <p><code>fogColorFromIbl</code> is ignored when skyTexture is specified.</p>
* `fogColorFromIbl` is ignored when skyTexture is specified.
*
* <p>In <code>linearFog</code> mode mipmap level 0 is always used.</p>
* In `linearFog` mode mipmap level 0 is always used.
*
* @see Texture
* @see #fogColorFromIbl
@@ -1720,9 +1723,9 @@ public class View {
/**
* Options to control Depth of Field (DoF) effect in the scene.
*
* <p>cocScale can be used to set the depth of field blur independently of the camera
* cocScale can be used to set the depth of field blur independently of the camera
* aperture, e.g. for artistic reasons. This can be achieved by setting:
* cocScale = cameraAperture / desiredDoFAperture</p>
* cocScale = cameraAperture / desiredDoFAperture
*
* @see Camera
*/
@@ -1733,24 +1736,59 @@ public class View {
MEDIAN,
}
/** circle of confusion scale factor (amount of blur) */
/**
* circle of confusion scale factor (amount of blur)
*/
public float cocScale = 1.0f;
/** width/height aspect ratio of the circle of confusion (simulate anamorphic lenses) */
/**
* width/height aspect ratio of the circle of confusion (simulate anamorphic lenses)
*/
public float cocAspectRatio = 1.0f;
/** maximum aperture diameter in meters (zero to disable rotation) */
/**
* maximum aperture diameter in meters (zero to disable rotation)
*/
public float maxApertureDiameter = 0.01f;
/** enable or disable depth of field effect */
/**
* enable or disable depth of field effect
*/
public boolean enabled = false;
/** filter to use for filling gaps in the kernel */
/**
* filter to use for filling gaps in the kernel
*/
@NonNull
public DepthOfFieldOptions.Filter filter = DepthOfFieldOptions.Filter.MEDIAN;
/** perform DoF processing at native resolution */
/**
* perform DoF processing at native resolution
*/
public boolean nativeResolution = false;
/** number of kernel rings for foreground tiles */
/**
* Number of of rings used by the gather kernels. The number of rings affects quality
* and performance. The actual number of sample per pixel is defined
* as (ringCount * 2 - 1)^2. Here are a few commonly used values:
* 3 rings : 25 ( 5x 5 grid)
* 4 rings : 49 ( 7x 7 grid)
* 5 rings : 81 ( 9x 9 grid)
* 17 rings : 1089 (33x33 grid)
*
* With a maximum circle-of-confusion of 32, it is never necessary to use more than 17 rings.
*
* Usually all three settings below are set to the same value, however, it is often
* acceptable to use a lower ring count for the "fast tiles", which improves performance.
* Fast tiles are regions of the screen where every pixels have a similar
* circle-of-confusion radius.
*
* A value of 0 means default, which is 5 on desktop and 3 on mobile.
*
* @{
*/
public int foregroundRingCount = 0;
/** number of kernel rings for background tiles */
/**
* number of kernel rings for background tiles
*/
public int backgroundRingCount = 0;
/** number of kernel rings for fast tiles */
/**
* number of kernel rings for fast tiles
*/
public int fastGatherRingCount = 0;
/**
* maximum circle-of-confusion in pixels for the foreground, must be in [0, 32] range.
@@ -1768,16 +1806,26 @@ public class View {
* Options to control the vignetting effect.
*/
public static class VignetteOptions {
/** high values restrict the vignette closer to the corners, between 0 and 1 */
/**
* high values restrict the vignette closer to the corners, between 0 and 1
*/
public float midPoint = 0.5f;
/** controls the shape of the vignette, from a rounded rectangle (0.0), to an oval (0.5), to a circle (1.0) */
/**
* controls the shape of the vignette, from a rounded rectangle (0.0), to an oval (0.5), to a circle (1.0)
*/
public float roundness = 0.5f;
/** softening amount of the vignette effect, between 0 and 1 */
/**
* softening amount of the vignette effect, between 0 and 1
*/
public float feather = 0.5f;
/** color of the vignette effect, alpha is currently ignored */
/**
* color of the vignette effect, alpha is currently ignored
*/
@NonNull @Size(min = 4)
public float[] color = {0.0f, 0.0f, 0.0f, 1.0f};
/** enables or disables the vignette effect */
/**
* enables or disables the vignette effect
*/
public boolean enabled = false;
}
@@ -1791,11 +1839,11 @@ public class View {
/**
* Sets the quality of the HDR color buffer.
*
* <p>A quality of HIGH or ULTRA means using an RGB16F or RGBA16F color buffer. This means
* A quality of HIGH or ULTRA means using an RGB16F or RGBA16F color buffer. This means
* colors in the LDR range (0..1) have a 10 bit precision. A quality of LOW or MEDIUM means
* using an R11G11B10F opaque color buffer or an RGBA16F transparent color buffer. With
* R11G11B10F colors in the LDR range have a precision of either 6 bits (red and green
* channels) or 5 bits (blue channel).</p>
* channels) or 5 bits (blue channel).
*/
@NonNull
public QualityLevel hdrColorBuffer = QualityLevel.HIGH;
@@ -1807,44 +1855,72 @@ public class View {
*/
public static class AmbientOcclusionOptions {
public enum AmbientOcclusionType {
/** use Scalable Ambient Occlusion */
/**
* use Scalable Ambient Occlusion
*/
SAO,
/** use Ground Truth-Based Ambient Occlusion */
/**
* use Ground Truth-Based Ambient Occlusion
*/
GTAO,
}
/** Type of ambient occlusion algorithm. */
/**
* Type of ambient occlusion algorithm.
*/
@NonNull
public AmbientOcclusionOptions.AmbientOcclusionType aoType = AmbientOcclusionOptions.AmbientOcclusionType.SAO;
/** Ambient Occlusion radius in meters, between 0 and ~10. */
/**
* Ambient Occlusion radius in meters, between 0 and ~10.
*/
public float radius = 0.3f;
/** Controls ambient occlusion's contrast. Must be positive. */
/**
* Controls ambient occlusion's contrast. Must be positive.
*/
public float power = 1.0f;
/**
* Self-occlusion bias in meters. Use to avoid self-occlusion.
* Between 0 and a few mm. No effect when aoType set to GTAO
*/
public float bias = 0.0005f;
/** How each dimension of the AO buffer is scaled. Must be either 0.5 or 1.0. */
/**
* How each dimension of the AO buffer is scaled. Must be either 0.5 or 1.0.
*/
public float resolution = 0.5f;
/** Strength of the Ambient Occlusion effect. */
/**
* Strength of the Ambient Occlusion effect.
*/
public float intensity = 1.0f;
/** depth distance that constitute an edge for filtering */
/**
* depth distance that constitute an edge for filtering
*/
public float bilateralThreshold = 0.05f;
/** affects # of samples used for AO and params for filtering */
/**
* affects # of samples used for AO and params for filtering
*/
@NonNull
public QualityLevel quality = QualityLevel.LOW;
/** affects AO smoothness. Recommend setting to HIGH when aoType set to GTAO. */
/**
* affects AO smoothness. Recommend setting to HIGH when aoType set to GTAO.
*/
@NonNull
public QualityLevel lowPassFilter = QualityLevel.MEDIUM;
/** affects AO buffer upsampling quality */
/**
* affects AO buffer upsampling quality
*/
@NonNull
public QualityLevel upsampling = QualityLevel.LOW;
/** enables or disables screen-space ambient occlusion */
/**
* enables or disables screen-space ambient occlusion
*/
public boolean enabled = false;
/** enables bent normals computation from AO, and specular AO */
/**
* enables bent normals computation from AO, and specular AO
*/
public boolean bentNormals = false;
/** min angle in radian to consider. No effect when aoType set to GTAO. */
/**
* min angle in radian to consider. No effect when aoType set to GTAO.
*/
public float minHorizonAngleRad = 0.0f;
/**
* Screen Space Cone Tracing (SSCT) options
@@ -1930,10 +2006,12 @@ public class View {
* @see #setMultiSampleAntiAliasingOptions
*/
public static class MultiSampleAntiAliasingOptions {
/** enables or disables msaa */
/**
* enables or disables msaa
*/
public boolean enabled = false;
/**
* sampleCount number of samples to use for multi-sampled anti-aliasing.<br>
* sampleCount number of samples to use for multi-sampled anti-aliasing.\n
* 0: treated as 1
* 1: no anti-aliasing
* n: sample count. Effective sample could be different depending on the
@@ -1952,75 +2030,106 @@ public class View {
* shaders to be recompiled. These options should be changed or set during initialization.
* `filterWidth`, `feedback` and `jitterPattern`, however, can be changed at any time.
*
* <p><code>feedback</code> of 0.1 effectively accumulates a maximum of 19 samples in steady state.
* see "A Survey of Temporal Antialiasing Techniques" by Lei Yang and all for more information.</p>
* `feedback` of 0.1 effectively accumulates a maximum of 19 samples in steady state.
* see "A Survey of Temporal Antialiasing Techniques" by Lei Yang and all for more information.
*
* @see #setTemporalAntiAliasingOptions
*/
public static class TemporalAntiAliasingOptions {
public enum BoxType {
/** use an AABB neighborhood */
/**
* use an AABB neighborhood
*/
AABB,
/** use both AABB and variance */
/**
* use both AABB and variance
*/
AABB_VARIANCE,
}
public enum BoxClipping {
/** Accurate box clipping */
/**
* Accurate box clipping
*/
ACCURATE,
/** clamping */
/**
* clamping
*/
CLAMP,
/** no rejections (use for debugging) */
/**
* no rejections (use for debugging)
*/
NONE,
}
public enum JitterPattern {
/** 4-samples, rotated grid sampling */
RGSS_X4,
/** 4-samples, uniform grid in helix sequence */
UNIFORM_HELIX_X4,
/** 8-samples of halton 2,3 */
HALTON_23_X8,
/** 16-samples of halton 2,3 */
HALTON_23_X16,
/** 32-samples of halton 2,3 */
HALTON_23_X32,
}
/** @deprecated has no effect. */
/**
* @deprecated has no effect.
*/
public float filterWidth = 1.0f;
/** history feedback, between 0 (maximum temporal AA) and 1 (no temporal AA). */
/**
* history feedback, between 0 (maximum temporal AA) and 1 (no temporal AA).
*/
public float feedback = 0.12f;
/** texturing lod bias (typically -1 or -2) */
/**
* texturing lod bias (typically -1 or -2)
*/
public float lodBias = -1.0f;
/** post-TAA sharpen, especially useful when upscaling is true. */
/**
* post-TAA sharpen, especially useful when upscaling is true.
*/
public float sharpness = 0.0f;
/** enables or disables temporal anti-aliasing */
/**
* enables or disables temporal anti-aliasing
*/
public boolean enabled = false;
/** Upscaling factor. Disables Dynamic Resolution. [BETA] */
/**
* Upscaling factor. Disables Dynamic Resolution. [BETA]
*/
public float upscaling = 1.0f;
/** whether to filter the history buffer */
/**
* whether to filter the history buffer
*/
public boolean filterHistory = true;
/** whether to apply the reconstruction filter to the input */
/**
* whether to apply the reconstruction filter to the input
*/
public boolean filterInput = true;
/** whether to use the YcoCg color-space for history rejection */
/**
* whether to use the YcoCg color-space for history rejection
*/
public boolean useYCoCg = false;
/** set to true for HDR content */
/**
* set to true for HDR content
*/
public boolean hdr = true;
/** type of color gamut box */
/**
* type of color gamut box
*/
@NonNull
public TemporalAntiAliasingOptions.BoxType boxType = TemporalAntiAliasingOptions.BoxType.AABB;
/** clipping algorithm */
/**
* clipping algorithm
*/
@NonNull
public TemporalAntiAliasingOptions.BoxClipping boxClipping = TemporalAntiAliasingOptions.BoxClipping.ACCURATE;
/** Jitter Pattern */
@NonNull
public TemporalAntiAliasingOptions.JitterPattern jitterPattern = TemporalAntiAliasingOptions.JitterPattern.HALTON_23_X16;
/** High values increases ghosting artefact, lower values increases jittering, range [0.75, 1.25] */
public float varianceGamma = 1.0f;
/** adjust the feedback dynamically to reduce flickering */
/**
* adjust the feedback dynamically to reduce flickering
*/
public boolean preventFlickering = false;
/** whether to apply history reprojection (debug option) */
/**
* whether to apply history reprojection (debug option)
*/
public boolean historyReprojection = true;
}
@@ -2029,22 +2138,30 @@ public class View {
* @see #setScreenSpaceReflectionsOptions
*/
public static class ScreenSpaceReflectionsOptions {
/** ray thickness, in world units */
/**
* ray thickness, in world units
*/
public float thickness = 0.1f;
/** bias, in world units, to prevent self-intersections */
/**
* bias, in world units, to prevent self-intersections
*/
public float bias = 0.01f;
/** maximum distance, in world units, to raycast */
/**
* maximum distance, in world units, to raycast
*/
public float maxDistance = 3.0f;
/** stride, in texels, for samples along the ray. */
/**
* stride, in texels, for samples along the ray.
*/
public float stride = 2.0f;
public boolean enabled = false;
}
/**
* Options for the screen-space guard band.
* <p>A guard band can be enabled to avoid some artifacts towards the edge of the screen when
* A guard band can be enabled to avoid some artifacts towards the edge of the screen when
* using screen-space effects such as SSAO. Enabling the guard band reduces performance slightly.
* Currently the guard band can only be enabled or disabled.</p>
* Currently the guard band can only be enabled or disabled.
*/
public static class GuardBandOptions {
public boolean enabled = false;
@@ -2057,9 +2174,13 @@ public class View {
* @see #setSampleCount
*/
public enum AntiAliasing {
/** no anti aliasing performed as part of post-processing */
/**
* no anti aliasing performed as part of post-processing
*/
NONE,
/** FXAA is a low-quality but very efficient type of anti-aliasing. (default). */
/**
* FXAA is a low-quality but very efficient type of anti-aliasing. (default).
*/
FXAA,
}
@@ -2067,9 +2188,13 @@ public class View {
* List of available post-processing dithering techniques.
*/
public enum Dithering {
/** No dithering */
/**
* No dithering
*/
NONE,
/** Temporal dithering (default) */
/**
* Temporal dithering (default)
*/
TEMPORAL,
}
@@ -2078,13 +2203,21 @@ public class View {
* @see #setShadowType
*/
public enum ShadowType {
/** percentage-closer filtered shadows (default) */
/**
* percentage-closer filtered shadows (default)
*/
PCF,
/** variance shadows */
/**
* variance shadows
*/
VSM,
/** PCF with contact hardening simulation */
/**
* PCF with contact hardening simulation
*/
DPCF,
/** PCF with soft shadows and contact hardening */
/**
* PCF with soft shadows and contact hardening
*/
PCSS,
PCFd,
}
@@ -2092,14 +2225,14 @@ public class View {
/**
* View-level options for VSM Shadowing.
* @see #setVsmShadowOptions
* <b>Warning:</b> This API is still experimental and subject to change.
* @warning This API is still experimental and subject to change.
*/
public static class VsmShadowOptions {
/**
* Sets the number of anisotropic samples to use when sampling a VSM shadow map. If greater
* than 0, mipmaps will automatically be generated each frame for all lights.
*
* <p>The number of anisotropic samples = 2 ^ vsmAnisotropy.</p>
* The number of anisotropic samples = 2 ^ vsmAnisotropy.
*/
public int anisotropy = 0;
/**
@@ -2133,7 +2266,7 @@ public class View {
/**
* View-level options for DPCF and PCSS Shadowing.
* @see #setSoftShadowOptions
* <b>Warning:</b> This API is still experimental and subject to change.
* @warning This API is still experimental and subject to change.
*/
public static class SoftShadowOptions {
/**
+1 -1
View File
@@ -7,7 +7,7 @@ apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
def tools = ['matc', 'cmgen']
def platforms = [
'mac': [classifier: 'osx-aarch_64', archive: "filament-v${VERSION_NAME}-mac.tgz", path: { t -> "filament/bin/${t}" }],
'mac': [classifier: 'osx-aarch64', archive: "filament-v${VERSION_NAME}-mac.tgz", path: { t -> "filament/bin/${t}" }],
'linux': [classifier: 'linux-x86_64', archive: "filament-v${VERSION_NAME}-linux.tgz", path: { t -> "filament/bin/${t}" }],
'windows': [classifier: 'windows-x86_64', archive: "filament-v${VERSION_NAME}-windows.tgz", path: { t -> "bin/${t}.exe" }]
]
@@ -22,10 +22,6 @@ add_library(image STATIC IMPORTED)
set_target_properties(image PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libimage.a)
add_library(imageio-lite STATIC IMPORTED)
set_target_properties(imageio-lite PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libimageio-lite.a)
add_library(ktxreader STATIC IMPORTED)
set_target_properties(ktxreader PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libktxreader.a)
@@ -34,10 +30,6 @@ add_library(viewer STATIC IMPORTED)
set_target_properties(viewer PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libviewer.a)
add_library(imagediff STATIC IMPORTED)
set_target_properties(imagediff PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libimagediff.a)
add_library(civetweb STATIC IMPORTED)
set_target_properties(civetweb PROPERTIES IMPORTED_LOCATION
${FILAMENT_DIR}/lib/${ANDROID_ABI}/libcivetweb.a)
@@ -65,7 +57,6 @@ add_library(filament-utils-jni SHARED
src/main/cpp/IBLPrefilterContext.cpp
src/main/cpp/Utils.cpp
src/main/cpp/Manipulator.cpp
src/main/cpp/ImageDiff.cpp
src/main/cpp/RemoteServer.cpp
${IMAGEIO_DIR}/include/imageio/ImageDecoder.h
@@ -83,7 +74,6 @@ target_include_directories(filament-utils-jni PRIVATE
${FILAMENT_DIR}/include
../../filament/backend/include
${IMAGEIO_DIR}/include
../../libs/imagediff/include
../../libs/utils/include)
set_target_properties(filament-utils-jni PROPERTIES LINK_DEPENDS ${VERSION_SCRIPT})
@@ -95,13 +85,10 @@ target_link_libraries(filament-utils-jni
PRIVATE camutils
PRIVATE iblprefilter
PRIVATE image
PRIVATE imageio-lite
PRIVATE filament-jni
PRIVATE ktxreader
PRIVATE viewer
PRIVATE imagediff
PRIVATE log
PRIVATE utils
PRIVATE perfetto # needed only when FILAMENT_ENABLE_PERFETTO is defined
PRIVATE jnigraphics # needed for AndroidBitmap_* functions in ImageDiff
)
@@ -159,50 +159,37 @@ extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nGetViewerOptions(JNIEnv* env, jclass,
jlong nativeObject, jobject result) {
AutomationEngine* automation = (AutomationEngine*) nativeObject;
const auto& settings = automation->getSettings();
const auto& options = settings.viewer;
auto options = automation->getViewerOptions();
const jclass klass = env->GetObjectClass(result);
const jfieldID cameraAperture = env->GetFieldID(klass, "cameraAperture", "F");
const jfieldID cameraSpeed = env->GetFieldID(klass, "cameraSpeed", "F");
const jfieldID cameraISO = env->GetFieldID(klass, "cameraISO", "F");
const jfieldID cameraNear = env->GetFieldID(klass, "cameraNear", "F");
const jfieldID cameraFar = env->GetFieldID(klass, "cameraFar", "F");
const jfieldID groundShadowStrength = env->GetFieldID(klass, "groundShadowStrength", "F");
const jfieldID groundPlaneEnabled = env->GetFieldID(klass, "groundPlaneEnabled", "Z");
const jfieldID skyboxEnabled = env->GetFieldID(klass, "skyboxEnabled", "Z");
const jfieldID cameraFocalLength = env->GetFieldID(klass, "cameraFocalLength", "F");
const jfieldID cameraFocusDistance = env->GetFieldID(klass, "cameraFocusDistance", "F");
const jfieldID autoScaleEnabled = env->GetFieldID(klass, "autoScaleEnabled", "Z");
const jfieldID autoInstancingEnabled = env->GetFieldID(klass, "autoInstancingEnabled", "Z");
env->SetFloatField(result, cameraAperture, options.cameraAperture);
env->SetFloatField(result, cameraSpeed, options.cameraSpeed);
env->SetFloatField(result, cameraISO, options.cameraISO);
env->SetFloatField(result, cameraNear, options.cameraNear);
env->SetFloatField(result, cameraFar, options.cameraFar);
env->SetFloatField(result, groundShadowStrength, options.groundShadowStrength);
env->SetBooleanField(result, groundPlaneEnabled, options.groundPlaneEnabled);
env->SetBooleanField(result, skyboxEnabled, options.skyboxEnabled);
env->SetFloatField(result, cameraFocalLength, options.cameraFocalLength);
env->SetFloatField(result, cameraFocusDistance, options.cameraFocusDistance);
env->SetBooleanField(result, autoScaleEnabled, options.autoScaleEnabled);
env->SetBooleanField(result, autoInstancingEnabled, options.autoInstancingEnabled);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nGetCameraSettings(JNIEnv* env, jclass,
jlong nativeObject, jobject result) {
AutomationEngine* automation = (AutomationEngine*) nativeObject;
const auto& settings = automation->getSettings();
const auto& camera = settings.camera;
const jclass klass = env->GetObjectClass(result);
const jfieldID aperture = env->GetFieldID(klass, "aperture", "F");
const jfieldID shutterSpeed = env->GetFieldID(klass, "shutterSpeed", "F");
const jfieldID sensitivity = env->GetFieldID(klass, "sensitivity", "F");
const jfieldID near = env->GetFieldID(klass, "near", "F");
const jfieldID far = env->GetFieldID(klass, "far", "F");
const jfieldID focalLength = env->GetFieldID(klass, "focalLength", "F");
const jfieldID focusDistance = env->GetFieldID(klass, "focusDistance", "F");
env->SetFloatField(result, aperture, camera.aperture);
env->SetFloatField(result, shutterSpeed, camera.shutterSpeed);
env->SetFloatField(result, sensitivity, camera.sensitivity);
env->SetFloatField(result, near, camera.near);
env->SetFloatField(result, far, camera.far);
env->SetFloatField(result, focalLength, camera.focalLength);
env->SetFloatField(result, focusDistance, camera.focusDistance);
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nGetColorGrading(JNIEnv*, jclass,
jlong nativeObject, jlong nativeEngine) {
@@ -228,18 +215,6 @@ Java_com_google_android_filament_utils_AutomationEngine_nShouldClose(JNIEnv*, jc
return automation->shouldClose();
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nGetTestCount(JNIEnv*, jclass, jlong native) {
AutomationEngine* automation = (AutomationEngine*) native;
return (jint) automation->testCount();
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nGetCurrentTest(JNIEnv*, jclass, jlong native) {
AutomationEngine* automation = (AutomationEngine*) native;
return (jint) automation->currentTest();
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nDestroy(JNIEnv*, jclass, jlong native) {
AutomationEngine* automation = (AutomationEngine*) native;
@@ -1,217 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <jni.h>
#include <android/bitmap.h>
#include <imagediff/ImageDiff.h>
#include <utils/Log.h>
#include <vector>
using namespace imagediff;
using namespace utils;
namespace {
struct BitmapLock {
JNIEnv* env;
jobject bitmap;
void* pixels;
AndroidBitmapInfo info;
BitmapLock(JNIEnv* env, jobject bitmap) : env(env), bitmap(bitmap), pixels(nullptr) {
if (!bitmap) return;
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
return;
}
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
pixels = nullptr;
}
}
~BitmapLock() {
if (pixels) {
AndroidBitmap_unlockPixels(env, bitmap);
}
}
bool isValid() const { return pixels != nullptr; }
imagediff::Bitmap toBitmap() const {
return {
.width = (uint32_t) info.width,
.height = (uint32_t) info.height,
.stride = (size_t) info.stride,
.data = pixels
};
}
};
} // namespace
// Helper to convert C++ ImageDiffResult to Java Result
jobject createResult(JNIEnv* env, ImageDiffResult const& result, bool generateDiff) {
// Create Result class/objects
jclass resultClass = env->FindClass("com/google/android/filament/utils/ImageDiff$Result");
jmethodID resultCtor = env->GetMethodID(resultClass, "<init>", "()V");
jobject resultObj = env->NewObject(resultClass, resultCtor);
jfieldID statusField = env->GetFieldID(resultClass, "status", "Lcom/google/android/filament/utils/ImageDiff$Result$Status;");
jfieldID failingCountField = env->GetFieldID(resultClass, "failingPixelCount", "J");
jfieldID maxDiffField = env->GetFieldID(resultClass, "maxDiffFound", "[F");
jfieldID diffImageField = env->GetFieldID(resultClass, "diffImage", "Landroid/graphics/Bitmap;");
// Map Status enum
jclass statusEnum = env->FindClass("com/google/android/filament/utils/ImageDiff$Result$Status");
jobject statusObj = nullptr;
jfieldID enumField = nullptr;
switch (result.status) {
case ImageDiffResult::Status::PASSED:
enumField = env->GetStaticFieldID(statusEnum, "PASSED", "Lcom/google/android/filament/utils/ImageDiff$Result$Status;");
break;
case ImageDiffResult::Status::SIZE_MISMATCH:
enumField = env->GetStaticFieldID(statusEnum, "SIZE_MISMATCH", "Lcom/google/android/filament/utils/ImageDiff$Result$Status;");
break;
case ImageDiffResult::Status::PIXEL_DIFFERENCE:
enumField = env->GetStaticFieldID(statusEnum, "PIXEL_DIFFERENCE", "Lcom/google/android/filament/utils/ImageDiff$Result$Status;");
break;
}
statusObj = env->GetStaticObjectField(statusEnum, enumField);
env->SetObjectField(resultObj, statusField, statusObj);
env->SetLongField(resultObj, failingCountField, (jlong) result.failingPixelCount);
jfloatArray maxDiffArray = env->NewFloatArray(4);
env->SetFloatArrayRegion(maxDiffArray, 0, 4, result.maxDiffFound);
env->SetObjectField(resultObj, maxDiffField, maxDiffArray);
if (generateDiff && result.diffImage.getWidth() > 0) {
jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmap = env->GetStaticMethodID(bitmapClass, "createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
jfieldID argb8888 = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
jobject configObj = env->GetStaticObjectField(configClass, argb8888);
uint32_t width = result.diffImage.getWidth();
uint32_t height = result.diffImage.getHeight();
jobject diffBitmap = env->CallStaticObjectMethod(bitmapClass, createBitmap, (jint)width, (jint)height, configObj);
if (diffBitmap) {
void* diffPixels;
if (AndroidBitmap_lockPixels(env, diffBitmap, &diffPixels) == 0) {
float const* src = result.diffImage.getPixelRef();
uint8_t* dst = (uint8_t*) diffPixels;
uint32_t channels = result.diffImage.getChannels(); // usually 4
for (size_t i = 0; i < width * height; ++i) {
for (int c = 0; c < 4; ++c) {
float v = 0.0f;
if (c < channels) v = src[i * channels + c];
if (c == 3 && channels < 4) v = 1.0f; // Alpha 1.0 if missing
dst[i * 4 + c] = (uint8_t) std::min(255.0f, std::max(0.0f, v * 255.0f));
}
}
AndroidBitmap_unlockPixels(env, diffBitmap);
env->SetObjectField(resultObj, diffImageField, diffBitmap);
}
}
}
return resultObj;
}
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_android_filament_utils_ImageDiff_nCompareBasic(JNIEnv* env, jclass,
jobject refBitmap, jobject candBitmap, jint mode, jint swizzle, jint channelMask,
jfloat maxAbsDiff, jfloat maxFailingPixelsFraction, jobject maskBitmap) {
BitmapLock refArg(env, refBitmap);
BitmapLock candArg(env, candBitmap);
BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false);
}
ImageDiffConfig config;
config.mode = (ImageDiffConfig::Mode) mode;
config.swizzle = (ImageDiffConfig::Swizzle) swizzle;
config.channelMask = (uint8_t) channelMask;
config.maxAbsDiff = maxAbsDiff;
config.maxFailingPixelsFraction = maxFailingPixelsFraction;
imagediff::Bitmap const* maskPtr = nullptr;
imagediff::Bitmap maskVal;
if (maskBitmap && maskArg.isValid()) {
maskVal = maskArg.toBitmap();
maskPtr = &maskVal;
}
bool generateDiff = true;
ImageDiffResult result = compare(refArg.toBitmap(), candArg.toBitmap(), config, maskPtr, generateDiff);
return createResult(env, result, generateDiff);
}
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclass,
jobject refBitmap, jobject candBitmap, jstring jsonConfig, jobject maskBitmap) {
BitmapLock refArg(env, refBitmap);
BitmapLock candArg(env, candBitmap);
BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false);
}
ImageDiffConfig config;
const char* nativeJson = env->GetStringUTFChars(jsonConfig, 0);
size_t length = env->GetStringUTFLength(jsonConfig);
bool parsed = parseConfig(nativeJson, length, &config);
env->ReleaseStringUTFChars(jsonConfig, nativeJson);
if (!parsed) {
// Fallback to default or error?
// We could log error.
utils::slog.e << "ImageDiff JNI: Failed to parse JSON config" << utils::io::endl;
ImageDiffResult errResult;
errResult.status = ImageDiffResult::Status::PIXEL_DIFFERENCE; // assume fail
return createResult(env, errResult, false);
}
imagediff::Bitmap const* maskPtr = nullptr;
imagediff::Bitmap maskVal;
if (maskBitmap && maskArg.isValid()) {
maskVal = maskArg.toBitmap();
maskPtr = &maskVal;
}
bool generateDiff = true;
ImageDiffResult result = compare(refArg.toBitmap(), candArg.toBitmap(), config, maskPtr, generateDiff);
return createResult(env, result, generateDiff);
}
@@ -94,23 +94,20 @@ public class AutomationEngine {
* Allows remote control for the viewer.
*/
public static class ViewerOptions {
public float cameraAperture = 16.0f;
public float cameraSpeed = 125.0f;
public float cameraISO = 100.0f;
public float cameraNear = 0.1f;
public float cameraFar = 100.0f;
public float groundShadowStrength = 0.75f;
public boolean groundPlaneEnabled = false;
public boolean skyboxEnabled = true;
public float cameraFocalLength = 28.0f;
public float cameraFocusDistance = 0.0f;
public boolean autoScaleEnabled = true;
public boolean autoInstancingEnabled = false;
}
public static class CameraSettings {
public float aperture = 16.0f;
public float shutterSpeed = 125.0f;
public float sensitivity = 100.0f;
public float near = 0.1f;
public float far = 100.0f;
public float focalLength = 28.0f;
public float focusDistance = 10.0f;
}
/**
* Creates an automation engine from a JSON specification.
*
@@ -232,13 +229,6 @@ public class AutomationEngine {
return result;
}
@NonNull
public CameraSettings getCameraSettings() {
CameraSettings result = new CameraSettings();
nGetCameraSettings(mNativeObject, result);
return result;
}
/**
* Gets a color grading object that corresponds to the latest settings.
*
@@ -271,9 +261,6 @@ public class AutomationEngine {
*/
public boolean shouldClose() { return nShouldClose(mNativeObject); }
public int getTestCount() { return nGetTestCount(mNativeObject); }
public int getCurrentTest() { return nGetCurrentTest(mNativeObject); }
@Override
protected void finalize() throws Throwable {
nDestroy(mNativeObject);
@@ -293,12 +280,9 @@ public class AutomationEngine {
long[] materials, long ibl, int sunlight, int[] assetLights, long lightManager,
long scene, long renderer);
private static native void nGetViewerOptions(long nativeObject, Object result);
private static native void nGetCameraSettings(long nativeObject, Object result);
private static native long nGetColorGrading(long nativeObject, long nativeEngine);
private static native void nSignalBatchMode(long nativeObject);
private static native void nStopRunning(long nativeObject);
private static native boolean nShouldClose(long nativeObject);
private static native int nGetTestCount(long nativeObject);
private static native int nGetCurrentTest(long nativeObject);
private static native void nDestroy(long nativeObject);
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.graphics.Bitmap;
public class ImageDiff {
public enum Mode {
LEAF, AND, OR
}
public enum Swizzle {
RGBA, BGRA
}
public static class Config {
@NonNull
public Mode mode = Mode.LEAF;
@NonNull
public Swizzle swizzle = Swizzle.RGBA;
public int channelMask = 0xF;
public float maxAbsDiff = 0.0f;
public float maxFailingPixelsFraction = 0.0f;
// Children not supported in this simple wrapper for now, can be added if needed
}
public static class Result {
public enum Status {
PASSED,
SIZE_MISMATCH,
PIXEL_DIFFERENCE
}
public Status status;
public long failingPixelCount;
public float[] maxDiffFound; // [R, G, B, A]
public Bitmap diffImage; // Null if not generated
}
/**
* Compares two bitmaps using a configuration object.
*
* @param reference Golden image
* @param candidate Actual image
* @param config Comparison configuration
* @param mask Optional mask (grayscale)
* @return Result of comparison
*/
@NonNull
public static Result compareBasic(@NonNull Bitmap reference, @NonNull Bitmap candidate,
@NonNull Config config, @Nullable Bitmap mask) {
return nCompareBasic(reference, candidate, config.mode.ordinal(), config.swizzle.ordinal(),
config.channelMask, config.maxAbsDiff, config.maxFailingPixelsFraction, mask);
}
/**
* Compares two bitmaps using a JSON configuration string.
*
* @param reference Golden image
* @param candidate Actual image
* @param jsonConfig Comparison configuration in JSON format
* @param mask Optional mask (grayscale)
* @return Result of comparison
*/
@NonNull
public static Result compare(@NonNull Bitmap reference, @NonNull Bitmap candidate,
@NonNull String jsonConfig, @Nullable Bitmap mask) {
return nCompareJson(reference, candidate, jsonConfig, mask);
}
private static native Result nCompareBasic(Bitmap reference, Bitmap candidate, int mode, int swizzle,
int channelMask, float maxAbsDiff, float maxFailingPixelsFraction, Bitmap mask);
private static native Result nCompareJson(Bitmap reference, Bitmap candidate, String jsonConfig, Bitmap mask);
}
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.69.2
VERSION_NAME=1.68.5
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
@@ -389,9 +389,9 @@ class MainActivity : Activity() {
viewerContent.assetLights = modelViewer.asset?.lightEntities
automation.applySettings(modelViewer.engine, json, viewerContent)
modelViewer.view.colorGrading = automation.getColorGrading(modelViewer.engine)
modelViewer.cameraFocalLength = automation.cameraSettings.focalLength
modelViewer.cameraNear = automation.cameraSettings.near
modelViewer.cameraFar = automation.cameraSettings.far
modelViewer.cameraFocalLength = automation.viewerOptions.cameraFocalLength
modelViewer.cameraNear = automation.viewerOptions.cameraNear
modelViewer.cameraFar = automation.viewerOptions.cameraFar
updateRootTransform()
}
@@ -1,35 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
project.ext.isSample = true
kotlin {
jvmToolchain(versions.jdk)
}
android {
namespace 'com.google.android.filament.validation'
compileSdkVersion versions.compileSdk
defaultConfig {
applicationId "com.google.android.filament.validation"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
}
compileOptions {
sourceCompatibility versions.jdk
targetCompatibility versions.jdk
}
}
dependencies {
implementation deps.kotlin
implementation deps.coroutines.core
implementation project(':filament-android')
implementation project(':gltfio-android')
implementation project(':filament-utils-android')
implementation project(':filament-utils-android')
}
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:label="Filament Validation"
android:supportsRtl="true"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:theme="@android:style/Theme.NoTitleBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -1,154 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.validation
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceView
import android.view.WindowManager
import android.widget.TextView
import android.widget.Toast
import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils
import java.io.File
class MainActivity : Activity(), ValidationRunner.Callback {
companion object {
init {
Utils.init()
System.loadLibrary("filament-utils-jni")
}
private const val TAG = "FilamentValidation"
}
private lateinit var surfaceView: SurfaceView
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView
private var validationRunner: ValidationRunner? = null
// Frame callback
private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
choreographer.postFrameCallback(this)
modelViewer.render(frameTimeNanos)
validationRunner?.onFrame(frameTimeNanos)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Simple layout
val layout = android.widget.FrameLayout(this)
surfaceView = SurfaceView(this)
layout.addView(surfaceView)
statusTextView = TextView(this)
statusTextView.setTextColor(0xFFFFFFFF.toInt())
statusTextView.textSize = 16f
statusTextView.setPadding(20, 20, 20, 20)
statusTextView.text = "Initializing..."
layout.addView(statusTextView)
setContentView(layout)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView)
// Check permissions?
// We assume 'adb install -g' or permissions granted.
// But for scoped storage we might not need PERMISSION if reading from app-specific dirs,
// but user mentioned /sdcard/ so we need MANAGE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE.
// For waiting/simplicity, we just try.
handleIntent()
}
private fun handleIntent() {
val intent = intent
val testConfigPath = intent.getStringExtra("test_config")
if (testConfigPath != null) {
startValidation(testConfigPath)
} else {
statusTextView.text = "No test_config provided via Intent.\nUse -e test_config <path>"
Log.w(TAG, "No test config provided")
}
}
private fun startValidation(configPath: String) {
try {
Log.i(TAG, "Parsing config from $configPath")
val config = ConfigParser.parseFromPath(configPath)
val outputDir = File(getExternalFilesDir(null), "validation_results")
Log.i(TAG, "Output dir: ${outputDir.absolutePath}")
validationRunner = ValidationRunner(this, modelViewer, config, outputDir)
validationRunner?.callback = this
validationRunner?.start()
} catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}"
}
}
override fun onResume() {
super.onResume()
choreographer.postFrameCallback(frameScheduler)
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameScheduler)
}
override fun onDestroy() {
super.onDestroy()
choreographer.removeFrameCallback(frameScheduler)
}
override fun onTestFinished(result: ValidationRunner.TestResult) {
runOnUiThread {
val status = "Test ${result.name} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status
Log.i(TAG, status)
}
}
override fun onAllTestsFinished() {
runOnUiThread {
statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished")
// Optional: Auto-close activity?
// finish()
}
}
override fun onStatusChanged(status: String) {
runOnUiThread {
statusTextView.text = status
}
}
}
@@ -1,166 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.validation
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.IOException
data class RenderTestConfig(
val name: String,
val backends: List<String>,
val models: Map<String, String>, // name -> path
val tests: List<TestConfig>
)
data class TestConfig(
val name: String,
val description: String?,
val backends: List<String>,
val models: Set<String>,
val rendering: JSONObject,
val tolerance: JSONObject?
)
// See test/renderdiff/FORMAT.md for the full specification matched by this parser.
class ConfigParser {
companion object {
fun parseFromPath(path: String): RenderTestConfig {
val file = File(path)
val jsonTxt = removeComments(file.readText())
val json = JSONObject(jsonTxt)
return parseRenderTestConfig(json, file.parentFile)
}
private fun removeComments(json: String): String {
return json.lines().joinToString("\n") { it.substringBefore("//") }
}
private fun parseRenderTestConfig(json: JSONObject, baseDir: File?): RenderTestConfig {
val name = json.getString("name")
val backends = json.getJSONArray("backends").toList<String>()
val modelSearchPaths = json.optJSONArray("model_search_paths")?.toList<String>() ?: emptyList()
val models = mutableMapOf<String, String>()
baseDir?.let { dir ->
modelSearchPaths.forEach { searchPath ->
val searchDir = File(dir, searchPath)
if (searchDir.exists()) {
searchDir.walkTopDown().filter { it.isFile && (it.extension == "glb" || it.extension == "gltf") }.forEach { file ->
models[file.nameWithoutExtension] = file.absolutePath
}
}
}
}
val presetsJson = json.optJSONArray("presets")
val presets = mutableMapOf<String, PresetConfig>()
if (presetsJson != null) {
for (i in 0 until presetsJson.length()) {
val p = parsePreset(presetsJson.getJSONObject(i), models.keys)
presets[p.name] = p
}
}
val testsJson = json.getJSONArray("tests")
val tests = mutableListOf<TestConfig>()
for (i in 0 until testsJson.length()) {
tests.add(parseTestConfig(testsJson.getJSONObject(i), models.keys, presets, backends))
}
return RenderTestConfig(name, backends, models, tests)
}
private fun parsePreset(json: JSONObject, existingModels: Set<String>): PresetConfig {
val name = json.getString("name")
val rendering = json.getJSONObject("rendering")
val models = json.optJSONArray("models")?.toList<String>() ?: emptyList()
// Validate models
models.forEach { if (!existingModels.contains(it)) throw IllegalArgumentException("Model $it not found") }
val tolerance = json.optJSONObject("tolerance")
return PresetConfig(name, rendering, models, tolerance)
}
private fun parseTestConfig(
json: JSONObject,
existingModels: Set<String>,
presets: Map<String, PresetConfig>,
defaultBackends: List<String>
): TestConfig {
val name = json.getString("name")
val description = json.optString("description")
val backends = json.optJSONArray("backends")?.toList<String>() ?: defaultBackends
val applyPresets = json.optJSONArray("apply_presets")?.toList<String>() ?: emptyList()
val rendering = JSONObject()
val combinedModels = mutableSetOf<String>()
var lastTolerance: JSONObject? = null
applyPresets.forEach { presetName ->
val preset = presets[presetName] ?: throw IllegalArgumentException("Unknown preset $presetName")
// Merge rendering (flat copy)
val keys = preset.rendering.keys()
while(keys.hasNext()) {
val k = keys.next()
rendering.put(k, preset.rendering.get(k))
}
combinedModels.addAll(preset.models)
if (preset.tolerance != null) lastTolerance = preset.tolerance
}
val testRendering = json.optJSONObject("rendering")
if (testRendering != null) {
val keys = testRendering.keys()
while(keys.hasNext()) {
val k = keys.next()
rendering.put(k, testRendering.get(k))
}
}
val testModels = json.optJSONArray("models")?.toList<String>() ?: emptyList()
combinedModels.addAll(testModels)
// Validate models
combinedModels.forEach { if (!existingModels.contains(it)) throw IllegalArgumentException("Model $it not found") }
val tolerance = json.optJSONObject("tolerance") ?: lastTolerance
return TestConfig(name, description, backends, combinedModels, rendering, tolerance)
}
}
}
data class PresetConfig(
val name: String,
val rendering: JSONObject,
val models: List<String>,
val tolerance: JSONObject?
)
private inline fun <reified T> JSONArray.toList(): List<T> {
val list = mutableListOf<T>()
for (i in 0 until length()) {
list.add(get(i) as T)
}
return list
}
@@ -1,327 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.validation
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.google.android.filament.Engine
import com.google.android.filament.Renderer
import com.google.android.filament.View
import com.google.android.filament.utils.AutomationEngine
import com.google.android.filament.utils.ImageDiff
import com.google.android.filament.utils.ModelViewer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class ValidationRunner(
private val context: Context,
private val modelViewer: ModelViewer,
private val config: RenderTestConfig,
private val outputDir: File
) {
private var currentState = State.IDLE
private var currentTestIndex = 0
private var currentModelIndex = 0
private var currentEngine: AutomationEngine? = null
private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null
private var loadStartFence: com.google.android.filament.Fence? = null
private var loadStartTime = 0L
enum class State {
IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
RUNNING_TEST,
COMPARING
}
interface Callback {
fun onTestFinished(result: TestResult)
fun onAllTestsFinished()
fun onStatusChanged(status: String)
}
var callback: Callback? = null
fun start() {
if (config.tests.isEmpty()) {
callback?.onAllTestsFinished()
return
}
currentTestIndex = 0
currentModelIndex = 0
startTest(config.tests[0])
}
private fun startTest(test: TestConfig) {
currentTestConfig = test
if (test.models.isEmpty()) {
nextTest()
return
}
currentModelIndex = 0
startModel(test.models.elementAt(0))
}
private fun startModel(modelName: String) {
currentModelName = modelName
val modelPath = config.models[modelName]
if (modelPath == null) {
Log.e("ValidationRunner", "Model $modelName not found")
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
// We assume this is called from main thread or we dispatch
loadModel(modelPath)
}
private fun loadModel(path: String) {
// Assume called on Main Thread
modelViewer.destroyModel()
try {
val bytes = File(path).readBytes()
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
modelViewer.transformToUnitCube()
loadStartFence = modelViewer.engine.createFence()
loadStartTime = System.nanoTime()
currentState = State.WAITING_FOR_FENCE
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
}
}
fun onFrame(frameTimeNanos: Long) {
when (currentState) {
State.IDLE -> {}
State.WAITING_FOR_FENCE -> {
loadStartFence?.let { fence ->
if (fence.wait(com.google.android.filament.Fence.Mode.FLUSH, 0) == com.google.android.filament.Fence.FenceStatus.CONDITION_SATISFIED) {
modelViewer.engine.destroyFence(fence)
loadStartFence = null
// Compile materials (simplified)
modelViewer.scene.forEach { entity ->
// ... existing material compilation logic ...
}
startAutomation()
}
}
}
State.RUNNING_TEST -> {
currentEngine?.let { engine ->
val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view
content.renderer = modelViewer.renderer
content.scene = modelViewer.scene
content.lightManager = modelViewer.engine.lightManager
// Tick
// Delta time?
val deltaTime = 1.0f / 60.0f // Fixed step for consistency?
engine.tick(modelViewer.engine, content, deltaTime)
if (!engine.isRunning) {
// Test finished (for this spec)
currentState = State.COMPARING
captureAndCompare()
}
}
}
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
}
}
private fun startAutomation() {
val test = currentTestConfig!!
val specJson = JSONObject()
specJson.put("name", test.name)
specJson.put("base", test.rendering)
val fullSpec = "[${specJson.toString()}]"
currentEngine = AutomationEngine(fullSpec)
val options = AutomationEngine.Options()
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
options.minFrameCount = 5 // Ensure some frames pass
currentEngine?.setOptions(options)
currentEngine?.startRunning()
currentState = State.RUNNING_TEST
}
private fun captureAndCompare() {
callback?.onStatusChanged("Comparing ${currentTestConfig?.name}...")
val view = modelViewer.view
val renderer = modelViewer.renderer
val width = view.viewport.width
val height = view.viewport.height
val buffer = ByteBuffer.allocateDirect(width * height * 4)
val pbd = com.google.android.filament.Texture.PixelBufferDescriptor(
buffer,
com.google.android.filament.Texture.Format.RGBA,
com.google.android.filament.Texture.Type.UBYTE,
1, 0, 0, 0, 0, // alignment, left, top, stride (0=default)
null // handler (null = current thread? no, handler is for callback)
) {
// Callback when readPixels is done
// Dispatch to background thread for comparison to avoid blocking UI?
// "it" is undefined here? The callback interface is Runnable?
// Kotlin lambda for Runnable.
compareCapturedImage(buffer, width, height)
}
renderer.readPixels(0, 0, width, height, pbd)
}
private fun compareCapturedImage(buffer: java.nio.Buffer, width: Int, height: Int) {
// This runs on... which thread? Filament driver thread possibly.
// We should use a helper to process.
val testName = currentTestConfig!!.name
val modelName = currentModelName!!
val backend = "opengl" // Hardcoded for now, or get from View/Engine?
val testFullName = "${testName}.${backend}.${modelName}"
// Golden path
// We expect a golden directory.
val goldenFile = File(config.models.get(modelName)!!).parentFile.parentFile.resolve("golden/${testFullName}.png")
// Strategy: models are in .../models/model.glb
// Goldens are in .../golden/
Thread {
try {
// Convert buffer to Bitmap
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
// Flip Y? ReadPixels is typically bottom-up?
// Filament readPixels is bottom-left? YES.
// Bitmap is top-left.
// We need to flip.
val matrix = android.graphics.Matrix()
matrix.postScale(1f, -1f)
val flipped = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true)
var passed = false
if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
// Populate tolerance from config
val tol = currentTestConfig?.tolerance ?: org.json.JSONObject()
val tolJson = tol.toString()
val result = ImageDiff.compare(golden, flipped, tolJson, null)
passed = (result.status == ImageDiff.Result.Status.PASSED)
// Save diff if failed?
if (!passed) {
val diffFile = File(outputDir, "${testFullName}_diff.png")
if (result.diffImage != null) {
FileOutputStream(diffFile).use { out ->
result.diffImage.compress(Bitmap.CompressFormat.PNG, 100, out)
}
}
}
} else {
Log.e("ValidationRunner", "Failed to load golden: ${goldenFile.absolutePath}")
}
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
}
// Save output
val outFile = File(outputDir, "${testFullName}.png")
FileOutputStream(outFile).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
callback?.onTestFinished(TestResult(testFullName, passed))
// Schedule next model on main thread
// Use Handler or View.post
modelViewer.view.viewport
// dispatch nextModel()
android.os.Handler(android.os.Looper.getMainLooper()).post {
nextModel()
}
} catch (e: Exception) {
Log.e("ValidationRunner", "Comparison failed", e)
android.os.Handler(android.os.Looper.getMainLooper()).post { nextModel() }
}
}.start()
}
private fun nextModel() {
currentModelIndex++
if (currentTestConfig != null && currentModelIndex < currentTestConfig!!.models.size) {
startModel(currentTestConfig!!.models.elementAt(currentModelIndex))
} else {
nextTest()
}
}
private fun nextTest() {
currentTestIndex++
if (currentTestIndex < config.tests.size) {
startTest(config.tests[currentTestIndex])
} else {
currentState = State.IDLE
zipResults()
callback?.onAllTestsFinished()
}
}
private fun zipResults() {
callback?.onStatusChanged("Zipping results...")
val zipFile = File(outputDir, "results.zip")
try {
java.util.zip.ZipOutputStream(java.io.FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(java.util.zip.ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i("ValidationRunner", "Zipped results to ${zipFile.absolutePath}")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to zip results", e)
}
}
data class TestResult(val name: String, val passed: Boolean)
-1
View File
@@ -21,6 +21,5 @@ include ':samples:sample-texture-view'
include ':samples:sample-texture-target'
include ':samples:sample-textured-object'
include ':samples:sample-transparent-view'
include ':samples:sample-render-validation'
rootProject.name = 'filament'
+19
View File
@@ -214,6 +214,8 @@ ENABLE_PERFETTO=""
BACKEND_DEBUG_FLAG_OPTION=""
STEREOSCOPIC_OPTION=""
OSMESA_OPTION=""
IOS_BUILD_SIMULATOR=false
@@ -314,6 +316,7 @@ function build_desktop_target {
${ASAN_UBSAN_OPTION} \
${COVERAGE_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${OSMESA_OPTION} \
${architectures} \
../..
@@ -452,6 +455,7 @@ function build_android_target {
${VULKAN_ANDROID_OPTION} \
${WEBGPU_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${ENABLE_PERFETTO} \
../..
ln -sf "out/cmake-android-${lc_target}-${arch}/compile_commands.json" \
@@ -693,6 +697,7 @@ function build_ios_target {
${WEBGPU_OPTION} \
${MATDBG_OPTION} \
${MATOPT_OPTION} \
${STEREOSCOPIC_OPTION} \
../..
ln -sf "out/cmake-ios-${lc_target}-${arch}/compile_commands.json" \
../../compile_commands.json
@@ -1006,6 +1011,20 @@ while getopts ":hacCfgimp:q:uvWslwedtk:bVx:S:X:Py:" opt; do
;;
x) BACKEND_DEBUG_FLAG_OPTION="-DFILAMENT_BACKEND_DEBUG_FLAG=${OPTARG}"
;;
S) case $(echo "${OPTARG}" | tr '[:upper:]' '[:lower:]') in
instanced)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=instanced"
;;
multiview)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=multiview"
;;
*)
echo "Unknown stereoscopic type ${OPTARG}"
echo "Type must be one of [instanced|multiview]"
echo ""
exit 1
esac
;;
X) OSMESA_OPTION="-DFILAMENT_OSMESA_PATH=${OPTARG}"
;;
y)
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.0'
implementation 'com.google.android.filament:filament-android:1.68.5'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -196,7 +196,7 @@ dependencies {
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.0'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.68.5'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+2 -2
View File
@@ -352,7 +352,7 @@ used to create the SPIR-V is not available.</p>
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../dup/viewer.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next prefetch" href="../dup/uberz.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
@@ -366,7 +366,7 @@ used to create the SPIR-V is not available.</p>
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../dup/viewer.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next prefetch" href="../dup/uberz.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
+2 -2
View File
@@ -236,7 +236,7 @@ mapping should be specified as an <code>optional</code> feature of the ubershade
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../dup/viewer.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a rel="prev" href="../dup/matdbg.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
@@ -250,7 +250,7 @@ mapping should be specified as an <code>optional</code> feature of the ubershade
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../dup/viewer.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a rel="prev" href="../dup/matdbg.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
-333
View File
@@ -1,333 +0,0 @@
<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>viewer - Filament</title>
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('light')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div style="display:flex;align-items:center;justify-content:center">
<img class="flogo" src="../images/filament_logo_small.png"></img>
</div>
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<!-- Filament: disable themes because the markdeep part does not look good for dark themes -->
<!--
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
-->
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Filament</h1>
<div class="right-buttons">
<a href="https://github.com/google/filament" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="viewer-library"><a class="header" href="#viewer-library">Viewer Library</a></h1>
<p>The <strong>Viewer Library</strong> (<code>libs/viewer</code>) provides a high-level abstraction for configuring and rendering Filament scenes. It is used by tools like <code>gltf_viewer</code> to load assets, manage settings, and drive the rendering loop.</p>
<h2 id="features"><a class="header" href="#features">Features</a></h2>
<ul>
<li><strong>Settings Management</strong>: Centralized configuration for View, Camera, Lights, and Materials via the <code>Settings</code> struct.</li>
<li><strong>JSON Serialization</strong>: Full support for loading and saving settings via JSON.</li>
<li><strong>Automation</strong>: <code>AutomationEngine</code> allows scripting the viewer with a sequence of JSON-based test cases (batch mode).</li>
<li><strong>GUI Integration</strong>: Built-in support for <code>imgui</code> via <code>ViewerGui</code> and <code>Settings</code> binding.</li>
</ul>
<h2 id="json-settings-schema"><a class="header" href="#json-settings-schema">JSON Settings Schema</a></h2>
<p>The viewer settings can be configured using a JSON object. This is used for <code>gltf_viewer --settings</code> or in automation specs.</p>
<h3 id="root-object"><a class="header" href="#root-object">Root Object</a></h3>
<p>The root object contains the following categories:</p>
<div class="table-wrapper"><table><thead><tr><th style="text-align: left">Key</th><th style="text-align: left">Type</th><th style="text-align: left">Description</th></tr></thead><tbody>
<tr><td style="text-align: left"><code>view</code></td><td style="text-align: left">Object</td><td style="text-align: left">Post-processing and rendering quality settings.</td></tr>
<tr><td style="text-align: left"><code>camera</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Explicit camera control (pose, projection, exposure).</td></tr>
<tr><td style="text-align: left"><code>lighting</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Environment and dynamic light settings.</td></tr>
<tr><td style="text-align: left"><code>viewer</code></td><td style="text-align: left">Object</td><td style="text-align: left">Global viewer options (skybox, background, scaling).</td></tr>
<tr><td style="text-align: left"><code>animation</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Animation playback control.</td></tr>
<tr><td style="text-align: left"><code>material</code></td><td style="text-align: left">Object</td><td style="text-align: left">Material overrides.</td></tr>
</tbody></table>
</div>
<hr />
<h3 id="camera-settings-camera"><a class="header" href="#camera-settings-camera">Camera Settings (<code>camera</code>)</a></h3>
<p>Allows explicit control over the camera. If <code>enabled</code> is false, the viewer uses its default orbit camera logic (auto-scaling/centering).</p>
<pre><code class="language-json">"camera": {
"enabled": true, // Must be true to use these explicit settings
"projection": "PERSPECTIVE", // "PERSPECTIVE" or "ORTHO"
"center": [0, 0, 0], // World-space look-at point
"lookAt": [0, 0, -1], // World-space eye position (confusingly named 'lookAt' in internal legacy, often 'eye')
"up": [0, 1, 0], // Up vector
"near": 0.1, // Near plane
"far": 100.0, // Far plane
"focalLength": 28.0, // Focal length in mm (Perspective only)
"fov": 0.0, // Field of view in degrees (overrides focalLength if &gt; 0)
"aperture": 16.0, // f-stop
"shutterSpeed": 125.0, // 1/seconds
"sensitivity": 100.0, // ISO
"focusDistance": 10.0, // Focus distance in world units
"scaling": [1.0, 1.0], // Custom projection matrix scaling (mostly for Ortho)
"shift": [0.0, 0.0] // Custom projection matrix shift
}
</code></pre>
<h3 id="lighting-settings-lighting"><a class="header" href="#lighting-settings-lighting">Lighting Settings (<code>lighting</code>)</a></h3>
<p>Controls the Image Based Lighting (IBL), the Sun, and additional dynamic lights.</p>
<pre><code class="language-json">"lighting": {
"iblIntensity": 30000.0,
"iblRotation": 0.0, // Rotation in degrees
"enableSunlight": true,
"enableShadows": true,
"sunlight": { // **[NEW]** Nested sunlight properties
"intensity": 100000.0,
"color": [0.98, 0.92, 0.89],
"direction": [0.6, -1.0, -0.8],
"sunHaloSize": 10.0,
"sunHaloFalloff": 80.0,
"sunAngularRadius": 1.9,
"castShadows": true,
"shadowOptions": { // Per-light shadow options
"mapSize": 1024,
"shadowCascades": 1,
"stable": false
}
},
"lights": [ // **[NEW]** Array of custom lights
{
"type": "POINT", // "POINT", "SPOT", "FOCUSED_SPOT", "DIRECTIONAL", "SUN"
"position": [0, 2, 0],
"color": [1, 0, 0],
"intensity": 5000.0,
"falloff": 10.0,
"castShadows": true,
"shadowOptions": { "mapSize": 512 }
},
{
"type": "SPOT",
"position": [2, 5, 2],
"direction": [0, -1, 0],
"spotInner": 0.5, // Inner cone angle (radians)
"spotOuter": 0.8 // Outer cone angle (radians)
}
]
}
</code></pre>
<h3 id="view-settings-view"><a class="header" href="#view-settings-view">View Settings (<code>view</code>)</a></h3>
<p>Standard Filament view settings.</p>
<pre><code class="language-json">"view": {
"postProcessingEnabled": true,
"antiAliasing": "FXAA", // "NONE", "FXAA"
"msaa": {
"enabled": true,
"sampleCount": 4
},
"ssao": { "enabled": true, ... },
"bloom": { "enabled": true, ... },
"dof": { "enabled": false, ... },
"vignette": { "enabled": false, ... },
"colorGrading": {
"toneMapping": "ACES_LEGACY", // "LINEAR", "ACES", "FILMIC", "PBR_NEUTRAL", etc.
"exposure": 0.0,
"gamma": [1.0, 1.0, 1.0]
}
}
</code></pre>
<h3 id="viewer-options-viewer"><a class="header" href="#viewer-options-viewer">Viewer Options (<code>viewer</code>)</a></h3>
<p>General app-level settings.</p>
<pre><code class="language-json">"viewer": {
"skyboxEnabled": true,
"backgroundColor": [0, 0, 0], // Used if skybox is disabled
"autoScaleEnabled": true, // Fit model to unit cube
"groundPlaneEnabled": false
}
</code></pre>
<h3 id="animation-settings-animation"><a class="header" href="#animation-settings-animation">Animation Settings (<code>animation</code>)</a></h3>
<p>Control glTF animation playback.</p>
<pre><code class="language-json">"animation": {
"enabled": true,
"speed": 1.0,
"time": -1.0 // If &gt;= 0, forces animation to this specific time (seconds)
}
</code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../dup/matdbg.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../dup/uberz.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../dup/matdbg.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../dup/uberz.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
# Building Skybox Assets
This directory contains the source code for the Simulated Skybox sample. The assets (material and textures) are pre-built in the `assets/` directory.
If you need to modify the material or regenerate the moon textures, you can use the scripts provided in the `tools/` directory.
## Prerequisites
- **Filament**: You must have a built version of Filament. The scripts assume a standard CMake build output structure (e.g., `out/cmake-release`).
- **Python 3**: Required for texture generation.
- **Python Dependencies**: `numpy`, `Pillow` (automatically installed if missing, via pip).
## Rebuilding the Material
If you modify `simulated_skybox.mat`, you must recompile it into a `.filamat` file.
```bash
# Run from the sample root or tools directory
./tools/build_material.sh
```
This will update `assets/simulated_skybox.filamat`.
## Regenerating Moon Textures
If you want to change the moon's resolution, bump scale, or blur, you can regenerate the textures.
```bash
# Run from the sample root or tools directory
./tools/generate_moon_assets.sh [size]
```
Example:
```bash
./tools/generate_moon_assets.sh 512
```
This will download raw NASA data (if not present) and generate:
- `assets/moon_disk.png`
- `assets/moon_normal.png`
-76
View File
@@ -1,76 +0,0 @@
# Analytic Skybox Sample
This sample demonstrates a **fully procedural, single-pass skybox shader** capable of simulating a dynamic day-night cycle, atmospheric scattering, volumetric clouds, and water reflections.
It is designed for graphics engineers and technical artists who need a lightweight yet physically plausible environment background without relying on static HDRI textures.
## Features & Performance
The shader uses a "Uber-Shader" approach where all features are computed per-pixel. Features can be toggled or tuned via uniforms to balance quality vs performance.
| Feature | Cost | Control | Description |
| :--- | :---: | :--- | :--- |
| **Atmosphere** | 🟡 **Medium** | `turbidity`, `rayleigh`, `mie` | Analytic Rayleigh & Mie scattering. Physically based colors. |
| **Sun Disk** | 🟢 **Low** | `sunHalo` (Radius, Limb) | Analytic sphere intersection with limb darkening. Conservation of energy (Lux). |
| **Moon & Earthshine** | 🟢 **Low** | `sunHalo2`, `moonIntensity` | Resolved moon disk with geometric phases and dynamic Earthshine. |
| **Stars** | 🟢 **Low** | `starControl` (Density) | High-frequency procedural noise. Occluded by clouds/moon. |
| **Clouds** | 🔴 **High** | `cloudControl` (Coverage, Density) | 4-Octave 3D Fractal Brownian Motion (FBM). Dominates the cost when enabled. |
| **Heat Shimmer** | 🟡 **Medium** | `shimmerControl` | UV perturbation near the horizon to simulate mirages. |
| **Water Reflection** | 🟣 **Very High** | `waterControl` | **Renders the sky twice**. Includes procedural waves (FBM) and fresnel. |
> **Note**: Rendering water (`V.y < 0`) is significantly more expensive (~2.5x) than the sky because it requires re-evaluating the atmospheric scattering and cloud noise for the reflection vector.
## Shader Techniques
### 1. Analytic Atmospheric Scattering
Based on the **Hoffman & Preetham** model. It solves the single-scattering integral analytically for air molecules (Rayleigh) and aerosols (Mie).
- **Rayleigh**: Produces the deep blue sky and red sunset colors.
- **Mie**: Produces the white halo around the sun and general haziness.
- **Optimization**: Uses a simplified optical depth approximation ("Air Mass") to avoid expensive ray-marching.
### 2. Procedural Clouds (3D Noise)
Clouds are rendered as a spherical shell at a specific altitude.
- **Technique**: Ray-sphere intersection finds the entry point, then **3D FBM Noise** determines density.
- **Lighting**: Uses a "Silver Lining" approximation (strong forward scattering) and Beers-Lambert attenuation for dark underbellies.
- **Animation**: The noise coordinate logic helps simulate wind drift and shape evolution over time.
### 3. Infinite Water Ocean
When looking below the horizon, the shader switches to "Water Mode".
- **Geometry**: A flat plane at $y=0$.
- **Waves**: Generated using **Derivative-Based Noise** (or Finite Difference). This creates slope vectors that perturb the normal without needing actual geometry.
- **Reflection**: A ray is cast from the water surface back into the sky ($R = \text{reflect}(V, N)$). The sky function is called again with $R$ to get the reflected color.
### 4. Dynamic Tone Mapping
Applies a custom tone mapping curve that varies with Sun Elevation.
- **Noon**: Linear/Gamma (Standard).
- **Sunset**: Higher contrast curve to compress the dynamic range and enhance the rich sunset oranges/purples.
## Integration
To use this in your own Filament application:
1. **Compile the Material**:
Use `matc` to compile `simulated_skybox.mat` into a `.filamat` file.
```bash
matc -p mobile -a opengl -o assets/simulated_skybox.filamat simulated_skybox.mat
```
2. **Load in JavaScript/C++**:
Create a Skybox entity and assign the material.
```javascript
// JavaScript Example
const material = engine.createMaterial('assets/simulated_skybox.filamat');
const skybox = engine.createSkybox(material);
scene.setSkybox(skybox);
```
3. **Update Uniforms**:
The shader requires specific uniforms (Sun Direction, Time, etc.) to be updated every frame. See `SimulatedSkybox.js` for a reference implementation of the uniform buffer management.
## References
* **Hoffman & Preetham (2002)**: *"Real-time Light-Atmosphere Interactions"*
* **Henyey & Greenstein (1941)**: *"Diffuse radiation in the galaxy"* (Mie Phase Function)
* **Kasten & Young (1989)**: *"Revised optical air mass tables"*
* **Three.js / Sky.js**: Empirical adjustments for "Golden Hour" aesthetics.
+8 -403
View File
@@ -21,31 +21,13 @@ class SimulatedSkybox {
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.focalLength = 24.0;
this.height = 1000.0;
this.starControl = [1.0, 1.0]; // x=Density (0-1), y=Enabled (0-1)
this.planetRadius = 6360.0;
// Sun Halo
// x=cos(rad), y=limbDarkening, z=intensity, w=enabled
// Sun Halo
this.sunHalo = [Math.cos(0.5 * Math.PI / 180.0), 0.5, 1.0, 1.0];
// Moon Parameters (Mapped to Secondary Sun)
this.moonDirection = [-0.2, 0.8, -0.2]; // Default Moon Pos
this.moonIntensity = 1.0; // Scale Factor (1.0 = Physical Peak)
// x=cos(rad), y=sin(rad) [Precision Fix], z=intensity, w=enabled
this.moonHalo = [Math.cos(0.5 * Math.PI / 180.0), Math.sin(0.5 * Math.PI / 180.0), 1.0, 0.0]; // Disabled by default
this.moonTextureObj = null;
this.moonNormalObj = null;
this.milkyWayTextureObj = null;
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
}
@@ -62,184 +44,6 @@ class SimulatedSkybox {
rcm.setMaterialInstanceAt(instance, 0, this.materialInstance);
console.log("Material loaded and bound.");
// Load Moon Texture
try {
const texUrl = 'assets/moon_disk.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.moonTextureObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.SRGB8_A8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly (Uint8ClampedArray)
// Filament supports RGBA upload for RGB/SRGB internal formats (drops alpha).
// This matches Canvas layout (Top-Down, RGBA) and is 4-byte aligned.
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer), // Wrap buffer to ensure Uint8Array
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.moonTextureObj.setImage(this.engine, 0, pbd);
this.moonTextureObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR,
MagFilter.LINEAR,
WrapMode.CLAMP_TO_EDGE
);
this.materialInstance.setTextureParameter('moonTexture', this.moonTextureObj, sampler);
} catch (e) {
console.error("Failed to load moon texture:", e);
}
// Load Moon Normal
try {
const texUrl = 'assets/moon_normal.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.moonNormalObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.RGBA8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer),
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.moonNormalObj.setImage(this.engine, 0, pbd);
this.moonNormalObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR_MIPMAP_LINEAR,
MagFilter.LINEAR,
WrapMode.CLAMP_TO_EDGE
);
this.materialInstance.setTextureParameter('moonNormal', this.moonNormalObj, sampler);
} catch (e) {
console.error("Failed to load moon normal:", e);
}
// Load Milky Way Texture
try {
const texUrl = 'assets/milkyway.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.milkyWayTextureObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.SRGB8_A8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
try {
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer),
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.milkyWayTextureObj.setImage(this.engine, 0, pbd);
this.milkyWayTextureObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR_MIPMAP_LINEAR,
MagFilter.LINEAR,
WrapMode.REPEAT // Equirectangular wraps horizontally
);
this.materialInstance.setTextureParameter('milkyWayTexture', this.milkyWayTextureObj, sampler);
} catch (err) {
console.warn("Milky Way texture data access failed (CORS?):", err);
}
} catch (e) {
console.error("Failed to load milky way texture:", e);
}
this.updateCoefficients();
}
@@ -280,16 +84,16 @@ class SimulatedSkybox {
this.ib.setBuffer(this.engine, TRIANGLE_INDICES);
// Build the Renderable without material; it will be set later by loadMaterial.
// We create a dummy material first or wait?
// In JS we usually can't block. We'll rely on loadMaterial being called.
// For now, we build the Renderable without material, then set it later.
RenderableManager.Builder(1)
.geometry(0, PrimitiveType.TRIANGLES, this.vb, this.ib)
.culling(false)
.castShadows(false)
.receiveShadows(false)
.priority(7) // Skybox priority
.priority(7) // Render behind translucent objects? 7 is skybox priority typically.
.build(this.engine, this.entity);
}
@@ -411,95 +215,11 @@ class SimulatedSkybox {
}
setStarControl(density, enabled) {
// Compensate for grid frequency reduction (350 -> 100)
// Fewer cells = fewer stars, so we increase density threshold.
// Factor ~ (350/100)^2 = 12.25
const compensatedDensity = density * 12.0;
this.starControl[0] = Math.max(0.0, Math.min(1.0, compensatedDensity));
this.starControl[0] = Math.max(0.0, Math.min(1.0, density));
this.starControl[1] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
setFocalLength(mm) {
this.focalLength = Math.max(1.0, mm);
this.updateStarFrequency();
}
setResolution(height) {
this.height = Math.max(1.0, height);
this.updateStarFrequency();
}
updateStarFrequency() {
// World-Anchored Stars
// z = Fixed Frequency (World Space Grid)
// w = Pixel Scale (Screen Space Radius)
// Fixed Frequency: Defines the "Universe" coordinate system.
// Reduced to 100.0 to allow larger stars without clipping (square artifacts).
this.starControl[2] = 100.0;
// Pixel Scale in Radians
// We use linear scaling (24/f) instead of atan(fov) to ensure star size remains
// constant in pixels across all focal lengths (Perspective Projection).
const fovFactor = 24.0 / this.focalLength;
const pixelScale = (1.0 / this.height) * fovFactor;
// Pass to shader (w component)
// Target radius: 1.3 pixels (Diameter 2.6 pixels)
// Visible but sharp.
this.starControl[3] = pixelScale * 1.3;
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
if (len > 0) {
this.moonDirection = [direction[0] / len, direction[1] / len, direction[2] / len];
}
this.updateCoefficients();
}
setMoonIntensity(intensity) {
this.moonIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonRadius(degrees) {
const rad = degrees * (Math.PI / 180.0);
this.moonHalo[0] = Math.cos(rad);
this.moonHalo[1] = Math.sin(rad);
this.updateCoefficients();
}
setMoonEnabled(enabled) {
this.moonHalo[3] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
setMilkyWayControl(intensity, saturation, blackPoint) {
this.milkyWayControl[0] = Math.max(0.0, intensity);
this.milkyWayControl[1] = Math.max(0.0, saturation);
if (blackPoint !== undefined) {
this.milkyWayControl[2] = Math.max(0.0, blackPoint);
}
this.updateCoefficients();
}
setMilkyWayEnabled(enabled) {
this.milkyWayEnabled = !!enabled;
this.updateCoefficients();
}
setMilkyWayRotation(rotationMatrix) {
if (rotationMatrix && rotationMatrix.length === 9) {
this.milkyWayRotation = rotationMatrix;
this.updateCoefficients();
}
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -577,130 +297,15 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('contrast', this.contrast);
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity); // Lux scaling
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity);
this.materialInstance.setFloat3Parameter('nightColor', new Float32Array(nightColorScaled));
this.materialInstance.setFloat4Parameter('shimmerControl', new Float32Array(shimmerUniform));
this.materialInstance.setFloat4Parameter('cloudControl', new Float32Array(cloudUniform));
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloat2Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
// Moon Upload (Secondary Sun)
this.materialInstance.setFloat3Parameter('sunDirection2', new Float32Array(this.moonDirection));
// Calculate Moon Phase Factor (Lambertian Sphere)
// We model the moon as a Lambertian sphere to calculate its integrated brightness (illuminance)
// based on the phase angle (angle between Sun-Moon and Observer-Moon vectors).
//
// Phase Angle (alpha):
// For a distant observer (Earth), the phase angle can be approximated as the angle between
// the vector to the Sun and the vector to the Earth (from the Moon).
// cos(alpha) = -dot(L_moon, L_sun)
//
// Lambertian Phase Law:
// The integrated flux of a lit sphere varies as:
// Phi(alpha) = (1/PI) * (sin(alpha) + (PI - alpha) * cos(alpha))
// This gives 1.0 at Full Moon (alpha=0) and 0.0 at New Moon (alpha=PI).
const dotSM = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
// Final Intensity = Peak * Scale (No Phase Factor - Phase is handled in Shader via N.L)
const MOON_PEAK_LUX = 1.0;
const finalMoonIntensity = MOON_PEAK_LUX * this.moonIntensity;
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
this.materialInstance.setFloat3Parameter('milkyWayControl', new Float32Array(mwUniform));
this.materialInstance.setMat3Parameter('milkyWayRotation', new Float32Array(this.milkyWayRotation));
// Moon Halo Upload (Disk Visualization)
// Multiplier = 1.0 / SolidAngle
const moonSolidAngle = 2.0 * F_PI * (1.0 - this.moonHalo[0]);
const moonRadConv = 1.0 / Math.max(1e-9, moonSolidAngle);
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);
const moonRadius = Math.acos(this.moonHalo[0]);
// Dot product of Sun and Moon directions
const dot = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
const separation = Math.acos(Math.max(-1.0, Math.min(1.0, dot)));
let eclipseFactor = 1.0;
// Only calculate if moon is enabled
if (this.moonHalo[3] > 0.5) {
const overlap = this.areaIntersection(sunRadius, moonRadius, separation);
const sunArea = Math.PI * sunRadius * sunRadius;
// Ensure we don't divide by zero and clamp result
const ratio = overlap / Math.max(1e-9, sunArea);
eclipseFactor = 1.0 - Math.max(0.0, Math.min(1.0, ratio));
}
// Safety check for NaN
if (isNaN(eclipseFactor)) {
console.warn("SimulatedSkybox: eclipseFactor is NaN, resetting to 1.0");
eclipseFactor = 1.0;
}
this.materialInstance.setFloatParameter('eclipseFactor', eclipseFactor);
}
areaIntersection(r1, r2, d) {
// Circle intersection area
// r1, r2: radii
// d: distance between centers
// Case 1: Too far apart
if (d >= r1 + r2) {
return 0.0;
}
// Case 2: One inside another
if (d <= Math.abs(r1 - r2)) {
return Math.PI * Math.min(r1, r2) * Math.min(r1, r2);
}
const r1sq = r1 * r1;
const r2sq = r2 * r2;
// Law of Cosines for sector angles
// c1 = (d^2 + r1^2 - r2^2) / (2 * d * r1)
// c2 = (d^2 + r2^2 - r1^2) / (2 * d * r2)
// We clamp to [-1, 1] to avoid NaN from floating point errors
const c1 = Math.max(-1.0, Math.min(1.0, (d * d + r1sq - r2sq) / (2.0 * d * r1)));
const c2 = Math.max(-1.0, Math.min(1.0, (d * d + r2sq - r1sq) / (2.0 * d * r2)));
const part1 = r1sq * Math.acos(c1);
const part2 = r2sq * Math.acos(c2);
// Heron's formula for the triangle area * 2
// The sqrt term represents the area of the two triangles formed by the chord and centers.
// Robust sqrt
const val = (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2);
const part3 = 0.5 * Math.sqrt(Math.max(0.0, val));
return part1 + part2 - part3;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.
-332
View File
@@ -1,332 +0,0 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
*/
(function () { 'use strict';
// shortcuts for easier to read formulas
var PI = Math.PI,
sin = Math.sin,
cos = Math.cos,
tan = Math.tan,
asin = Math.asin,
atan = Math.atan2,
acos = Math.acos,
rad = PI / 180;
// sun calculations are based on https://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
var dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
function toDays(date) { return toJulian(date) - J2000; }
// general calculations for position
var e = rad * 23.4397; // obliquity of the Earth
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
function astroRefraction(h) {
if (h < 0) // the following formula works for positive altitudes only.
h = 0; // if h = -0.08901179 a div/0 would occur.
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
}
// general sun calculations
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
function eclipticLongitude(M) {
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
P = rad * 102.9372; // perihelion of the Earth
return M + C + P + PI;
}
function sunCoords(d) {
var M = solarMeanAnomaly(d),
L = eclipticLongitude(M);
return {
dec: declination(L, 0),
ra: rightAscension(L, 0)
};
}
var SunCalc = {};
// calculates sun position for a given date and latitude/longitude
SunCalc.getPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = sunCoords(d),
H = siderealTime(d, lw) - c.ra;
return {
azimuth: azimuth(H, phi, c.dec),
altitude: altitude(H, phi, c.dec)
};
};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[-0.3, 'sunriseEnd', 'sunsetStart' ],
[-6, 'dawn', 'dusk' ],
[-12, 'nauticalDawn', 'nauticalDusk'],
[-18, 'nightEnd', 'night' ],
[6, 'goldenHourEnd', 'goldenHour' ]
];
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// calculations for sun times
var J0 = 0.0009;
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
// returns set time for the given sun altitude
function getSetJ(h, lw, phi, dec, n, M, L) {
var w = hourAngle(h, phi, dec),
a = approxTransit(w, lw, n);
return solarTransitJ(a, M, L);
}
// calculates sun times for a given date, latitude/longitude, and, optionally,
// the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height) {
height = height || 0;
var lw = rad * -lng,
phi = rad * lat,
dh = observerAngle(height),
d = toDays(date),
n = julianCycle(d, lw),
ds = approxTransit(0, lw, n),
M = solarMeanAnomaly(ds),
L = eclipticLongitude(M),
dec = declination(L, 0),
Jnoon = solarTransitJ(ds, M, L);
var result = {
solarNoon: fromJulian(Jnoon),
nadir: fromJulian(Jnoon - 0.5)
};
var i, len, time, h0, Jset, Jrise;
for (i = 0, len = times.length; i < len; i += 1) {
time = times[i];
h0 = (time[0] + dh) * rad;
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
Jrise = Jnoon - (Jset - Jnoon);
result[time[1]] = fromJulian(Jrise);
result[time[2]] = fromJulian(Jset);
}
return result;
};
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
M = rad * (134.963 + 13.064993 * d), // mean anomaly
F = rad * (93.272 + 13.229350 * d), // mean distance
l = L + rad * 6.289 * sin(M), // longitude
b = rad * 5.128 * sin(F), // latitude
dt = 385001 - 20905 * cos(M); // distance to the moon in km
return {
ra: rightAscension(l, b),
dec: declination(l, b),
dist: dt
};
}
SunCalc.getMoonPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = moonCoords(d),
H = siderealTime(d, lw) - c.ra,
h = altitude(H, phi, c.dec),
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
return {
azimuth: azimuth(H, phi, c.dec),
altitude: h + astroRefraction(h), // altitude correction for refraction,
distance: c.dist,
parallacticAngle: pa
};
};
// calculations for illumination parameters of the moon,
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
SunCalc.getMoonIllumination = function (date) {
var d = toDays(date || new Date()),
s = sunCoords(d),
m = moonCoords(d),
sdist = 149598000, // distance from Earth to Sun in km
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
return {
fraction: (1 + cos(inc)) / 2,
phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
angle: angle
};
};
function hoursLater(date, h) {
return new Date(date.valueOf() + h * dayMs / 24);
}
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
var t = new Date(date);
if (inUTC) t.setUTCHours(0, 0, 0, 0);
else t.setHours(0, 0, 0, 0);
var hc = 0.133 * rad,
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
for (var i = 1; i <= 24; i += 2) {
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
a = (h0 + h2) / 2 - h1;
b = (h2 - h0) / 2;
xe = -b / (2 * a);
ye = (a * xe + b) * xe + h1;
d = b * b - 4 * a * h1;
roots = 0;
if (d >= 0) {
dx = Math.sqrt(d) / (Math.abs(a) * 2);
x1 = xe - dx;
x2 = xe + dx;
if (Math.abs(x1) <= 1) roots++;
if (Math.abs(x2) <= 1) roots++;
if (x1 < -1) x1 = x2;
}
if (roots === 1) {
if (h0 < 0) rise = i + x1;
else set = i + x1;
} else if (roots === 2) {
rise = i + (ye < 0 ? x2 : x1);
set = i + (ye < 0 ? x1 : x2);
}
if (rise && set) break;
h0 = h2;
}
var result = {};
if (rise) result.rise = hoursLater(t, rise);
if (set) result.set = hoursLater(t, set);
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
return result;
};
// observerAngle helper was missing in my copy-paste?
// Ah, it was in the chunks. Let me check the chunks again.
// Function `observerAngle` was in chunk 0.
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
// I included it.
// Note: I replaced var with const/let or kept var?
// The original used const/let but I used var in my head above?
// I will just use the original code but adapted.
// Actually, to be safe, I'll use the code I wrote in this block which is adapted to use 'var' (ES5 safe) or just ES6 is fine.
// The user Environment is modern (Chrome/ES6 likely).
// But for safety:
// I'll stick to 'var' definitions I wrote above which seem complete?
// Wait, I might have missed `observerAngle` definition in my manual reconstruction above?
// No, `observerAngle` is called in `getTimes`.
// I MUST define it.
// I added it in the loop above? No.
// Let me double check the `write_to_file` content I prepared.
// I see:
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
// Yes, it is there.
// export
window.SunCalc = SunCalc;
}());
@@ -1,10 +1,10 @@
# Result: /Users/mathias/sources/git/filament/out/cmake-release/tools/matc/matc
MATC="../../../../../out/cmake-release/tools/matc/matc"
MATC="../../../../out/cmake-release/tools/matc/matc"
# Navigate to script directory to ensure relative paths work
cd "$(dirname "$0")"
set -e
$MATC -a opengl -p mobile -o ../assets/simulated_skybox.filamat ../simulated_skybox.mat
$MATC -a opengl -p mobile -o assets/simulated_skybox.filamat simulated_skybox.mat
echo "Material recompiled to assets/simulated_skybox.filamat"
File diff suppressed because one or more lines are too long
Binary file not shown.
+2 -7
View File
@@ -24,7 +24,6 @@
<body>
<canvas></canvas>
<!-- Filament -->
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
@@ -32,13 +31,9 @@
<!-- UI -->
<script src="lil-gui.js"></script>
<!-- Utils -->
<script src="assets/suncalc_global.js"></script>
<!-- App -->
<script src="SimulatedSkybox.js?v=9999"></script>
<script src="main.js?v=9999"></script>
<script src="SimulatedSkybox.js?v=26"></script>
<script src="main.js?v=26"></script>
</body>
</html>
+97 -715
View File
@@ -1,123 +1,10 @@
// main.js
Filament.init(['assets/simulated_skybox.filamat?v=' + Date.now()], () => {
Filament.init(['assets/simulated_skybox.filamat'], () => {
window.app = new App(document.getElementsByTagName('canvas')[0]);
});
// Helper: Julian Date
function getJD(date) {
return date.getTime() / 86400000.0 + 2440587.5;
}
// Helper: GMST from Date
function getGMST(date) {
const JD = getJD(date);
const D = JD - 2451545.0;
// GMST = 18.697... + 24.0657... * D
let gmst = 18.697374558 + 24.06570982441908 * D;
gmst = gmst % 24.0;
if (gmst < 0) gmst += 24.0;
return gmst;
}
// Helper: Matrix Rotation
function rotateX(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m1 = m[1], m2 = m[2];
const m4 = m[4], m5 = m[5];
const m7 = m[7], m8 = m[8];
m[1] = m1 * c - m2 * s;
m[2] = m1 * s + m2 * c;
m[4] = m4 * c - m5 * s;
m[5] = m4 * s + m5 * c;
m[7] = m7 * c - m8 * s;
m[8] = m7 * s + m8 * c;
}
function rotateY(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m0 = m[0], m2 = m[2];
const m3 = m[3], m5 = m[5];
const m6 = m[6], m8 = m[8];
m[0] = m0 * c + m2 * s;
m[2] = -m0 * s + m2 * c;
m[3] = m3 * c + m5 * s;
m[5] = -m3 * s + m5 * c;
m[6] = m6 * c + m8 * s;
m[8] = -m6 * s + m8 * c;
}
function rotateZ(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m0 = m[0], m1 = m[1];
const m3 = m[3], m4 = m[4];
const m6 = m[6], m7 = m[7];
m[0] = m0 * c - m1 * s;
m[1] = m0 * s + m1 * c;
m[3] = m3 * c - m4 * s;
m[4] = m3 * s + m4 * c;
m[6] = m6 * c - m7 * s;
m[7] = m6 * s + m7 * c;
}
// Galactic to Equatorial (J2000)
// This matrix converts Galactic vectors to Equatorial vectors.
// Or effectively, if we want to render Galactic texture from Equatorial View vector V_eq:
// V_gal = Inv(Rot_Gal_to_Eq) * V_eq = Rot_Eq_to_Gal * V_eq.
// The shader does: V_gal = Rotation * V_world.
// So Rotation = Rot_Eq_to_Gal * Rot_World_to_Eq.
//
// Galactic North Pole (J2000): RA = 192.85948, Dec = 27.12825
// Ascending Node: RA = 282.85
//
// Pre-computed Rotation Matrix (Equatorial -> Galactic)
// Based on standard transformation matrices.
//
// R_eq_gal =
// [ -0.054876 -0.873437 -0.483835 ]
// [ 0.494109 -0.444830 0.746982 ]
// [ -0.867666 -0.198076 0.455984 ]
//
// Let's use this static definition.
const MAT_EQ_TO_GAL = [
-0.054876, 0.494109, -0.867666,
-0.873437, -0.444830, -0.198076,
-0.483835, 0.746982, 0.455984
];
// Matrix multiplication 3x3
function multiplyMat3(a, b) {
const out = new Float32Array(9);
// Row-major or Column-major? Filament is Column-major usually.
// GLSL is Column-major.
// Mat3 in array: [col0.x, col0.y, col0.z, col1.x, ...]
// So a[0] is (0,0), a[1] is (1,0), a[3] is (0,1).
//
// out = a * b
const a00 = a[0], a10 = a[1], a20 = a[2];
const a01 = a[3], a11 = a[4], a21 = a[5];
const a02 = a[6], a12 = a[7], a22 = a[8];
const b00 = b[0], b10 = b[1], b20 = b[2];
const b01 = b[3], b11 = b[4], b21 = b[5];
const b02 = b[6], b12 = b[7], b22 = b[8];
out[0] = a00 * b00 + a01 * b10 + a02 * b20;
out[1] = a10 * b00 + a11 * b10 + a12 * b20;
out[2] = a20 * b00 + a21 * b10 + a22 * b20;
out[3] = a00 * b01 + a01 * b11 + a02 * b21;
out[4] = a10 * b01 + a11 * b11 + a12 * b21;
out[5] = a20 * b01 + a21 * b11 + a22 * b21;
out[6] = a00 * b02 + a01 * b12 + a02 * b22;
out[7] = a10 * b02 + a11 * b12 + a12 * b22;
out[8] = a20 * b02 + a21 * b12 + a22 * b22;
return out;
}
class App {
constructor(canvas) {
this.canvas = canvas;
@@ -128,10 +15,13 @@ class App {
this.skybox.entity = this.skybox.entity; // Ensuring access if needed
this.scene.addEntity(this.skybox.entity);
// Load the material explicitly. SimulatedSkybox.loadMaterial fetches it.
const matUrl = 'assets/simulated_skybox.filamat?v=' + Date.now();
this.skybox.loadMaterial(matUrl).then(() => {
// Load the material explicitly since we passed it to init but SimulatedSkybox needs to bind it
// Actually SimulatedSkybox.loadMaterial fetches it.
// Since we already loaded it in Filament.init, we can arguably just use it if we had a way to access the asset.
// But Filament.init assets are for internal or easy access via assets object if configured?
// Let's just let SimulatedSkybox fetch it again or use a blob if we wanted.
// Simpler: Just let SimulatedSkybox fetch it.
this.skybox.loadMaterial('assets/simulated_skybox.filamat').then(() => {
this.initGUI();
});
@@ -181,15 +71,6 @@ class App {
this.initControls(); // Initialize controls immediately
this.mwParams = {
enabled: true,
intensity: 1.0,
saturation: 1.0,
blackPoint: 0.07,
siderealTime: 0.0, // Hours [0-24]
latitude: 34.0, // Default Lat
};
this.resize();
window.addEventListener('resize', this.resize.bind(this));
@@ -218,12 +99,6 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
// Moon Exposure
if (this.mParams) {
const preExposedMoon = this.mParams.intensity * exposure;
this.skybox.setMoonIntensity(preExposedMoon);
}
}
updateCameraProjection() {
@@ -231,7 +106,6 @@ class App {
const height = this.canvas.height;
const aspect = width / height;
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setFocalLength(this.params.focalLength);
}
initGUI() {
@@ -240,7 +114,10 @@ class App {
const sky = this.skybox;
// Initialize local params from skybox defaults
// Initialize local params from skybox defaults
// REMOVED: Do not overwrite this.params from sky.sunDirection (Zenith)
// const currentDir = sky.sunDirection;
// this.params.sunTheta = ...
const updateSun = () => {
const theta = this.params.sunTheta;
@@ -251,165 +128,29 @@ class App {
sky.setSunPosition([x, y, z]);
};
// Sun UI Proxy
this.sunUI = {
azimuth: (this.params.sunPhi * 180.0 / Math.PI) % 360.0,
height: Math.cos(this.params.sunTheta)
};
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
const sunFolder = gui.addFolder('Sun');
sunFolder.add(this.sunUI, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(v => {
this.params.sunPhi = v * (Math.PI / 180.0);
updateSun();
});
sunFolder.add(this.sunUI, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(v => {
// Helper for "Sun Height" cosine slider like C++
const sunHeightParam = { height: Math.cos(this.params.sunTheta) };
sunFolder.add(sunHeightParam, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
this.params.sunTheta = Math.acos(v);
updateSun();
});
sunFolder.add(this.params, 'sunPhi', 0.0, Math.PI * 2).name('Azimuth').onChange(updateSun);
// Updated: Controls params.sunIntensity and triggers updateSunIntensity
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).onChange(v => this.updateSunIntensity());
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).name('Intensity').onChange(v => this.updateSunIntensity());
const moonFolder = gui.addFolder('Moon');
this.mParams = {
enabled: true,
azimuth: 180.0,
height: Math.cos(45.0 * Math.PI / 180.0), // Default 45 degrees elevation -> cos(45) ~ 0.707
radius: 1.2,
intensity: 6.0
};
const updateMoon = () => {
const az = this.mParams.azimuth * (Math.PI / 180.0);
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
};
// Initial Moon Sync
updateMoon();
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
sky.setMoonIntensity(this.mParams.intensity);
moonFolder.add(this.mParams, 'enabled').name('Enabled').onChange(v => sky.setMoonEnabled(v));
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(updateMoon);
moonFolder.add(this.mParams, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(updateMoon);
moonFolder.add(this.mParams, 'intensity', 0.0, 1000.0).name('Intensity').onChange(v => this.updateSunIntensity());
moonFolder.add(this.mParams, 'radius', 0.1, 5.0).name('Radius').onChange(v => sky.setMoonRadius(v));
moonFolder.close();
const mwFolder = gui.addFolder('Milky Way');
const updateMW = () => {
sky.setMilkyWayEnabled(this.mwParams.enabled);
sky.setMilkyWayControl(this.mwParams.intensity, this.mwParams.saturation, this.mwParams.blackPoint);
// Calculate Rotation
// V_gal = Rot_Eq_to_Gal * Rot_World_to_Eq * V_world
// World: Y=Up, X=East, Z=South (Filament Camera Convention is different!)
// In Filament Camera: -Z is Forward.
// Skybox V direction is World Space direction.
// Let's assume standard Horizontal Coordinates:
// Y = Zenith.
// Z = North? Or South?
// Usually Z is South in RH Y-up.
// LST (Local Sidereal Time) converts Hour Angle to RA.
// LST in Radians.
const LST = this.mwParams.siderealTime * (Math.PI / 12.0); // Hours to Rad
const Lat = this.mwParams.latitude * (Math.PI / 180.0);
// Rotation World (Horizontal) -> Equatorial
// 1. Rotate around X by -(90 - Lat) to align Equatorial Plane.
// 2. Rotate around Y (Polar Axis) by -LST.
// Mat3 Identity
const rot = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// Rotate Z by LST (Earth Rotation).
// Actually, transformation from Horizontal (Az, Alt) to Equatorial (HA, Dec):
// sin(Dec) = sin(Alt)sin(Lat) - cos(Alt)cos(Lat)cos(Az)
// ...
// Let's construct matrix directly.
// WorldToEq:
// Rotate X by (Lat - 90 deg) -> brings Pole to Zenith.
// Rotate Y by -LST (or Z?)
// Filament Space:
// +Y = Up
// Let's match typical skybox conventions.
// Rot_World_to_Eq = Rot_Z_LST * Rot_X_Lat
// But we need to use Filament matrix ops which are column major.
// Let's use simple rotations:
// 1. Tilt Pole: Rotate X by (Lat - 90).
// 2. Spin Earth: Rotate Y by LST.
// Let's iterate until it looks right visually or trust the math.
// Rot_World_To_Equatorial:
// R_z(-LST) * R_x(Lat - 90)?
// Let's build it from scratch in JS using helper.
// Start Identity.
// Rotate X (Latitude Tilt).
// Rotate Y (Sidereal Spin).
// Note: rotate functions modify in place.
const mWorldToEq = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// 1. Tilt for Latitude (Align Celestial Pole)
// At Lat 90 (North Pole), Zenith is Pole. No tilt needed if Y is Pole?
// No, Y is Zenith. Pole is Y.
// At Lat 0 (Equator), Pole is at Horizon (Z?).
// So we rotate X by (Lat - 90).
rotateX(mWorldToEq, Lat - Math.PI / 2);
// 2. Spin for Time (LST)
// Rotate around new Pole (Y) by LST.
rotateY(mWorldToEq, LST);
// Combine with Gal Transform
// Rot = MAT_EQ_TO_GAL * mWorldToEq
const finalRot = multiplyMat3(MAT_EQ_TO_GAL, mWorldToEq);
sky.setMilkyWayRotation(finalRot);
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
mwFolder.add(this.mwParams, 'latitude', -90.0, 90.0).name('Latitude').onChange(updateMW);
mwFolder.close();
// Initial MW Update
updateMW();
this.updateMW = updateMW; // Export for sync
const sunDisk = sunFolder.addFolder('Disk');
// We need local proxy for sunRadius due to conversion
this.diskParams = {
radius: 1.2
const diskParams = {
radius: 1.2,
enabled: true // Enable sun disk
};
sky.setSunDiskEnabled(true);
sky.setSunRadius(1.2);
sunFolder.add(this.diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
sunFolder.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
sunFolder.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
sunDisk.add(diskParams, 'enabled').onChange(v => sky.setSunDiskEnabled(v));
sunDisk.add(diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
sunDisk.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
sunDisk.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
const atmFolder = gui.addFolder('Atmosphere');
atmFolder.add(sky, 'turbidity', 1.0, 10.0).onChange(v => sky.setTurbidity(v));
@@ -420,69 +161,12 @@ class App {
atmFolder.add(sky, 'ozone', 0.0, 1.0).onChange(v => sky.setOzone(v));
atmFolder.add(sky, 'mieG', 0.0, 0.999).onChange(v => sky.setMieG(v));
const cloudFolder = gui.addFolder('Clouds');
this.cParams = {
volumetrics: sky.cloudControl2[1] > 0.5,
coverage: 0.4,
density: 0.02,
height: sky.cloudControl[2],
speed: 50.0,
evolution: 0.02
};
// Apply Cloud Defaults
sky.setCloudControl(0.4, 0.02, this.cParams.height, 50.0);
sky.setCloudShapeEvolution(0.02);
cloudFolder.add(this.cParams, 'volumetrics').onChange(v => sky.setCloudVolumetricLighting(v));
cloudFolder.add(this.cParams, 'coverage', 0.0, 1.0).onChange(v => sky.setCloudControl(v, this.cParams.density, this.cParams.height, this.cParams.speed));
cloudFolder.add(this.cParams, 'density', 0.0, 1.0).onChange(v => sky.setCloudControl(this.cParams.coverage, v, this.cParams.height, this.cParams.speed));
cloudFolder.add(this.cParams, 'height', 2000.0, 20000.0).onChange(v => sky.setCloudControl(this.cParams.coverage, this.cParams.density, v, this.cParams.speed));
// Reverse speed calc: w = speed * (0.05 / 72.0)
cloudFolder.add(this.cParams, 'speed', 0.0, 200.0).onChange(v => sky.setCloudControl(this.cParams.coverage, this.cParams.density, this.cParams.height, v));
cloudFolder.add(this.cParams, 'evolution', 0.0, 2.0).onChange(v => sky.setCloudShapeEvolution(v));
const waterFolder = gui.addFolder('Water');
this.wParams = {
derivativeTrick: true,
strength: 50.0,
speed: 1.0,
octaves: 4.0
};
// Initialize defaults
sky.setWaterControl(50.0, 1.0, 1.0, 4.0); // 1.0 = Derivative Trick On, 4 octaves
const updateWater = () => {
sky.setWaterControl(this.wParams.strength, this.wParams.speed, this.wParams.derivativeTrick ? 1.0 : 0.0, this.wParams.octaves);
};
waterFolder.add(this.wParams, 'derivativeTrick').name('Derivative Trick').onChange(updateWater);
waterFolder.add(this.wParams, 'strength', 10.0, 100.0).onChange(updateWater);
waterFolder.add(this.wParams, 'speed', 0.0, 5.0).onChange(updateWater);
waterFolder.add(this.wParams, 'octaves', 1, 8, 1).name('Octaves').onChange(updateWater);
waterFolder.close();
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.close();
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
sky.setHorizonGlow(0.0);
sky.msFactors[2] = 0.0;
// Set Contrast default to 1.0
sky.setContrast(1.0);
// Set Horizon Glow default to 1.0
sky.setHorizonGlow(1.0);
sky.msFactors[2] = 1.0;
// Set Contrast default to 0.85
sky.setContrast(0.85);
artFolder.add(sky.msFactors, 0, 0.0, 2.0).name('MS Rayleigh').onChange(v => sky.setMultiScattering(v, sky.msFactors[1]));
artFolder.add(sky.msFactors, 1, 0.0, 2.0).name('MS Mie').onChange(v => sky.setMultiScattering(sky.msFactors[0], v));
@@ -499,6 +183,63 @@ class App {
shmFolder.add(sky.shimmerControl, 1, 1.0, 100.0).name('Frequency').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], v, sky.shimmerControl[2]));
shmFolder.add(sky.shimmerControl, 2, 0.01, 0.5).name('Mask Height').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], sky.shimmerControl[1], v));
const cloudFolder = gui.addFolder('Clouds');
const cParams = {
volumetrics: sky.cloudControl2[1] > 0.5,
coverage: 0.4,
density: 0.02,
height: sky.cloudControl[2],
speed: 50.0,
evolution: 0.02
};
// Apply Cloud Defaults
sky.setCloudControl(0.4, 0.02, cParams.height, 50.0);
sky.setCloudShapeEvolution(0.02);
cloudFolder.add(cParams, 'volumetrics').onChange(v => sky.setCloudVolumetricLighting(v));
cloudFolder.add(cParams, 'coverage', 0.0, 1.0).onChange(v => sky.setCloudControl(v, cParams.density, cParams.height, cParams.speed));
cloudFolder.add(cParams, 'density', 0.0, 1.0).onChange(v => sky.setCloudControl(cParams.coverage, v, cParams.height, cParams.speed));
cloudFolder.add(cParams, 'height', 2000.0, 20000.0).onChange(v => sky.setCloudControl(cParams.coverage, cParams.density, v, cParams.speed));
// Reverse speed calc: w = speed * (0.05 / 72.0)
cloudFolder.add(cParams, 'speed', 0.0, 200.0).onChange(v => sky.setCloudControl(cParams.coverage, cParams.density, cParams.height, v));
cloudFolder.add(cParams, 'evolution', 0.0, 2.0).onChange(v => sky.setCloudShapeEvolution(v));
const waterFolder = gui.addFolder('Water');
const wParams = {
derivativeTrick: true,
strength: 50.0,
speed: 1.0,
octaves: 4.0
};
// Initialize defaults
sky.setWaterControl(50.0, 1.0, 1.0, 4.0); // 1.0 = Derivative Trick On, 4 octaves
const updateWater = () => {
sky.setWaterControl(wParams.strength, wParams.speed, wParams.derivativeTrick ? 1.0 : 0.0, wParams.octaves);
};
waterFolder.add(wParams, 'derivativeTrick').name('Derivative Trick').onChange(updateWater);
waterFolder.add(wParams, 'strength', 10.0, 100.0).onChange(updateWater);
waterFolder.add(wParams, 'speed', 0.0, 5.0).onChange(updateWater);
waterFolder.add(wParams, 'octaves', 1, 8, 1).name('Octaves').onChange(updateWater);
waterFolder.close();
const starFolder = gui.addFolder('Stars');
const sParams = {
enabled: true,
density: 1.0
};
// Initialize defaults (Density 1.0, Enabled True)
sky.setStarControl(1.0, true);
const updateStars = () => {
sky.setStarControl(sParams.density, sParams.enabled);
};
starFolder.add(sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(sParams, 'density', 0.0, 1.0).name('Density').onChange(updateStars);
starFolder.close();
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
@@ -506,362 +247,35 @@ class App {
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
this.bParams = {
enabled: true,
const bParams = {
enabled: false,
lensFlare: false
};
const updateBloom = () => {
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare
enabled: bParams.enabled,
lensFlare: bParams.lensFlare
});
};
bloomFolder.add(this.bParams, 'enabled').onChange(updateBloom);
bloomFolder.add(this.bParams, 'lensFlare').onChange(updateBloom);
bloomFolder.add(bParams, 'enabled').onChange(updateBloom);
bloomFolder.add(bParams, 'lensFlare').onChange(updateBloom);
bloomFolder.close();
// Collapse folders by default
sunDisk.close();
atmFolder.close();
artFolder.close();
// shmFolder is inside artFolder, so it's hidden, but we can close it too if we want
shmFolder.close();
cloudFolder.close();
// camFolder left open by default for convenience.
// camFolder left open? User didn't specify, but "Artistic, shimmer and clouds" + "Disk, Atmosphere" were requested.
// So Camera might stay open or close. Let's keep Camera open for now as it wasn't listed.
// Initial sync
updateSun();
this.updateCameraExposure(); // This will trigger updateSunIntensity too
updateBloom();
// Check URL for config
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('config')) {
try {
const state = JSON.parse(atob(urlParams.get('config')));
this.applyURLState(state);
// Update GUI
gui.controllers.forEach(c => c.updateDisplay());
// Recursive update for folders
gui.folders.forEach(f => {
f.controllers.forEach(c => c.updateDisplay());
// And sub-folders if any (Shimmer/Bloom)
f.folders.forEach(sf => sf.controllers.forEach(sc => sc.updateDisplay()));
});
} catch (e) {
console.error("Failed to load config:", e);
}
}
const syncFolder = gui.addFolder('Real-Time Sync');
this.syncParams = {
enabled: false,
lat: 0.0,
lng: 0.0,
status: 'Disabled'
};
const updateSync = () => {
if (this.syncParams.enabled) {
if (navigator.geolocation) {
this.syncParams.status = "Locating...";
navigator.geolocation.getCurrentPosition(
(pos) => {
this.syncParams.lat = pos.coords.latitude;
this.syncParams.lng = pos.coords.longitude;
this.syncParams.status = "Active";
syncFolder.controllers.forEach(c => c.updateDisplay());
},
(err) => {
console.error(err);
this.syncParams.status = "Error (See Console)";
this.syncParams.enabled = false;
syncFolder.controllers.forEach(c => c.updateDisplay());
}
);
} else {
this.syncParams.status = "Not Supported";
}
} else {
this.syncParams.status = "Disabled";
}
syncFolder.controllers.forEach(c => c.updateDisplay());
};
syncFolder.add(this.syncParams, 'enabled').name('Enable Sync').onChange(updateSync);
syncFolder.add(this.syncParams, 'status').name('Status').disable().listen();
this.syncFolder = syncFolder;
const shareParams = {
copyUrl: () => {
const state = this.getURLState();
const str = btoa(JSON.stringify(state));
const url = `${window.location.origin}${window.location.pathname}?config=${str}`;
navigator.clipboard.writeText(url).then(() => {
alert("Configuration URL copied to clipboard!");
}).catch(err => {
console.error('Could not copy text: ', err);
prompt("Copy this URL:", url);
});
}
};
gui.add(shareParams, 'copyUrl').name('Share Configuration');
}
updateRealTimeSync() {
if (!this.syncParams || !this.syncParams.enabled || !window.SunCalc) return;
const now = new Date();
const lat = this.syncParams.lat;
const lng = this.syncParams.lng;
// Sun
const sunPos = window.SunCalc.getPosition(now, lat, lng);
// Azimuth: South=0, West=PI/2.
// Skybox Phi: +X=0, +Z=PI/2.
// If +Z is South:
// SunAz 0 (South) -> Skybox PI/2 (+Z).
// SunAz PI/2 (West) -> Skybox PI (-X).
// So Phi = Az + PI/2.
const sunPhi = sunPos.azimuth + Math.PI / 2;
// Altitude: 0=Horizon, PI/2=Zenith.
// Skybox Theta: 0=Zenith, PI/2=Horizon.
const sunTheta = Math.PI / 2 - sunPos.altitude;
this.params.sunPhi = sunPhi;
this.params.sunTheta = sunTheta;
// Moon
const moonPos = window.SunCalc.getMoonPosition(now, lat, lng);
const moonPhi = moonPos.azimuth + Math.PI / 2;
const moonTheta = Math.PI / 2 - moonPos.altitude;
this.mParams.azimuth = (moonPhi * 180.0 / Math.PI) % 360.0;
this.mParams.height = Math.cos(moonTheta);
// Milky Way Sync
const gmst = getGMST(now);
const lst = (gmst + lng / 15.0 + 24.0) % 24.0;
this.mwParams.siderealTime = lst;
this.mwParams.latitude = lat;
if (this.updateMW) this.updateMW();
// Update Skybox
const sky = this.skybox;
// Update Sun Vector
const sx = Math.sin(sunTheta) * Math.cos(sunPhi);
const sy = Math.cos(sunTheta);
const sz = Math.sin(sunTheta) * Math.sin(sunPhi);
sky.setSunPosition([sx, sy, sz]);
// Update Moon Vector
const mx = Math.sin(moonTheta) * Math.cos(moonPhi);
const my = Math.cos(moonTheta);
const mz = Math.sin(moonTheta) * Math.sin(moonPhi);
sky.setMoonPosition([mx, my, mz]);
// Update UI Proxies
if (this.sunUI) {
this.sunUI.azimuth = (sunPhi * 180.0 / Math.PI) % 360.0;
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
this.sunUI.height = Math.cos(sunTheta);
}
}
getURLState() {
// Update Camera LookAt
// Serialize current state (Minified)
// Mapping:
// p: params (Camera) -> a:aperture, ss:shutterSpeed, i:iso, st:sunTheta, sp:sunPhi, fl:focalLength, si:sunIntensity
// c: cParams (Clouds) -> v:volumetrics, co:coverage, d:density, h:height, s:speed, e:evolution
// w: wParams (Water) -> dt:derivativeTrick, st:strength, s:speed, o:octaves
// s: sParams (Stars) -> e:enabled, d:density
// b: bParams (Bloom) -> e:enabled, lf:lensFlare
// k: sky (Skybox) -> t:turbidity, r:rayleigh, mc:mieCoefficient, mg:mieG, o:ozone, ms:msFactors, co:contrast, nc:nightColor, sh:shimmerControl, hl:sunHalo
const p = this.params;
const c = this.cParams;
const w = this.wParams;
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
k: {
t: sk.turbidity,
r: sk.rayleigh,
mc: sk.mieCoefficient,
mg: sk.mieG,
o: sk.ozone,
ms: [...sk.msFactors],
co: sk.contrast,
nc: [...sk.nightColor],
sh: [...sk.shimmerControl],
hl: [...sk.sunHalo]
}
};
}
applyURLState(state) {
const p = state.p;
const c = state.c;
const w = state.w;
const s = state.s;
const b = state.b;
const m = state.m;
const k = state.k;
const cm = state.cm;
if (p) {
if (p.a !== undefined) this.params.aperture = p.a;
if (p.ss !== undefined) this.params.shutterSpeed = p.ss;
if (p.i !== undefined) this.params.iso = p.i;
if (p.st !== undefined) this.params.sunTheta = p.st;
if (p.sp !== undefined) this.params.sunPhi = p.sp;
if (p.fl !== undefined) this.params.focalLength = p.fl;
if (p.si !== undefined) this.params.sunIntensity = p.si;
}
if (c) {
if (c.v !== undefined) this.cParams.volumetrics = c.v;
if (c.co !== undefined) this.cParams.coverage = c.co;
if (c.d !== undefined) this.cParams.density = c.d;
if (c.h !== undefined) this.cParams.height = c.h;
if (c.s !== undefined) this.cParams.speed = c.s;
if (c.e !== undefined) this.cParams.evolution = c.e;
}
if (w) {
if (w.dt !== undefined) this.wParams.derivativeTrick = w.dt;
if (w.st !== undefined) this.wParams.strength = w.st;
if (w.s !== undefined) this.wParams.speed = w.s;
if (w.o !== undefined) this.wParams.octaves = w.o;
}
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
}
if (b) {
if (b.e !== undefined) this.bParams.enabled = b.e;
if (b.lf !== undefined) this.bParams.lensFlare = b.lf;
}
const sky = this.skybox;
if (k) {
if (k.t !== undefined) sky.setTurbidity(k.t);
if (k.r !== undefined) sky.setRayleigh(k.r);
if (k.mc !== undefined) sky.setMieCoefficient(k.mc);
if (k.mg !== undefined) sky.setMieG(k.mg);
if (k.o !== undefined) sky.setOzone(k.o);
if (k.ms) { sky.setMultiScattering(k.ms[0], k.ms[1]); sky.setHorizonGlow(k.ms[2]); }
if (k.co !== undefined) sky.setContrast(k.co);
if (k.nc) sky.setNightColor(k.nc);
if (k.sh) sky.setShimmerControl(k.sh[0], k.sh[1], k.sh[2]);
// Sun Halo
const savedHalo = k.hl;
if (savedHalo && savedHalo.length === 4) {
sky.sunHalo[0] = savedHalo[0];
sky.sunHalo[1] = savedHalo[1];
sky.sunHalo[2] = savedHalo[2];
sky.sunHalo[3] = savedHalo[3];
// Update derived UI params for Sun Disk
const rad = Math.acos(Math.max(-1.0, Math.min(1.0, savedHalo[0])));
this.diskParams.radius = rad * (180.0 / Math.PI);
this.diskParams.enabled = savedHalo[3] > 0.5;
}
sky.updateCoefficients();
}
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
// Update derived Sun UI
if (this.sunUI) {
this.sunUI.height = Math.cos(this.params.sunTheta);
this.sunUI.azimuth = (this.params.sunPhi * 180.0 / Math.PI) % 360.0;
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
}
// Apply Local Params via Setters
sky.setCloudControl(this.cParams.coverage, this.cParams.density, this.cParams.height, this.cParams.speed);
sky.setCloudVolumetricLighting(this.cParams.volumetrics);
sky.setCloudShapeEvolution(this.cParams.evolution);
sky.setWaterControl(this.wParams.strength, this.wParams.speed, this.wParams.derivativeTrick ? 1.0 : 0.0, this.wParams.octaves);
sky.setStarControl(this.sParams.density, this.sParams.enabled);
if (m) {
if (m.e !== undefined) this.mParams.enabled = m.e;
if (m.az !== undefined) this.mParams.azimuth = m.az;
// Compat: if 'el' exists (old link) convert to 'h'
if (m.h !== undefined) {
this.mParams.height = m.h;
} else if (m.el !== undefined) {
// Convert elevation degrees to height cos
this.mParams.height = Math.cos(m.el * Math.PI / 180.0);
}
if (m.r !== undefined) this.mParams.radius = m.r;
if (m.i !== undefined) this.mParams.intensity = m.i;
// Sync Moon
const az = this.mParams.azimuth * (Math.PI / 180.0);
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
sky.setMoonIntensity(this.mParams.intensity);
}
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare
});
// Update Sun Position from Params
const theta = this.params.sunTheta;
const phi = this.params.sunPhi;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setSunPosition([x, y, z]);
// Update Camera Projection (Focal Length) and Exposure (Aperture/Shutter/ISO)
this.updateCameraProjection();
this.updateCameraExposure();
}
initControls() {
@@ -891,41 +305,10 @@ class App {
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
});
// Touch support
this.canvas.addEventListener('touchstart', e => {
if (e.touches.length > 0) {
e.preventDefault(); // Prevent scroll/long-press
this.camState.dragging = true;
this.camState.lastX = e.touches[0].clientX;
this.camState.lastY = e.touches[0].clientY;
}
}, { passive: false });
window.addEventListener('touchend', () => {
this.camState.dragging = false;
});
window.addEventListener('touchmove', e => {
if (!this.camState.dragging || e.touches.length === 0) return;
e.preventDefault(); // Prevent scrolling
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
const dx = x - this.camState.lastX;
const dy = y - this.camState.lastY;
this.camState.lastX = x;
this.camState.lastY = y;
const sensitivity = 0.005;
this.camState.theta -= dx * sensitivity;
this.camState.phi += dy * sensitivity;
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
}, { passive: false });
}
render() {
this.updateRealTimeSync();
// Update Camera LookAt
const r = 1.0;
const theta = this.camState.theta;
@@ -958,6 +341,5 @@ class App {
const aspect = width / height;
// near=0.1, far=5000.0
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setResolution(height);
}
}
-61
View File
@@ -1,61 +0,0 @@
import os
import urllib.request
import ssl
from PIL import Image
import argparse
# URL of the Milky Way texture (Gaia EDR3) from ESA
# Low Res PNG (2.71 MB) is sufficient for our 1024x512 target
URL = "https://www.esa.int/var/esa/storage/images/esa_multimedia/images/2020/12/the_colour_of_the_sky_from_gaia_s_early_data_release_3/22358049-1-eng-GB/The_colour_of_the_sky_from_Gaia_s_Early_Data_Release_3.png"
OUTPUT_DIR = "assets"
OUTPUT_FILENAME = "milkyway.png"
TARGET_WIDTH = 1024
TARGET_HEIGHT = 512
def main():
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)
temp_path = os.path.join(OUTPUT_DIR, "temp_milkyway.png")
print(f"Downloading Milky Way texture from {URL}...")
# Bypass SSL verification globally
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
try:
req = urllib.request.Request(URL, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response, open(temp_path, 'wb') as out_file:
out_file.write(response.read())
except Exception as e:
print(f"Failed to download: {e}")
return
if not os.path.exists(temp_path):
print("Error: Download failed.")
return
print("Processing image...")
with Image.open(temp_path) as img:
# Convert to RGB (remove alpha if present, though this is likely opaque)
img = img.convert("RGB")
# Resize to user requested dimensions
print(f"Resizing to {TARGET_WIDTH}x{TARGET_HEIGHT}...")
img = img.resize((TARGET_WIDTH, TARGET_HEIGHT), Image.Resampling.LANCZOS)
# Save
print(f"Saving to {output_path}...")
img.save(output_path, "PNG")
# Cleanup
if os.path.exists(temp_path):
os.remove(temp_path)
print("Done!")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,21 +0,0 @@
#!/bin/bash
set -e
# Default size
SIZE=${1:-256}
OUTPUT_COLOR="../assets/moon_disk.png"
OUTPUT_NORMAL="../assets/moon_normal.png"
# Navigate to script directory
cd "$(dirname "$0")"
# Check dependencies
if ! python3 -c "import numpy, PIL" 2>/dev/null; then
echo "Installing dependencies (numpy, Pillow)..."
pip3 install numpy Pillow
fi
echo "Generating Moon Asset (Size: ${SIZE}x${SIZE})..."
python3 process_moon.py --size $SIZE --supersample 4 --blur 1.0 --output-color $OUTPUT_COLOR --output-normal $OUTPUT_NORMAL
echo "Generated $OUTPUT_COLOR and $OUTPUT_NORMAL"
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

-249
View File
@@ -1,249 +0,0 @@
import argparse
import urllib.request
import os
import sys
import math
import ssl
from PIL import Image
# Ensure numpy is available
try:
import numpy as np
except ImportError:
print("Error: numpy is required. Please install it with: pip install numpy")
sys.exit(1)
# Default URLs
COLOR_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_2k.jpg"
DISP_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/ldem_4.tif"
SRC_COLOR_FILENAME = "lroc_color_2k.jpg"
SRC_DISP_FILENAME = "ldem_4.tif"
def download_file(url, filename):
if os.path.exists(filename):
print(f"File {filename} already exists. Skipping download.")
return
print(f"Downloading {url} to {filename}...")
# Create unverified context to avoid potential SSL cert issues
context = ssl._create_unverified_context()
try:
with urllib.request.urlopen(url, context=context) as response, open(filename, 'wb') as out_file:
data = response.read()
out_file.write(data)
print(f"Download of {filename} complete.")
except Exception as e:
print(f"Error downloading file {filename}: {e}")
# Don't exit hard if it's just one file, maybe?
# But for this script, we likely need it.
sys.exit(1)
def equirectangular_to_orthographic(src_img, size, mode=None):
"""
Reprojects an equirectangular image to an orthographic projection (sphere view).
src_img: PIL Image (Equirectangular)
size: Output size (width, height) - usually square
"""
print(f"Reprojecting to {size}x{size} Orthographic Disk...")
width, height = size, size
src_w, src_h = src_img.size
# Create coordinate grid centered at 0,0 (-1 to 1)
y, x = np.mgrid[size/2:-size/2:-1, -size/2:size/2] # Note: y goes high to low
# Normalize to -1..1
x = x / (size / 2)
y = y / (size / 2)
# Mask for points outside the circle
r2 = x*x + y*y
mask = r2 <= 1.0
# Calculate sphere coordinates (z > 0 for front face)
z = np.zeros_like(r2)
z[mask] = np.sqrt(1.0 - r2[mask])
# Vector P = (x, y, z) on unit sphere
# Lat = asin(y)
# Lon = atan2(x, z)
# Apply mask to avoid invalid calculations
lat = np.arcsin(y * mask)
lon = np.arctan2(x * mask, z * mask)
# Map to UV [0, 1]
u = (lon / (2 * math.pi)) + 0.5
v = (lat / math.pi) + 0.5
# Map to Source Pixels
u = np.clip(u, 0, 1)
v = np.clip(v, 0, 1)
src_x = (u * (src_w - 1)).astype(np.int32)
src_y = ((1.0 - v) * (src_h - 1)).astype(np.int32) # Flip V for image coords
# Sample pixels
src_array = np.array(src_img)
# Handle dimensions
if len(src_array.shape) == 2:
# Grayscale / Single channel
out_channels = 1
src_array = src_array[:, :, np.newaxis] # Expand for consistent indexing
else:
out_channels = src_array.shape[2]
out_array = np.zeros((height, width, out_channels), dtype=src_array.dtype)
# Advanced indexing
valid_y, valid_x = np.where(mask)
# Extract coordinates for valid pixels
sx = src_x[valid_y, valid_x]
sy = src_y[valid_y, valid_x]
out_array[valid_y, valid_x] = src_array[sy, sx]
# Squeeze if single channel
if out_channels == 1:
out_array = out_array.squeeze(axis=2)
return Image.fromarray(out_array, mode or src_img.mode)
def compute_normal_map(height_img, scale=1.0, blur_radius=0.0):
print("Computing Normal Map from Height Map...")
# Convert to float array
h = np.array(height_img).astype(np.float32)
# Apply Blur if requested
if blur_radius > 0:
try:
from scipy.ndimage import gaussian_filter
print(f"Applying Gaussian Blur (Radius: {blur_radius})...")
h = gaussian_filter(h, sigma=blur_radius)
except ImportError:
print("Warning: scipy not found. Skipping Gaussian Blur.")
# Normalize height to 0..1 for consistent gradient scale regardless of input depth
h_min, h_max = h.min(), h.max()
print(f"Height Map Range: {h_min} to {h_max}")
if h_max > h_min:
h_norm = (h - h_min) / (h_max - h_min)
else:
h_norm = h
# Gradients
dy, dx = np.gradient(h_norm)
# Pre-emphasis scale
bump_scale = scale
# Normal vector components
# Map is Top-Down Y.
nx = -dx * bump_scale
ny = -dy * bump_scale
nz = np.ones_like(nx)
# Mask out normals where r > 0.96 (avoid edge cliff artifacts)
rows, cols = h.shape
y, x = np.ogrid[:rows, :cols]
center_y, center_x = rows/2.0, cols/2.0
# max radius is size/2
radius_sq = (min(rows, cols) / 2.0)**2
dist_sq = (x - center_x)**2 + (y - center_y)**2
mask = dist_sq < (radius_sq * 0.96 * 0.96)
nx[~mask] = 0
ny[~mask] = 0
nz[~mask] = 1
# Normalize
len_n = np.sqrt(nx*nx + ny*ny + nz*nz)
# Avoid divide by zero
len_n[len_n == 0] = 1.0
nx /= len_n
ny /= len_n
nz /= len_n
# Pack to 0..255
# [-1, 1] -> [0, 255]
out_x = ((nx + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_y = ((ny + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_z = ((nz + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_rgb = np.dstack((out_x, out_y, out_z))
return Image.fromarray(out_rgb, 'RGB')
def main():
parser = argparse.ArgumentParser(description='Process Moon Texture')
parser.add_argument('--size', type=int, default=256, help='Output resolution (square)')
parser.add_argument('--supersample', type=int, default=2, help='Internal processing resolution multiplier')
parser.add_argument('--blur', type=float, default=1.0, help='Gaussian blur radius for height map')
parser.add_argument('--bump-scale', type=float, default=60.0, help='Normal map bump scale')
parser.add_argument('--output-color', type=str, default='assets/moon_disk.png', help='Output color filename')
parser.add_argument('--output-normal', type=str, default='assets/moon_normal.png', help='Output normal filename')
parser.add_argument('--skip-download', action='store_true', help='Skip downloading files')
args = parser.parse_args()
internal_size = args.size * args.supersample
# Ensure assets dir exists
os.makedirs(os.path.dirname(args.output_color) or '.', exist_ok=True)
# 1. Download
if not args.skip_download:
download_file(COLOR_URL, SRC_COLOR_FILENAME)
download_file(DISP_URL, SRC_DISP_FILENAME)
# 2. Process Color
print(f"Processing Color Map (Internal Size: {internal_size}x{internal_size})...")
try:
img_color = Image.open(SRC_COLOR_FILENAME).convert('RGB')
out_color = equirectangular_to_orthographic(img_color, internal_size)
if args.supersample > 1:
print(f"Downsampling Color to {args.size}x{args.size}...")
out_color = out_color.resize((args.size, args.size), Image.LANCZOS)
out_color.save(args.output_color)
print(f"Saved {args.output_color}")
except Exception as e:
print(f"Error processing color: {e}")
# 3. Process Normal
print(f"Processing Displacement Map (Internal Size: {internal_size}x{internal_size})...")
try:
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
# Check if file exists
if not os.path.exists(SRC_DISP_FILENAME):
print(f"Displacement map {SRC_DISP_FILENAME} not found!")
return
img_disp = Image.open(SRC_DISP_FILENAME)
out_disp = equirectangular_to_orthographic(img_disp, internal_size)
# Compute Normals
out_normal = compute_normal_map(out_disp, scale=args.bump_scale, blur_radius=args.blur)
if args.supersample > 1:
print(f"Downsampling Normal to {args.size}x{args.size}...")
out_normal = out_normal.resize((args.size, args.size), Image.LANCZOS)
out_normal.save(args.output_normal)
print(f"Saved {args.output_normal}")
except Exception as e:
print(f"Error processing normal: {e}")
import traceback
traceback.print_exc()
print("Done.")
if __name__ == "__main__":
main()
-3
View File
@@ -49,9 +49,6 @@
"libs/fgviewer/README.md": {
"dest": "dup/fgviewer.md"
},
"libs/viewer/README.md": {
"dest": "dup/viewer.md"
},
"ios/CocoaPods/README.md": {
"dest": "release/cocoapods.md"
},
-1
View File
@@ -41,7 +41,6 @@
- [gltfio](./dup/gltfio.md)
- [iblprefilter](./dup/iblprefilter.md)
- [matdbg](./dup/matdbg.md)
- [viewer](./dup/viewer.md)
- [uberz](./dup/uberz.md)
- [Tools](./notes/tools.md)
- [beamsplitter](./dup/beamsplitter.md)
File diff suppressed because one or more lines are too long
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
# Building Skybox Assets
This directory contains the source code for the Simulated Skybox sample. The assets (material and textures) are pre-built in the `assets/` directory.
If you need to modify the material or regenerate the moon textures, you can use the scripts provided in the `tools/` directory.
## Prerequisites
- **Filament**: You must have a built version of Filament. The scripts assume a standard CMake build output structure (e.g., `out/cmake-release`).
- **Python 3**: Required for texture generation.
- **Python Dependencies**: `numpy`, `Pillow` (automatically installed if missing, via pip).
## Rebuilding the Material
If you modify `simulated_skybox.mat`, you must recompile it into a `.filamat` file.
```bash
# Run from the sample root or tools directory
./tools/build_material.sh
```
This will update `assets/simulated_skybox.filamat`.
## Regenerating Moon Textures
If you want to change the moon's resolution, bump scale, or blur, you can regenerate the textures.
```bash
# Run from the sample root or tools directory
./tools/generate_moon_assets.sh [size]
```
Example:
```bash
./tools/generate_moon_assets.sh 512
```
This will download raw NASA data (if not present) and generate:
- `assets/moon_disk.png`
- `assets/moon_normal.png`
-76
View File
@@ -1,76 +0,0 @@
# Analytic Skybox Sample
This sample demonstrates a **fully procedural, single-pass skybox shader** capable of simulating a dynamic day-night cycle, atmospheric scattering, volumetric clouds, and water reflections.
It is designed for graphics engineers and technical artists who need a lightweight yet physically plausible environment background without relying on static HDRI textures.
## Features & Performance
The shader uses a "Uber-Shader" approach where all features are computed per-pixel. Features can be toggled or tuned via uniforms to balance quality vs performance.
| Feature | Cost | Control | Description |
| :--- | :---: | :--- | :--- |
| **Atmosphere** | 🟡 **Medium** | `turbidity`, `rayleigh`, `mie` | Analytic Rayleigh & Mie scattering. Physically based colors. |
| **Sun Disk** | 🟢 **Low** | `sunHalo` (Radius, Limb) | Analytic sphere intersection with limb darkening. Conservation of energy (Lux). |
| **Moon & Earthshine** | 🟢 **Low** | `sunHalo2`, `moonIntensity` | Resolved moon disk with geometric phases and dynamic Earthshine. |
| **Stars** | 🟢 **Low** | `starControl` (Density) | High-frequency procedural noise. Occluded by clouds/moon. |
| **Clouds** | 🔴 **High** | `cloudControl` (Coverage, Density) | 4-Octave 3D Fractal Brownian Motion (FBM). Dominates the cost when enabled. |
| **Heat Shimmer** | 🟡 **Medium** | `shimmerControl` | UV perturbation near the horizon to simulate mirages. |
| **Water Reflection** | 🟣 **Very High** | `waterControl` | **Renders the sky twice**. Includes procedural waves (FBM) and fresnel. |
> **Note**: Rendering water (`V.y < 0`) is significantly more expensive (~2.5x) than the sky because it requires re-evaluating the atmospheric scattering and cloud noise for the reflection vector.
## Shader Techniques
### 1. Analytic Atmospheric Scattering
Based on the **Hoffman & Preetham** model. It solves the single-scattering integral analytically for air molecules (Rayleigh) and aerosols (Mie).
- **Rayleigh**: Produces the deep blue sky and red sunset colors.
- **Mie**: Produces the white halo around the sun and general haziness.
- **Optimization**: Uses a simplified optical depth approximation ("Air Mass") to avoid expensive ray-marching.
### 2. Procedural Clouds (3D Noise)
Clouds are rendered as a spherical shell at a specific altitude.
- **Technique**: Ray-sphere intersection finds the entry point, then **3D FBM Noise** determines density.
- **Lighting**: Uses a "Silver Lining" approximation (strong forward scattering) and Beers-Lambert attenuation for dark underbellies.
- **Animation**: The noise coordinate logic helps simulate wind drift and shape evolution over time.
### 3. Infinite Water Ocean
When looking below the horizon, the shader switches to "Water Mode".
- **Geometry**: A flat plane at $y=0$.
- **Waves**: Generated using **Derivative-Based Noise** (or Finite Difference). This creates slope vectors that perturb the normal without needing actual geometry.
- **Reflection**: A ray is cast from the water surface back into the sky ($R = \text{reflect}(V, N)$). The sky function is called again with $R$ to get the reflected color.
### 4. Dynamic Tone Mapping
Applies a custom tone mapping curve that varies with Sun Elevation.
- **Noon**: Linear/Gamma (Standard).
- **Sunset**: Higher contrast curve to compress the dynamic range and enhance the rich sunset oranges/purples.
## Integration
To use this in your own Filament application:
1. **Compile the Material**:
Use `matc` to compile `simulated_skybox.mat` into a `.filamat` file.
```bash
matc -p mobile -a opengl -o assets/simulated_skybox.filamat simulated_skybox.mat
```
2. **Load in JavaScript/C++**:
Create a Skybox entity and assign the material.
```javascript
// JavaScript Example
const material = engine.createMaterial('assets/simulated_skybox.filamat');
const skybox = engine.createSkybox(material);
scene.setSkybox(skybox);
```
3. **Update Uniforms**:
The shader requires specific uniforms (Sun Direction, Time, etc.) to be updated every frame. See `SimulatedSkybox.js` for a reference implementation of the uniform buffer management.
## References
* **Hoffman & Preetham (2002)**: *"Real-time Light-Atmosphere Interactions"*
* **Henyey & Greenstein (1941)**: *"Diffuse radiation in the galaxy"* (Mie Phase Function)
* **Kasten & Young (1989)**: *"Revised optical air mass tables"*
* **Three.js / Sky.js**: Empirical adjustments for "Golden Hour" aesthetics.
+8 -403
View File
@@ -21,31 +21,13 @@ class SimulatedSkybox {
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.focalLength = 24.0;
this.height = 1000.0;
this.starControl = [1.0, 1.0]; // x=Density (0-1), y=Enabled (0-1)
this.planetRadius = 6360.0;
// Sun Halo
// x=cos(rad), y=limbDarkening, z=intensity, w=enabled
// Sun Halo
this.sunHalo = [Math.cos(0.5 * Math.PI / 180.0), 0.5, 1.0, 1.0];
// Moon Parameters (Mapped to Secondary Sun)
this.moonDirection = [-0.2, 0.8, -0.2]; // Default Moon Pos
this.moonIntensity = 1.0; // Scale Factor (1.0 = Physical Peak)
// x=cos(rad), y=sin(rad) [Precision Fix], z=intensity, w=enabled
this.moonHalo = [Math.cos(0.5 * Math.PI / 180.0), Math.sin(0.5 * Math.PI / 180.0), 1.0, 0.0]; // Disabled by default
this.moonTextureObj = null;
this.moonNormalObj = null;
this.milkyWayTextureObj = null;
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
}
@@ -62,184 +44,6 @@ class SimulatedSkybox {
rcm.setMaterialInstanceAt(instance, 0, this.materialInstance);
console.log("Material loaded and bound.");
// Load Moon Texture
try {
const texUrl = 'assets/moon_disk.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.moonTextureObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.SRGB8_A8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly (Uint8ClampedArray)
// Filament supports RGBA upload for RGB/SRGB internal formats (drops alpha).
// This matches Canvas layout (Top-Down, RGBA) and is 4-byte aligned.
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer), // Wrap buffer to ensure Uint8Array
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.moonTextureObj.setImage(this.engine, 0, pbd);
this.moonTextureObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR,
MagFilter.LINEAR,
WrapMode.CLAMP_TO_EDGE
);
this.materialInstance.setTextureParameter('moonTexture', this.moonTextureObj, sampler);
} catch (e) {
console.error("Failed to load moon texture:", e);
}
// Load Moon Normal
try {
const texUrl = 'assets/moon_normal.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.moonNormalObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.RGBA8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer),
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.moonNormalObj.setImage(this.engine, 0, pbd);
this.moonNormalObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR_MIPMAP_LINEAR,
MagFilter.LINEAR,
WrapMode.CLAMP_TO_EDGE
);
this.materialInstance.setTextureParameter('moonNormal', this.moonNormalObj, sampler);
} catch (e) {
console.error("Failed to load moon normal:", e);
}
// Load Milky Way Texture
try {
const texUrl = 'assets/milkyway.png';
const Texture = Filament.Texture;
const TextureSampler = Filament.TextureSampler;
const PixelDataFormat = Filament.PixelDataFormat;
const PixelDataType = Filament.PixelDataType;
const TextureUsage = Filament.Texture$Usage;
const TextureFormat = Filament.Texture$InternalFormat;
const MinFilter = Filament.MinFilter;
const MagFilter = Filament.MagFilter;
const WrapMode = Filament.WrapMode;
const texResponse = await fetch(texUrl);
const texBlob = await texResponse.blob();
const bitmap = await createImageBitmap(texBlob);
const width = bitmap.width;
const height = bitmap.height;
this.milkyWayTextureObj = Texture.Builder()
.width(width)
.height(height)
.levels(0xff)
.format(TextureFormat.SRGB8_A8)
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
.build(this.engine);
// Extract Data using a Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
try {
const imageData = ctx.getImageData(0, 0, width, height);
// Use RGBA data directly
const pbd = Filament.PixelBuffer(
new Uint8Array(imageData.data.buffer),
PixelDataFormat.RGBA,
PixelDataType.UBYTE
);
this.milkyWayTextureObj.setImage(this.engine, 0, pbd);
this.milkyWayTextureObj.generateMipmaps(this.engine);
const sampler = new TextureSampler(
MinFilter.LINEAR_MIPMAP_LINEAR,
MagFilter.LINEAR,
WrapMode.REPEAT // Equirectangular wraps horizontally
);
this.materialInstance.setTextureParameter('milkyWayTexture', this.milkyWayTextureObj, sampler);
} catch (err) {
console.warn("Milky Way texture data access failed (CORS?):", err);
}
} catch (e) {
console.error("Failed to load milky way texture:", e);
}
this.updateCoefficients();
}
@@ -280,16 +84,16 @@ class SimulatedSkybox {
this.ib.setBuffer(this.engine, TRIANGLE_INDICES);
// Build the Renderable without material; it will be set later by loadMaterial.
// We create a dummy material first or wait?
// In JS we usually can't block. We'll rely on loadMaterial being called.
// For now, we build the Renderable without material, then set it later.
RenderableManager.Builder(1)
.geometry(0, PrimitiveType.TRIANGLES, this.vb, this.ib)
.culling(false)
.castShadows(false)
.receiveShadows(false)
.priority(7) // Skybox priority
.priority(7) // Render behind translucent objects? 7 is skybox priority typically.
.build(this.engine, this.entity);
}
@@ -411,95 +215,11 @@ class SimulatedSkybox {
}
setStarControl(density, enabled) {
// Compensate for grid frequency reduction (350 -> 100)
// Fewer cells = fewer stars, so we increase density threshold.
// Factor ~ (350/100)^2 = 12.25
const compensatedDensity = density * 12.0;
this.starControl[0] = Math.max(0.0, Math.min(1.0, compensatedDensity));
this.starControl[0] = Math.max(0.0, Math.min(1.0, density));
this.starControl[1] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
setFocalLength(mm) {
this.focalLength = Math.max(1.0, mm);
this.updateStarFrequency();
}
setResolution(height) {
this.height = Math.max(1.0, height);
this.updateStarFrequency();
}
updateStarFrequency() {
// World-Anchored Stars
// z = Fixed Frequency (World Space Grid)
// w = Pixel Scale (Screen Space Radius)
// Fixed Frequency: Defines the "Universe" coordinate system.
// Reduced to 100.0 to allow larger stars without clipping (square artifacts).
this.starControl[2] = 100.0;
// Pixel Scale in Radians
// We use linear scaling (24/f) instead of atan(fov) to ensure star size remains
// constant in pixels across all focal lengths (Perspective Projection).
const fovFactor = 24.0 / this.focalLength;
const pixelScale = (1.0 / this.height) * fovFactor;
// Pass to shader (w component)
// Target radius: 1.3 pixels (Diameter 2.6 pixels)
// Visible but sharp.
this.starControl[3] = pixelScale * 1.3;
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
if (len > 0) {
this.moonDirection = [direction[0] / len, direction[1] / len, direction[2] / len];
}
this.updateCoefficients();
}
setMoonIntensity(intensity) {
this.moonIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonRadius(degrees) {
const rad = degrees * (Math.PI / 180.0);
this.moonHalo[0] = Math.cos(rad);
this.moonHalo[1] = Math.sin(rad);
this.updateCoefficients();
}
setMoonEnabled(enabled) {
this.moonHalo[3] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
setMilkyWayControl(intensity, saturation, blackPoint) {
this.milkyWayControl[0] = Math.max(0.0, intensity);
this.milkyWayControl[1] = Math.max(0.0, saturation);
if (blackPoint !== undefined) {
this.milkyWayControl[2] = Math.max(0.0, blackPoint);
}
this.updateCoefficients();
}
setMilkyWayEnabled(enabled) {
this.milkyWayEnabled = !!enabled;
this.updateCoefficients();
}
setMilkyWayRotation(rotationMatrix) {
if (rotationMatrix && rotationMatrix.length === 9) {
this.milkyWayRotation = rotationMatrix;
this.updateCoefficients();
}
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -577,130 +297,15 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('contrast', this.contrast);
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity); // Lux scaling
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity);
this.materialInstance.setFloat3Parameter('nightColor', new Float32Array(nightColorScaled));
this.materialInstance.setFloat4Parameter('shimmerControl', new Float32Array(shimmerUniform));
this.materialInstance.setFloat4Parameter('cloudControl', new Float32Array(cloudUniform));
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloat2Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
// Moon Upload (Secondary Sun)
this.materialInstance.setFloat3Parameter('sunDirection2', new Float32Array(this.moonDirection));
// Calculate Moon Phase Factor (Lambertian Sphere)
// We model the moon as a Lambertian sphere to calculate its integrated brightness (illuminance)
// based on the phase angle (angle between Sun-Moon and Observer-Moon vectors).
//
// Phase Angle (alpha):
// For a distant observer (Earth), the phase angle can be approximated as the angle between
// the vector to the Sun and the vector to the Earth (from the Moon).
// cos(alpha) = -dot(L_moon, L_sun)
//
// Lambertian Phase Law:
// The integrated flux of a lit sphere varies as:
// Phi(alpha) = (1/PI) * (sin(alpha) + (PI - alpha) * cos(alpha))
// This gives 1.0 at Full Moon (alpha=0) and 0.0 at New Moon (alpha=PI).
const dotSM = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
// Final Intensity = Peak * Scale (No Phase Factor - Phase is handled in Shader via N.L)
const MOON_PEAK_LUX = 1.0;
const finalMoonIntensity = MOON_PEAK_LUX * this.moonIntensity;
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
this.materialInstance.setFloat3Parameter('milkyWayControl', new Float32Array(mwUniform));
this.materialInstance.setMat3Parameter('milkyWayRotation', new Float32Array(this.milkyWayRotation));
// Moon Halo Upload (Disk Visualization)
// Multiplier = 1.0 / SolidAngle
const moonSolidAngle = 2.0 * F_PI * (1.0 - this.moonHalo[0]);
const moonRadConv = 1.0 / Math.max(1e-9, moonSolidAngle);
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);
const moonRadius = Math.acos(this.moonHalo[0]);
// Dot product of Sun and Moon directions
const dot = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
const separation = Math.acos(Math.max(-1.0, Math.min(1.0, dot)));
let eclipseFactor = 1.0;
// Only calculate if moon is enabled
if (this.moonHalo[3] > 0.5) {
const overlap = this.areaIntersection(sunRadius, moonRadius, separation);
const sunArea = Math.PI * sunRadius * sunRadius;
// Ensure we don't divide by zero and clamp result
const ratio = overlap / Math.max(1e-9, sunArea);
eclipseFactor = 1.0 - Math.max(0.0, Math.min(1.0, ratio));
}
// Safety check for NaN
if (isNaN(eclipseFactor)) {
console.warn("SimulatedSkybox: eclipseFactor is NaN, resetting to 1.0");
eclipseFactor = 1.0;
}
this.materialInstance.setFloatParameter('eclipseFactor', eclipseFactor);
}
areaIntersection(r1, r2, d) {
// Circle intersection area
// r1, r2: radii
// d: distance between centers
// Case 1: Too far apart
if (d >= r1 + r2) {
return 0.0;
}
// Case 2: One inside another
if (d <= Math.abs(r1 - r2)) {
return Math.PI * Math.min(r1, r2) * Math.min(r1, r2);
}
const r1sq = r1 * r1;
const r2sq = r2 * r2;
// Law of Cosines for sector angles
// c1 = (d^2 + r1^2 - r2^2) / (2 * d * r1)
// c2 = (d^2 + r2^2 - r1^2) / (2 * d * r2)
// We clamp to [-1, 1] to avoid NaN from floating point errors
const c1 = Math.max(-1.0, Math.min(1.0, (d * d + r1sq - r2sq) / (2.0 * d * r1)));
const c2 = Math.max(-1.0, Math.min(1.0, (d * d + r2sq - r1sq) / (2.0 * d * r2)));
const part1 = r1sq * Math.acos(c1);
const part2 = r2sq * Math.acos(c2);
// Heron's formula for the triangle area * 2
// The sqrt term represents the area of the two triangles formed by the chord and centers.
// Robust sqrt
const val = (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2);
const part3 = 0.5 * Math.sqrt(Math.max(0.0, val));
return part1 + part2 - part3;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

@@ -1,332 +0,0 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
*/
(function () { 'use strict';
// shortcuts for easier to read formulas
var PI = Math.PI,
sin = Math.sin,
cos = Math.cos,
tan = Math.tan,
asin = Math.asin,
atan = Math.atan2,
acos = Math.acos,
rad = PI / 180;
// sun calculations are based on https://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
var dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
function toDays(date) { return toJulian(date) - J2000; }
// general calculations for position
var e = rad * 23.4397; // obliquity of the Earth
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
function astroRefraction(h) {
if (h < 0) // the following formula works for positive altitudes only.
h = 0; // if h = -0.08901179 a div/0 would occur.
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
}
// general sun calculations
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
function eclipticLongitude(M) {
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
P = rad * 102.9372; // perihelion of the Earth
return M + C + P + PI;
}
function sunCoords(d) {
var M = solarMeanAnomaly(d),
L = eclipticLongitude(M);
return {
dec: declination(L, 0),
ra: rightAscension(L, 0)
};
}
var SunCalc = {};
// calculates sun position for a given date and latitude/longitude
SunCalc.getPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = sunCoords(d),
H = siderealTime(d, lw) - c.ra;
return {
azimuth: azimuth(H, phi, c.dec),
altitude: altitude(H, phi, c.dec)
};
};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[-0.3, 'sunriseEnd', 'sunsetStart' ],
[-6, 'dawn', 'dusk' ],
[-12, 'nauticalDawn', 'nauticalDusk'],
[-18, 'nightEnd', 'night' ],
[6, 'goldenHourEnd', 'goldenHour' ]
];
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// calculations for sun times
var J0 = 0.0009;
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
// returns set time for the given sun altitude
function getSetJ(h, lw, phi, dec, n, M, L) {
var w = hourAngle(h, phi, dec),
a = approxTransit(w, lw, n);
return solarTransitJ(a, M, L);
}
// calculates sun times for a given date, latitude/longitude, and, optionally,
// the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height) {
height = height || 0;
var lw = rad * -lng,
phi = rad * lat,
dh = observerAngle(height),
d = toDays(date),
n = julianCycle(d, lw),
ds = approxTransit(0, lw, n),
M = solarMeanAnomaly(ds),
L = eclipticLongitude(M),
dec = declination(L, 0),
Jnoon = solarTransitJ(ds, M, L);
var result = {
solarNoon: fromJulian(Jnoon),
nadir: fromJulian(Jnoon - 0.5)
};
var i, len, time, h0, Jset, Jrise;
for (i = 0, len = times.length; i < len; i += 1) {
time = times[i];
h0 = (time[0] + dh) * rad;
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
Jrise = Jnoon - (Jset - Jnoon);
result[time[1]] = fromJulian(Jrise);
result[time[2]] = fromJulian(Jset);
}
return result;
};
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
M = rad * (134.963 + 13.064993 * d), // mean anomaly
F = rad * (93.272 + 13.229350 * d), // mean distance
l = L + rad * 6.289 * sin(M), // longitude
b = rad * 5.128 * sin(F), // latitude
dt = 385001 - 20905 * cos(M); // distance to the moon in km
return {
ra: rightAscension(l, b),
dec: declination(l, b),
dist: dt
};
}
SunCalc.getMoonPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = moonCoords(d),
H = siderealTime(d, lw) - c.ra,
h = altitude(H, phi, c.dec),
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
return {
azimuth: azimuth(H, phi, c.dec),
altitude: h + astroRefraction(h), // altitude correction for refraction,
distance: c.dist,
parallacticAngle: pa
};
};
// calculations for illumination parameters of the moon,
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
SunCalc.getMoonIllumination = function (date) {
var d = toDays(date || new Date()),
s = sunCoords(d),
m = moonCoords(d),
sdist = 149598000, // distance from Earth to Sun in km
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
return {
fraction: (1 + cos(inc)) / 2,
phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
angle: angle
};
};
function hoursLater(date, h) {
return new Date(date.valueOf() + h * dayMs / 24);
}
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
var t = new Date(date);
if (inUTC) t.setUTCHours(0, 0, 0, 0);
else t.setHours(0, 0, 0, 0);
var hc = 0.133 * rad,
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
for (var i = 1; i <= 24; i += 2) {
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
a = (h0 + h2) / 2 - h1;
b = (h2 - h0) / 2;
xe = -b / (2 * a);
ye = (a * xe + b) * xe + h1;
d = b * b - 4 * a * h1;
roots = 0;
if (d >= 0) {
dx = Math.sqrt(d) / (Math.abs(a) * 2);
x1 = xe - dx;
x2 = xe + dx;
if (Math.abs(x1) <= 1) roots++;
if (Math.abs(x2) <= 1) roots++;
if (x1 < -1) x1 = x2;
}
if (roots === 1) {
if (h0 < 0) rise = i + x1;
else set = i + x1;
} else if (roots === 2) {
rise = i + (ye < 0 ? x2 : x1);
set = i + (ye < 0 ? x1 : x2);
}
if (rise && set) break;
h0 = h2;
}
var result = {};
if (rise) result.rise = hoursLater(t, rise);
if (set) result.set = hoursLater(t, set);
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
return result;
};
// observerAngle helper was missing in my copy-paste?
// Ah, it was in the chunks. Let me check the chunks again.
// Function `observerAngle` was in chunk 0.
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
// I included it.
// Note: I replaced var with const/let or kept var?
// The original used const/let but I used var in my head above?
// I will just use the original code but adapted.
// Actually, to be safe, I'll use the code I wrote in this block which is adapted to use 'var' (ES5 safe) or just ES6 is fine.
// The user Environment is modern (Chrome/ES6 likely).
// But for safety:
// I'll stick to 'var' definitions I wrote above which seem complete?
// Wait, I might have missed `observerAngle` definition in my manual reconstruction above?
// No, `observerAngle` is called in `getTimes`.
// I MUST define it.
// I added it in the loop above? No.
// Let me double check the `write_to_file` content I prepared.
// I see:
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
// Yes, it is there.
// export
window.SunCalc = SunCalc;
}());
@@ -1,10 +1,10 @@
# Result: /Users/mathias/sources/git/filament/out/cmake-release/tools/matc/matc
MATC="../../../../../out/cmake-release/tools/matc/matc"
MATC="../../../../out/cmake-release/tools/matc/matc"
# Navigate to script directory to ensure relative paths work
cd "$(dirname "$0")"
set -e
$MATC -a opengl -p mobile -o ../assets/simulated_skybox.filamat ../simulated_skybox.mat
$MATC -a opengl -p mobile -o assets/simulated_skybox.filamat simulated_skybox.mat
echo "Material recompiled to assets/simulated_skybox.filamat"
File diff suppressed because one or more lines are too long
Binary file not shown.
+2 -7
View File
@@ -24,7 +24,6 @@
<body>
<canvas></canvas>
<!-- Filament -->
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
@@ -32,13 +31,9 @@
<!-- UI -->
<script src="lil-gui.js"></script>
<!-- Utils -->
<script src="assets/suncalc_global.js"></script>
<!-- App -->
<script src="SimulatedSkybox.js?v=9999"></script>
<script src="main.js?v=9999"></script>
<script src="SimulatedSkybox.js?v=26"></script>
<script src="main.js?v=26"></script>
</body>
</html>
+97 -715
View File
@@ -1,123 +1,10 @@
// main.js
Filament.init(['assets/simulated_skybox.filamat?v=' + Date.now()], () => {
Filament.init(['assets/simulated_skybox.filamat'], () => {
window.app = new App(document.getElementsByTagName('canvas')[0]);
});
// Helper: Julian Date
function getJD(date) {
return date.getTime() / 86400000.0 + 2440587.5;
}
// Helper: GMST from Date
function getGMST(date) {
const JD = getJD(date);
const D = JD - 2451545.0;
// GMST = 18.697... + 24.0657... * D
let gmst = 18.697374558 + 24.06570982441908 * D;
gmst = gmst % 24.0;
if (gmst < 0) gmst += 24.0;
return gmst;
}
// Helper: Matrix Rotation
function rotateX(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m1 = m[1], m2 = m[2];
const m4 = m[4], m5 = m[5];
const m7 = m[7], m8 = m[8];
m[1] = m1 * c - m2 * s;
m[2] = m1 * s + m2 * c;
m[4] = m4 * c - m5 * s;
m[5] = m4 * s + m5 * c;
m[7] = m7 * c - m8 * s;
m[8] = m7 * s + m8 * c;
}
function rotateY(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m0 = m[0], m2 = m[2];
const m3 = m[3], m5 = m[5];
const m6 = m[6], m8 = m[8];
m[0] = m0 * c + m2 * s;
m[2] = -m0 * s + m2 * c;
m[3] = m3 * c + m5 * s;
m[5] = -m3 * s + m5 * c;
m[6] = m6 * c + m8 * s;
m[8] = -m6 * s + m8 * c;
}
function rotateZ(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const m0 = m[0], m1 = m[1];
const m3 = m[3], m4 = m[4];
const m6 = m[6], m7 = m[7];
m[0] = m0 * c - m1 * s;
m[1] = m0 * s + m1 * c;
m[3] = m3 * c - m4 * s;
m[4] = m3 * s + m4 * c;
m[6] = m6 * c - m7 * s;
m[7] = m6 * s + m7 * c;
}
// Galactic to Equatorial (J2000)
// This matrix converts Galactic vectors to Equatorial vectors.
// Or effectively, if we want to render Galactic texture from Equatorial View vector V_eq:
// V_gal = Inv(Rot_Gal_to_Eq) * V_eq = Rot_Eq_to_Gal * V_eq.
// The shader does: V_gal = Rotation * V_world.
// So Rotation = Rot_Eq_to_Gal * Rot_World_to_Eq.
//
// Galactic North Pole (J2000): RA = 192.85948, Dec = 27.12825
// Ascending Node: RA = 282.85
//
// Pre-computed Rotation Matrix (Equatorial -> Galactic)
// Based on standard transformation matrices.
//
// R_eq_gal =
// [ -0.054876 -0.873437 -0.483835 ]
// [ 0.494109 -0.444830 0.746982 ]
// [ -0.867666 -0.198076 0.455984 ]
//
// Let's use this static definition.
const MAT_EQ_TO_GAL = [
-0.054876, 0.494109, -0.867666,
-0.873437, -0.444830, -0.198076,
-0.483835, 0.746982, 0.455984
];
// Matrix multiplication 3x3
function multiplyMat3(a, b) {
const out = new Float32Array(9);
// Row-major or Column-major? Filament is Column-major usually.
// GLSL is Column-major.
// Mat3 in array: [col0.x, col0.y, col0.z, col1.x, ...]
// So a[0] is (0,0), a[1] is (1,0), a[3] is (0,1).
//
// out = a * b
const a00 = a[0], a10 = a[1], a20 = a[2];
const a01 = a[3], a11 = a[4], a21 = a[5];
const a02 = a[6], a12 = a[7], a22 = a[8];
const b00 = b[0], b10 = b[1], b20 = b[2];
const b01 = b[3], b11 = b[4], b21 = b[5];
const b02 = b[6], b12 = b[7], b22 = b[8];
out[0] = a00 * b00 + a01 * b10 + a02 * b20;
out[1] = a10 * b00 + a11 * b10 + a12 * b20;
out[2] = a20 * b00 + a21 * b10 + a22 * b20;
out[3] = a00 * b01 + a01 * b11 + a02 * b21;
out[4] = a10 * b01 + a11 * b11 + a12 * b21;
out[5] = a20 * b01 + a21 * b11 + a22 * b21;
out[6] = a00 * b02 + a01 * b12 + a02 * b22;
out[7] = a10 * b02 + a11 * b12 + a12 * b22;
out[8] = a20 * b02 + a21 * b12 + a22 * b22;
return out;
}
class App {
constructor(canvas) {
this.canvas = canvas;
@@ -128,10 +15,13 @@ class App {
this.skybox.entity = this.skybox.entity; // Ensuring access if needed
this.scene.addEntity(this.skybox.entity);
// Load the material explicitly. SimulatedSkybox.loadMaterial fetches it.
const matUrl = 'assets/simulated_skybox.filamat?v=' + Date.now();
this.skybox.loadMaterial(matUrl).then(() => {
// Load the material explicitly since we passed it to init but SimulatedSkybox needs to bind it
// Actually SimulatedSkybox.loadMaterial fetches it.
// Since we already loaded it in Filament.init, we can arguably just use it if we had a way to access the asset.
// But Filament.init assets are for internal or easy access via assets object if configured?
// Let's just let SimulatedSkybox fetch it again or use a blob if we wanted.
// Simpler: Just let SimulatedSkybox fetch it.
this.skybox.loadMaterial('assets/simulated_skybox.filamat').then(() => {
this.initGUI();
});
@@ -181,15 +71,6 @@ class App {
this.initControls(); // Initialize controls immediately
this.mwParams = {
enabled: true,
intensity: 1.0,
saturation: 1.0,
blackPoint: 0.07,
siderealTime: 0.0, // Hours [0-24]
latitude: 34.0, // Default Lat
};
this.resize();
window.addEventListener('resize', this.resize.bind(this));
@@ -218,12 +99,6 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
// Moon Exposure
if (this.mParams) {
const preExposedMoon = this.mParams.intensity * exposure;
this.skybox.setMoonIntensity(preExposedMoon);
}
}
updateCameraProjection() {
@@ -231,7 +106,6 @@ class App {
const height = this.canvas.height;
const aspect = width / height;
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setFocalLength(this.params.focalLength);
}
initGUI() {
@@ -240,7 +114,10 @@ class App {
const sky = this.skybox;
// Initialize local params from skybox defaults
// Initialize local params from skybox defaults
// REMOVED: Do not overwrite this.params from sky.sunDirection (Zenith)
// const currentDir = sky.sunDirection;
// this.params.sunTheta = ...
const updateSun = () => {
const theta = this.params.sunTheta;
@@ -251,165 +128,29 @@ class App {
sky.setSunPosition([x, y, z]);
};
// Sun UI Proxy
this.sunUI = {
azimuth: (this.params.sunPhi * 180.0 / Math.PI) % 360.0,
height: Math.cos(this.params.sunTheta)
};
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
const sunFolder = gui.addFolder('Sun');
sunFolder.add(this.sunUI, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(v => {
this.params.sunPhi = v * (Math.PI / 180.0);
updateSun();
});
sunFolder.add(this.sunUI, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(v => {
// Helper for "Sun Height" cosine slider like C++
const sunHeightParam = { height: Math.cos(this.params.sunTheta) };
sunFolder.add(sunHeightParam, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
this.params.sunTheta = Math.acos(v);
updateSun();
});
sunFolder.add(this.params, 'sunPhi', 0.0, Math.PI * 2).name('Azimuth').onChange(updateSun);
// Updated: Controls params.sunIntensity and triggers updateSunIntensity
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).onChange(v => this.updateSunIntensity());
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).name('Intensity').onChange(v => this.updateSunIntensity());
const moonFolder = gui.addFolder('Moon');
this.mParams = {
enabled: true,
azimuth: 180.0,
height: Math.cos(45.0 * Math.PI / 180.0), // Default 45 degrees elevation -> cos(45) ~ 0.707
radius: 1.2,
intensity: 6.0
};
const updateMoon = () => {
const az = this.mParams.azimuth * (Math.PI / 180.0);
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
};
// Initial Moon Sync
updateMoon();
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
sky.setMoonIntensity(this.mParams.intensity);
moonFolder.add(this.mParams, 'enabled').name('Enabled').onChange(v => sky.setMoonEnabled(v));
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(updateMoon);
moonFolder.add(this.mParams, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(updateMoon);
moonFolder.add(this.mParams, 'intensity', 0.0, 1000.0).name('Intensity').onChange(v => this.updateSunIntensity());
moonFolder.add(this.mParams, 'radius', 0.1, 5.0).name('Radius').onChange(v => sky.setMoonRadius(v));
moonFolder.close();
const mwFolder = gui.addFolder('Milky Way');
const updateMW = () => {
sky.setMilkyWayEnabled(this.mwParams.enabled);
sky.setMilkyWayControl(this.mwParams.intensity, this.mwParams.saturation, this.mwParams.blackPoint);
// Calculate Rotation
// V_gal = Rot_Eq_to_Gal * Rot_World_to_Eq * V_world
// World: Y=Up, X=East, Z=South (Filament Camera Convention is different!)
// In Filament Camera: -Z is Forward.
// Skybox V direction is World Space direction.
// Let's assume standard Horizontal Coordinates:
// Y = Zenith.
// Z = North? Or South?
// Usually Z is South in RH Y-up.
// LST (Local Sidereal Time) converts Hour Angle to RA.
// LST in Radians.
const LST = this.mwParams.siderealTime * (Math.PI / 12.0); // Hours to Rad
const Lat = this.mwParams.latitude * (Math.PI / 180.0);
// Rotation World (Horizontal) -> Equatorial
// 1. Rotate around X by -(90 - Lat) to align Equatorial Plane.
// 2. Rotate around Y (Polar Axis) by -LST.
// Mat3 Identity
const rot = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// Rotate Z by LST (Earth Rotation).
// Actually, transformation from Horizontal (Az, Alt) to Equatorial (HA, Dec):
// sin(Dec) = sin(Alt)sin(Lat) - cos(Alt)cos(Lat)cos(Az)
// ...
// Let's construct matrix directly.
// WorldToEq:
// Rotate X by (Lat - 90 deg) -> brings Pole to Zenith.
// Rotate Y by -LST (or Z?)
// Filament Space:
// +Y = Up
// Let's match typical skybox conventions.
// Rot_World_to_Eq = Rot_Z_LST * Rot_X_Lat
// But we need to use Filament matrix ops which are column major.
// Let's use simple rotations:
// 1. Tilt Pole: Rotate X by (Lat - 90).
// 2. Spin Earth: Rotate Y by LST.
// Let's iterate until it looks right visually or trust the math.
// Rot_World_To_Equatorial:
// R_z(-LST) * R_x(Lat - 90)?
// Let's build it from scratch in JS using helper.
// Start Identity.
// Rotate X (Latitude Tilt).
// Rotate Y (Sidereal Spin).
// Note: rotate functions modify in place.
const mWorldToEq = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// 1. Tilt for Latitude (Align Celestial Pole)
// At Lat 90 (North Pole), Zenith is Pole. No tilt needed if Y is Pole?
// No, Y is Zenith. Pole is Y.
// At Lat 0 (Equator), Pole is at Horizon (Z?).
// So we rotate X by (Lat - 90).
rotateX(mWorldToEq, Lat - Math.PI / 2);
// 2. Spin for Time (LST)
// Rotate around new Pole (Y) by LST.
rotateY(mWorldToEq, LST);
// Combine with Gal Transform
// Rot = MAT_EQ_TO_GAL * mWorldToEq
const finalRot = multiplyMat3(MAT_EQ_TO_GAL, mWorldToEq);
sky.setMilkyWayRotation(finalRot);
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
mwFolder.add(this.mwParams, 'latitude', -90.0, 90.0).name('Latitude').onChange(updateMW);
mwFolder.close();
// Initial MW Update
updateMW();
this.updateMW = updateMW; // Export for sync
const sunDisk = sunFolder.addFolder('Disk');
// We need local proxy for sunRadius due to conversion
this.diskParams = {
radius: 1.2
const diskParams = {
radius: 1.2,
enabled: true // Enable sun disk
};
sky.setSunDiskEnabled(true);
sky.setSunRadius(1.2);
sunFolder.add(this.diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
sunFolder.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
sunFolder.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
sunDisk.add(diskParams, 'enabled').onChange(v => sky.setSunDiskEnabled(v));
sunDisk.add(diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
sunDisk.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
sunDisk.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
const atmFolder = gui.addFolder('Atmosphere');
atmFolder.add(sky, 'turbidity', 1.0, 10.0).onChange(v => sky.setTurbidity(v));
@@ -420,69 +161,12 @@ class App {
atmFolder.add(sky, 'ozone', 0.0, 1.0).onChange(v => sky.setOzone(v));
atmFolder.add(sky, 'mieG', 0.0, 0.999).onChange(v => sky.setMieG(v));
const cloudFolder = gui.addFolder('Clouds');
this.cParams = {
volumetrics: sky.cloudControl2[1] > 0.5,
coverage: 0.4,
density: 0.02,
height: sky.cloudControl[2],
speed: 50.0,
evolution: 0.02
};
// Apply Cloud Defaults
sky.setCloudControl(0.4, 0.02, this.cParams.height, 50.0);
sky.setCloudShapeEvolution(0.02);
cloudFolder.add(this.cParams, 'volumetrics').onChange(v => sky.setCloudVolumetricLighting(v));
cloudFolder.add(this.cParams, 'coverage', 0.0, 1.0).onChange(v => sky.setCloudControl(v, this.cParams.density, this.cParams.height, this.cParams.speed));
cloudFolder.add(this.cParams, 'density', 0.0, 1.0).onChange(v => sky.setCloudControl(this.cParams.coverage, v, this.cParams.height, this.cParams.speed));
cloudFolder.add(this.cParams, 'height', 2000.0, 20000.0).onChange(v => sky.setCloudControl(this.cParams.coverage, this.cParams.density, v, this.cParams.speed));
// Reverse speed calc: w = speed * (0.05 / 72.0)
cloudFolder.add(this.cParams, 'speed', 0.0, 200.0).onChange(v => sky.setCloudControl(this.cParams.coverage, this.cParams.density, this.cParams.height, v));
cloudFolder.add(this.cParams, 'evolution', 0.0, 2.0).onChange(v => sky.setCloudShapeEvolution(v));
const waterFolder = gui.addFolder('Water');
this.wParams = {
derivativeTrick: true,
strength: 50.0,
speed: 1.0,
octaves: 4.0
};
// Initialize defaults
sky.setWaterControl(50.0, 1.0, 1.0, 4.0); // 1.0 = Derivative Trick On, 4 octaves
const updateWater = () => {
sky.setWaterControl(this.wParams.strength, this.wParams.speed, this.wParams.derivativeTrick ? 1.0 : 0.0, this.wParams.octaves);
};
waterFolder.add(this.wParams, 'derivativeTrick').name('Derivative Trick').onChange(updateWater);
waterFolder.add(this.wParams, 'strength', 10.0, 100.0).onChange(updateWater);
waterFolder.add(this.wParams, 'speed', 0.0, 5.0).onChange(updateWater);
waterFolder.add(this.wParams, 'octaves', 1, 8, 1).name('Octaves').onChange(updateWater);
waterFolder.close();
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.close();
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
sky.setHorizonGlow(0.0);
sky.msFactors[2] = 0.0;
// Set Contrast default to 1.0
sky.setContrast(1.0);
// Set Horizon Glow default to 1.0
sky.setHorizonGlow(1.0);
sky.msFactors[2] = 1.0;
// Set Contrast default to 0.85
sky.setContrast(0.85);
artFolder.add(sky.msFactors, 0, 0.0, 2.0).name('MS Rayleigh').onChange(v => sky.setMultiScattering(v, sky.msFactors[1]));
artFolder.add(sky.msFactors, 1, 0.0, 2.0).name('MS Mie').onChange(v => sky.setMultiScattering(sky.msFactors[0], v));
@@ -499,6 +183,63 @@ class App {
shmFolder.add(sky.shimmerControl, 1, 1.0, 100.0).name('Frequency').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], v, sky.shimmerControl[2]));
shmFolder.add(sky.shimmerControl, 2, 0.01, 0.5).name('Mask Height').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], sky.shimmerControl[1], v));
const cloudFolder = gui.addFolder('Clouds');
const cParams = {
volumetrics: sky.cloudControl2[1] > 0.5,
coverage: 0.4,
density: 0.02,
height: sky.cloudControl[2],
speed: 50.0,
evolution: 0.02
};
// Apply Cloud Defaults
sky.setCloudControl(0.4, 0.02, cParams.height, 50.0);
sky.setCloudShapeEvolution(0.02);
cloudFolder.add(cParams, 'volumetrics').onChange(v => sky.setCloudVolumetricLighting(v));
cloudFolder.add(cParams, 'coverage', 0.0, 1.0).onChange(v => sky.setCloudControl(v, cParams.density, cParams.height, cParams.speed));
cloudFolder.add(cParams, 'density', 0.0, 1.0).onChange(v => sky.setCloudControl(cParams.coverage, v, cParams.height, cParams.speed));
cloudFolder.add(cParams, 'height', 2000.0, 20000.0).onChange(v => sky.setCloudControl(cParams.coverage, cParams.density, v, cParams.speed));
// Reverse speed calc: w = speed * (0.05 / 72.0)
cloudFolder.add(cParams, 'speed', 0.0, 200.0).onChange(v => sky.setCloudControl(cParams.coverage, cParams.density, cParams.height, v));
cloudFolder.add(cParams, 'evolution', 0.0, 2.0).onChange(v => sky.setCloudShapeEvolution(v));
const waterFolder = gui.addFolder('Water');
const wParams = {
derivativeTrick: true,
strength: 50.0,
speed: 1.0,
octaves: 4.0
};
// Initialize defaults
sky.setWaterControl(50.0, 1.0, 1.0, 4.0); // 1.0 = Derivative Trick On, 4 octaves
const updateWater = () => {
sky.setWaterControl(wParams.strength, wParams.speed, wParams.derivativeTrick ? 1.0 : 0.0, wParams.octaves);
};
waterFolder.add(wParams, 'derivativeTrick').name('Derivative Trick').onChange(updateWater);
waterFolder.add(wParams, 'strength', 10.0, 100.0).onChange(updateWater);
waterFolder.add(wParams, 'speed', 0.0, 5.0).onChange(updateWater);
waterFolder.add(wParams, 'octaves', 1, 8, 1).name('Octaves').onChange(updateWater);
waterFolder.close();
const starFolder = gui.addFolder('Stars');
const sParams = {
enabled: true,
density: 1.0
};
// Initialize defaults (Density 1.0, Enabled True)
sky.setStarControl(1.0, true);
const updateStars = () => {
sky.setStarControl(sParams.density, sParams.enabled);
};
starFolder.add(sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(sParams, 'density', 0.0, 1.0).name('Density').onChange(updateStars);
starFolder.close();
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
@@ -506,362 +247,35 @@ class App {
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
this.bParams = {
enabled: true,
const bParams = {
enabled: false,
lensFlare: false
};
const updateBloom = () => {
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare
enabled: bParams.enabled,
lensFlare: bParams.lensFlare
});
};
bloomFolder.add(this.bParams, 'enabled').onChange(updateBloom);
bloomFolder.add(this.bParams, 'lensFlare').onChange(updateBloom);
bloomFolder.add(bParams, 'enabled').onChange(updateBloom);
bloomFolder.add(bParams, 'lensFlare').onChange(updateBloom);
bloomFolder.close();
// Collapse folders by default
sunDisk.close();
atmFolder.close();
artFolder.close();
// shmFolder is inside artFolder, so it's hidden, but we can close it too if we want
shmFolder.close();
cloudFolder.close();
// camFolder left open by default for convenience.
// camFolder left open? User didn't specify, but "Artistic, shimmer and clouds" + "Disk, Atmosphere" were requested.
// So Camera might stay open or close. Let's keep Camera open for now as it wasn't listed.
// Initial sync
updateSun();
this.updateCameraExposure(); // This will trigger updateSunIntensity too
updateBloom();
// Check URL for config
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('config')) {
try {
const state = JSON.parse(atob(urlParams.get('config')));
this.applyURLState(state);
// Update GUI
gui.controllers.forEach(c => c.updateDisplay());
// Recursive update for folders
gui.folders.forEach(f => {
f.controllers.forEach(c => c.updateDisplay());
// And sub-folders if any (Shimmer/Bloom)
f.folders.forEach(sf => sf.controllers.forEach(sc => sc.updateDisplay()));
});
} catch (e) {
console.error("Failed to load config:", e);
}
}
const syncFolder = gui.addFolder('Real-Time Sync');
this.syncParams = {
enabled: false,
lat: 0.0,
lng: 0.0,
status: 'Disabled'
};
const updateSync = () => {
if (this.syncParams.enabled) {
if (navigator.geolocation) {
this.syncParams.status = "Locating...";
navigator.geolocation.getCurrentPosition(
(pos) => {
this.syncParams.lat = pos.coords.latitude;
this.syncParams.lng = pos.coords.longitude;
this.syncParams.status = "Active";
syncFolder.controllers.forEach(c => c.updateDisplay());
},
(err) => {
console.error(err);
this.syncParams.status = "Error (See Console)";
this.syncParams.enabled = false;
syncFolder.controllers.forEach(c => c.updateDisplay());
}
);
} else {
this.syncParams.status = "Not Supported";
}
} else {
this.syncParams.status = "Disabled";
}
syncFolder.controllers.forEach(c => c.updateDisplay());
};
syncFolder.add(this.syncParams, 'enabled').name('Enable Sync').onChange(updateSync);
syncFolder.add(this.syncParams, 'status').name('Status').disable().listen();
this.syncFolder = syncFolder;
const shareParams = {
copyUrl: () => {
const state = this.getURLState();
const str = btoa(JSON.stringify(state));
const url = `${window.location.origin}${window.location.pathname}?config=${str}`;
navigator.clipboard.writeText(url).then(() => {
alert("Configuration URL copied to clipboard!");
}).catch(err => {
console.error('Could not copy text: ', err);
prompt("Copy this URL:", url);
});
}
};
gui.add(shareParams, 'copyUrl').name('Share Configuration');
}
updateRealTimeSync() {
if (!this.syncParams || !this.syncParams.enabled || !window.SunCalc) return;
const now = new Date();
const lat = this.syncParams.lat;
const lng = this.syncParams.lng;
// Sun
const sunPos = window.SunCalc.getPosition(now, lat, lng);
// Azimuth: South=0, West=PI/2.
// Skybox Phi: +X=0, +Z=PI/2.
// If +Z is South:
// SunAz 0 (South) -> Skybox PI/2 (+Z).
// SunAz PI/2 (West) -> Skybox PI (-X).
// So Phi = Az + PI/2.
const sunPhi = sunPos.azimuth + Math.PI / 2;
// Altitude: 0=Horizon, PI/2=Zenith.
// Skybox Theta: 0=Zenith, PI/2=Horizon.
const sunTheta = Math.PI / 2 - sunPos.altitude;
this.params.sunPhi = sunPhi;
this.params.sunTheta = sunTheta;
// Moon
const moonPos = window.SunCalc.getMoonPosition(now, lat, lng);
const moonPhi = moonPos.azimuth + Math.PI / 2;
const moonTheta = Math.PI / 2 - moonPos.altitude;
this.mParams.azimuth = (moonPhi * 180.0 / Math.PI) % 360.0;
this.mParams.height = Math.cos(moonTheta);
// Milky Way Sync
const gmst = getGMST(now);
const lst = (gmst + lng / 15.0 + 24.0) % 24.0;
this.mwParams.siderealTime = lst;
this.mwParams.latitude = lat;
if (this.updateMW) this.updateMW();
// Update Skybox
const sky = this.skybox;
// Update Sun Vector
const sx = Math.sin(sunTheta) * Math.cos(sunPhi);
const sy = Math.cos(sunTheta);
const sz = Math.sin(sunTheta) * Math.sin(sunPhi);
sky.setSunPosition([sx, sy, sz]);
// Update Moon Vector
const mx = Math.sin(moonTheta) * Math.cos(moonPhi);
const my = Math.cos(moonTheta);
const mz = Math.sin(moonTheta) * Math.sin(moonPhi);
sky.setMoonPosition([mx, my, mz]);
// Update UI Proxies
if (this.sunUI) {
this.sunUI.azimuth = (sunPhi * 180.0 / Math.PI) % 360.0;
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
this.sunUI.height = Math.cos(sunTheta);
}
}
getURLState() {
// Update Camera LookAt
// Serialize current state (Minified)
// Mapping:
// p: params (Camera) -> a:aperture, ss:shutterSpeed, i:iso, st:sunTheta, sp:sunPhi, fl:focalLength, si:sunIntensity
// c: cParams (Clouds) -> v:volumetrics, co:coverage, d:density, h:height, s:speed, e:evolution
// w: wParams (Water) -> dt:derivativeTrick, st:strength, s:speed, o:octaves
// s: sParams (Stars) -> e:enabled, d:density
// b: bParams (Bloom) -> e:enabled, lf:lensFlare
// k: sky (Skybox) -> t:turbidity, r:rayleigh, mc:mieCoefficient, mg:mieG, o:ozone, ms:msFactors, co:contrast, nc:nightColor, sh:shimmerControl, hl:sunHalo
const p = this.params;
const c = this.cParams;
const w = this.wParams;
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
k: {
t: sk.turbidity,
r: sk.rayleigh,
mc: sk.mieCoefficient,
mg: sk.mieG,
o: sk.ozone,
ms: [...sk.msFactors],
co: sk.contrast,
nc: [...sk.nightColor],
sh: [...sk.shimmerControl],
hl: [...sk.sunHalo]
}
};
}
applyURLState(state) {
const p = state.p;
const c = state.c;
const w = state.w;
const s = state.s;
const b = state.b;
const m = state.m;
const k = state.k;
const cm = state.cm;
if (p) {
if (p.a !== undefined) this.params.aperture = p.a;
if (p.ss !== undefined) this.params.shutterSpeed = p.ss;
if (p.i !== undefined) this.params.iso = p.i;
if (p.st !== undefined) this.params.sunTheta = p.st;
if (p.sp !== undefined) this.params.sunPhi = p.sp;
if (p.fl !== undefined) this.params.focalLength = p.fl;
if (p.si !== undefined) this.params.sunIntensity = p.si;
}
if (c) {
if (c.v !== undefined) this.cParams.volumetrics = c.v;
if (c.co !== undefined) this.cParams.coverage = c.co;
if (c.d !== undefined) this.cParams.density = c.d;
if (c.h !== undefined) this.cParams.height = c.h;
if (c.s !== undefined) this.cParams.speed = c.s;
if (c.e !== undefined) this.cParams.evolution = c.e;
}
if (w) {
if (w.dt !== undefined) this.wParams.derivativeTrick = w.dt;
if (w.st !== undefined) this.wParams.strength = w.st;
if (w.s !== undefined) this.wParams.speed = w.s;
if (w.o !== undefined) this.wParams.octaves = w.o;
}
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
}
if (b) {
if (b.e !== undefined) this.bParams.enabled = b.e;
if (b.lf !== undefined) this.bParams.lensFlare = b.lf;
}
const sky = this.skybox;
if (k) {
if (k.t !== undefined) sky.setTurbidity(k.t);
if (k.r !== undefined) sky.setRayleigh(k.r);
if (k.mc !== undefined) sky.setMieCoefficient(k.mc);
if (k.mg !== undefined) sky.setMieG(k.mg);
if (k.o !== undefined) sky.setOzone(k.o);
if (k.ms) { sky.setMultiScattering(k.ms[0], k.ms[1]); sky.setHorizonGlow(k.ms[2]); }
if (k.co !== undefined) sky.setContrast(k.co);
if (k.nc) sky.setNightColor(k.nc);
if (k.sh) sky.setShimmerControl(k.sh[0], k.sh[1], k.sh[2]);
// Sun Halo
const savedHalo = k.hl;
if (savedHalo && savedHalo.length === 4) {
sky.sunHalo[0] = savedHalo[0];
sky.sunHalo[1] = savedHalo[1];
sky.sunHalo[2] = savedHalo[2];
sky.sunHalo[3] = savedHalo[3];
// Update derived UI params for Sun Disk
const rad = Math.acos(Math.max(-1.0, Math.min(1.0, savedHalo[0])));
this.diskParams.radius = rad * (180.0 / Math.PI);
this.diskParams.enabled = savedHalo[3] > 0.5;
}
sky.updateCoefficients();
}
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
// Update derived Sun UI
if (this.sunUI) {
this.sunUI.height = Math.cos(this.params.sunTheta);
this.sunUI.azimuth = (this.params.sunPhi * 180.0 / Math.PI) % 360.0;
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
}
// Apply Local Params via Setters
sky.setCloudControl(this.cParams.coverage, this.cParams.density, this.cParams.height, this.cParams.speed);
sky.setCloudVolumetricLighting(this.cParams.volumetrics);
sky.setCloudShapeEvolution(this.cParams.evolution);
sky.setWaterControl(this.wParams.strength, this.wParams.speed, this.wParams.derivativeTrick ? 1.0 : 0.0, this.wParams.octaves);
sky.setStarControl(this.sParams.density, this.sParams.enabled);
if (m) {
if (m.e !== undefined) this.mParams.enabled = m.e;
if (m.az !== undefined) this.mParams.azimuth = m.az;
// Compat: if 'el' exists (old link) convert to 'h'
if (m.h !== undefined) {
this.mParams.height = m.h;
} else if (m.el !== undefined) {
// Convert elevation degrees to height cos
this.mParams.height = Math.cos(m.el * Math.PI / 180.0);
}
if (m.r !== undefined) this.mParams.radius = m.r;
if (m.i !== undefined) this.mParams.intensity = m.i;
// Sync Moon
const az = this.mParams.azimuth * (Math.PI / 180.0);
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
sky.setMoonIntensity(this.mParams.intensity);
}
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare
});
// Update Sun Position from Params
const theta = this.params.sunTheta;
const phi = this.params.sunPhi;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setSunPosition([x, y, z]);
// Update Camera Projection (Focal Length) and Exposure (Aperture/Shutter/ISO)
this.updateCameraProjection();
this.updateCameraExposure();
}
initControls() {
@@ -891,41 +305,10 @@ class App {
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
});
// Touch support
this.canvas.addEventListener('touchstart', e => {
if (e.touches.length > 0) {
e.preventDefault(); // Prevent scroll/long-press
this.camState.dragging = true;
this.camState.lastX = e.touches[0].clientX;
this.camState.lastY = e.touches[0].clientY;
}
}, { passive: false });
window.addEventListener('touchend', () => {
this.camState.dragging = false;
});
window.addEventListener('touchmove', e => {
if (!this.camState.dragging || e.touches.length === 0) return;
e.preventDefault(); // Prevent scrolling
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
const dx = x - this.camState.lastX;
const dy = y - this.camState.lastY;
this.camState.lastX = x;
this.camState.lastY = y;
const sensitivity = 0.005;
this.camState.theta -= dx * sensitivity;
this.camState.phi += dy * sensitivity;
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
}, { passive: false });
}
render() {
this.updateRealTimeSync();
// Update Camera LookAt
const r = 1.0;
const theta = this.camState.theta;
@@ -958,6 +341,5 @@ class App {
const aspect = width / height;
// near=0.1, far=5000.0
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setResolution(height);
}
}
@@ -1,61 +0,0 @@
import os
import urllib.request
import ssl
from PIL import Image
import argparse
# URL of the Milky Way texture (Gaia EDR3) from ESA
# Low Res PNG (2.71 MB) is sufficient for our 1024x512 target
URL = "https://www.esa.int/var/esa/storage/images/esa_multimedia/images/2020/12/the_colour_of_the_sky_from_gaia_s_early_data_release_3/22358049-1-eng-GB/The_colour_of_the_sky_from_Gaia_s_Early_Data_Release_3.png"
OUTPUT_DIR = "assets"
OUTPUT_FILENAME = "milkyway.png"
TARGET_WIDTH = 1024
TARGET_HEIGHT = 512
def main():
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)
temp_path = os.path.join(OUTPUT_DIR, "temp_milkyway.png")
print(f"Downloading Milky Way texture from {URL}...")
# Bypass SSL verification globally
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
try:
req = urllib.request.Request(URL, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response, open(temp_path, 'wb') as out_file:
out_file.write(response.read())
except Exception as e:
print(f"Failed to download: {e}")
return
if not os.path.exists(temp_path):
print("Error: Download failed.")
return
print("Processing image...")
with Image.open(temp_path) as img:
# Convert to RGB (remove alpha if present, though this is likely opaque)
img = img.convert("RGB")
# Resize to user requested dimensions
print(f"Resizing to {TARGET_WIDTH}x{TARGET_HEIGHT}...")
img = img.resize((TARGET_WIDTH, TARGET_HEIGHT), Image.Resampling.LANCZOS)
# Save
print(f"Saving to {output_path}...")
img.save(output_path, "PNG")
# Cleanup
if os.path.exists(temp_path):
os.remove(temp_path)
print("Done!")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,21 +0,0 @@
#!/bin/bash
set -e
# Default size
SIZE=${1:-256}
OUTPUT_COLOR="../assets/moon_disk.png"
OUTPUT_NORMAL="../assets/moon_normal.png"
# Navigate to script directory
cd "$(dirname "$0")"
# Check dependencies
if ! python3 -c "import numpy, PIL" 2>/dev/null; then
echo "Installing dependencies (numpy, Pillow)..."
pip3 install numpy Pillow
fi
echo "Generating Moon Asset (Size: ${SIZE}x${SIZE})..."
python3 process_moon.py --size $SIZE --supersample 4 --blur 1.0 --output-color $OUTPUT_COLOR --output-normal $OUTPUT_NORMAL
echo "Generated $OUTPUT_COLOR and $OUTPUT_NORMAL"
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

@@ -1,249 +0,0 @@
import argparse
import urllib.request
import os
import sys
import math
import ssl
from PIL import Image
# Ensure numpy is available
try:
import numpy as np
except ImportError:
print("Error: numpy is required. Please install it with: pip install numpy")
sys.exit(1)
# Default URLs
COLOR_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_2k.jpg"
DISP_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/ldem_4.tif"
SRC_COLOR_FILENAME = "lroc_color_2k.jpg"
SRC_DISP_FILENAME = "ldem_4.tif"
def download_file(url, filename):
if os.path.exists(filename):
print(f"File {filename} already exists. Skipping download.")
return
print(f"Downloading {url} to {filename}...")
# Create unverified context to avoid potential SSL cert issues
context = ssl._create_unverified_context()
try:
with urllib.request.urlopen(url, context=context) as response, open(filename, 'wb') as out_file:
data = response.read()
out_file.write(data)
print(f"Download of {filename} complete.")
except Exception as e:
print(f"Error downloading file {filename}: {e}")
# Don't exit hard if it's just one file, maybe?
# But for this script, we likely need it.
sys.exit(1)
def equirectangular_to_orthographic(src_img, size, mode=None):
"""
Reprojects an equirectangular image to an orthographic projection (sphere view).
src_img: PIL Image (Equirectangular)
size: Output size (width, height) - usually square
"""
print(f"Reprojecting to {size}x{size} Orthographic Disk...")
width, height = size, size
src_w, src_h = src_img.size
# Create coordinate grid centered at 0,0 (-1 to 1)
y, x = np.mgrid[size/2:-size/2:-1, -size/2:size/2] # Note: y goes high to low
# Normalize to -1..1
x = x / (size / 2)
y = y / (size / 2)
# Mask for points outside the circle
r2 = x*x + y*y
mask = r2 <= 1.0
# Calculate sphere coordinates (z > 0 for front face)
z = np.zeros_like(r2)
z[mask] = np.sqrt(1.0 - r2[mask])
# Vector P = (x, y, z) on unit sphere
# Lat = asin(y)
# Lon = atan2(x, z)
# Apply mask to avoid invalid calculations
lat = np.arcsin(y * mask)
lon = np.arctan2(x * mask, z * mask)
# Map to UV [0, 1]
u = (lon / (2 * math.pi)) + 0.5
v = (lat / math.pi) + 0.5
# Map to Source Pixels
u = np.clip(u, 0, 1)
v = np.clip(v, 0, 1)
src_x = (u * (src_w - 1)).astype(np.int32)
src_y = ((1.0 - v) * (src_h - 1)).astype(np.int32) # Flip V for image coords
# Sample pixels
src_array = np.array(src_img)
# Handle dimensions
if len(src_array.shape) == 2:
# Grayscale / Single channel
out_channels = 1
src_array = src_array[:, :, np.newaxis] # Expand for consistent indexing
else:
out_channels = src_array.shape[2]
out_array = np.zeros((height, width, out_channels), dtype=src_array.dtype)
# Advanced indexing
valid_y, valid_x = np.where(mask)
# Extract coordinates for valid pixels
sx = src_x[valid_y, valid_x]
sy = src_y[valid_y, valid_x]
out_array[valid_y, valid_x] = src_array[sy, sx]
# Squeeze if single channel
if out_channels == 1:
out_array = out_array.squeeze(axis=2)
return Image.fromarray(out_array, mode or src_img.mode)
def compute_normal_map(height_img, scale=1.0, blur_radius=0.0):
print("Computing Normal Map from Height Map...")
# Convert to float array
h = np.array(height_img).astype(np.float32)
# Apply Blur if requested
if blur_radius > 0:
try:
from scipy.ndimage import gaussian_filter
print(f"Applying Gaussian Blur (Radius: {blur_radius})...")
h = gaussian_filter(h, sigma=blur_radius)
except ImportError:
print("Warning: scipy not found. Skipping Gaussian Blur.")
# Normalize height to 0..1 for consistent gradient scale regardless of input depth
h_min, h_max = h.min(), h.max()
print(f"Height Map Range: {h_min} to {h_max}")
if h_max > h_min:
h_norm = (h - h_min) / (h_max - h_min)
else:
h_norm = h
# Gradients
dy, dx = np.gradient(h_norm)
# Pre-emphasis scale
bump_scale = scale
# Normal vector components
# Map is Top-Down Y.
nx = -dx * bump_scale
ny = -dy * bump_scale
nz = np.ones_like(nx)
# Mask out normals where r > 0.96 (avoid edge cliff artifacts)
rows, cols = h.shape
y, x = np.ogrid[:rows, :cols]
center_y, center_x = rows/2.0, cols/2.0
# max radius is size/2
radius_sq = (min(rows, cols) / 2.0)**2
dist_sq = (x - center_x)**2 + (y - center_y)**2
mask = dist_sq < (radius_sq * 0.96 * 0.96)
nx[~mask] = 0
ny[~mask] = 0
nz[~mask] = 1
# Normalize
len_n = np.sqrt(nx*nx + ny*ny + nz*nz)
# Avoid divide by zero
len_n[len_n == 0] = 1.0
nx /= len_n
ny /= len_n
nz /= len_n
# Pack to 0..255
# [-1, 1] -> [0, 255]
out_x = ((nx + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_y = ((ny + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_z = ((nz + 1.0) * 0.5 * 255.0).astype(np.uint8)
out_rgb = np.dstack((out_x, out_y, out_z))
return Image.fromarray(out_rgb, 'RGB')
def main():
parser = argparse.ArgumentParser(description='Process Moon Texture')
parser.add_argument('--size', type=int, default=256, help='Output resolution (square)')
parser.add_argument('--supersample', type=int, default=2, help='Internal processing resolution multiplier')
parser.add_argument('--blur', type=float, default=1.0, help='Gaussian blur radius for height map')
parser.add_argument('--bump-scale', type=float, default=60.0, help='Normal map bump scale')
parser.add_argument('--output-color', type=str, default='assets/moon_disk.png', help='Output color filename')
parser.add_argument('--output-normal', type=str, default='assets/moon_normal.png', help='Output normal filename')
parser.add_argument('--skip-download', action='store_true', help='Skip downloading files')
args = parser.parse_args()
internal_size = args.size * args.supersample
# Ensure assets dir exists
os.makedirs(os.path.dirname(args.output_color) or '.', exist_ok=True)
# 1. Download
if not args.skip_download:
download_file(COLOR_URL, SRC_COLOR_FILENAME)
download_file(DISP_URL, SRC_DISP_FILENAME)
# 2. Process Color
print(f"Processing Color Map (Internal Size: {internal_size}x{internal_size})...")
try:
img_color = Image.open(SRC_COLOR_FILENAME).convert('RGB')
out_color = equirectangular_to_orthographic(img_color, internal_size)
if args.supersample > 1:
print(f"Downsampling Color to {args.size}x{args.size}...")
out_color = out_color.resize((args.size, args.size), Image.LANCZOS)
out_color.save(args.output_color)
print(f"Saved {args.output_color}")
except Exception as e:
print(f"Error processing color: {e}")
# 3. Process Normal
print(f"Processing Displacement Map (Internal Size: {internal_size}x{internal_size})...")
try:
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
# Check if file exists
if not os.path.exists(SRC_DISP_FILENAME):
print(f"Displacement map {SRC_DISP_FILENAME} not found!")
return
img_disp = Image.open(SRC_DISP_FILENAME)
out_disp = equirectangular_to_orthographic(img_disp, internal_size)
# Compute Normals
out_normal = compute_normal_map(out_disp, scale=args.bump_scale, blur_radius=args.blur)
if args.supersample > 1:
print(f"Downsampling Normal to {args.size}x{args.size}...")
out_normal = out_normal.resize((args.size, args.size), Image.LANCZOS)
out_normal.save(args.output_normal)
print(f"Saved {args.output_normal}")
except Exception as e:
print(f"Error processing normal: {e}")
import traceback
traceback.print_exc()
print("Done.")
if __name__ == "__main__":
main()
+25 -12
View File
@@ -57,7 +57,6 @@ set(SRCS
src/materials/colorGrading/colorGrading.cpp
src/materials/dof/dof.cpp
src/materials/flare/flare.cpp
src/materials/fog/fog.cpp
src/materials/fsr/fsr.cpp
src/materials/sgsr/sgsr.cpp
src/materials/ssao/ssao.cpp
@@ -92,7 +91,6 @@ set(SRCS
src/MaterialParser.cpp
src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp
src/ProgramSpecialization.cpp
src/RenderPass.cpp
src/RenderPrimitive.cpp
src/RenderTarget.cpp
@@ -189,7 +187,6 @@ set(PRIVATE_HDRS
src/MaterialInstanceManager.h
src/PIDController.h
src/PostProcessManager.h
src/ProgramSpecialization.h
src/RenderPass.h
src/RenderPrimitive.h
src/RendererUtils.h
@@ -284,7 +281,6 @@ set(MATERIAL_SRCS
src/materials/dof/dofTiles.mat
src/materials/dof/dofTilesSwizzle.mat
src/materials/flare/flare.mat
src/materials/fog/fog.mat
src/materials/fsr/fsr_easu.mat
src/materials/fsr/fsr_easu_mobile.mat
src/materials/fsr/fsr_easu_mobileF.mat
@@ -314,6 +310,11 @@ set(MATERIAL_FL0_SRCS
src/materials/skybox.mat
)
set(MATERIAL_MULTIVIEW_SRCS
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# ==================================================================================================
# Configuration
# ==================================================================================================
@@ -338,6 +339,11 @@ if (FILAMENT_ENABLE_FEATURE_LEVEL_0)
add_definitions(-DFILAMENT_ENABLE_FEATURE_LEVEL_0)
endif()
# Whether to include MULTIVIEW materials.
if (FILAMENT_ENABLE_MULTIVIEW)
add_definitions(-DFILAMENT_ENABLE_MULTIVIEW)
endif()
# Whether to force the profiling mode.
if (FILAMENT_FORCE_PROFILING_MODE)
add_definitions(-DFILAMENT_FORCE_PROFILING_MODE)
@@ -428,6 +434,21 @@ foreach(mat_dir ${MATERIAL_DIRS})
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
endif()
# --- Multiview variant ---
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
add_custom_command(
OUTPUT ${output_path_multiview}
COMMAND matc ${MATC_BASE_FLAGS} -PstereoscopicType=multiview -o ${output_path_multiview} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (Multiview)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_multiview})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_multiview})
endif()
endforeach()
# Generate a single resource file for the whole group
@@ -502,14 +523,6 @@ add_custom_command(
APPEND
)
add_custom_command(
OUTPUT "${MATERIAL_DIR}/fog.filamat"
DEPENDS ../shaders/src/surface_fog.fs
DEPENDS src/materials/utils/depthUtils.fs
DEPENDS src/materials/utils/geometry.fs
APPEND
)
add_custom_command(
OUTPUT "${MATERIAL_DIR}/sao.filamat"
DEPENDS src/materials/ssao/ssaoUtils.fs
-3
View File
@@ -305,8 +305,6 @@ if (FILAMENT_SUPPORTS_WEBGPU)
src/webgpu/WebGPUFence.h
src/webgpu/WebGPUIndexBuffer.cpp
src/webgpu/WebGPUIndexBuffer.h
src/webgpu/WebGPUMemoryMappedBuffer.cpp
src/webgpu/WebGPUMemoryMappedBuffer.h
src/webgpu/WebGPUMsaaTextureResolver.cpp
src/webgpu/WebGPUMsaaTextureResolver.h
src/webgpu/WebGPUPipelineCache.cpp
@@ -582,7 +580,6 @@ if (APPLE OR LINUX)
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
+4 -1
View File
@@ -20,7 +20,6 @@
#include <utils/CString.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Invocable.h>
#include <utils/Slice.h>
#include <backend/DriverEnums.h>
@@ -146,6 +145,10 @@ public:
return mSpecializationConstants;
}
SpecializationConstantsInfo& getSpecializationConstants() noexcept {
return mSpecializationConstants;
}
DescriptorSetInfo& getDescriptorBindings() noexcept {
return mDescriptorBindings;
}
@@ -617,16 +617,6 @@ DECL_DRIVER_API_N(readPixels,
uint32_t, height,
backend::PixelBufferDescriptor&&, data)
DECL_DRIVER_API_N(readTexture,
backend::TextureHandle, src,
uint8_t, level,
uint16_t, layer,
uint32_t, x,
uint32_t, y,
uint32_t, width,
uint32_t, height,
backend::PixelBufferDescriptor&&, data)
DECL_DRIVER_API_N(readBufferSubData,
backend::BufferObjectHandle, src,
uint32_t, offset,
-3
View File
@@ -84,9 +84,6 @@ public:
size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) {
// TODO: there's a fast-path where memcpy will work but currently not being taken advantage
// of.
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
+2 -3
View File
@@ -17,11 +17,10 @@
#include <backend/Program.h>
#include <backend/DriverEnums.h>
#include <utils/CString.h>
#include <utils/Invocable.h>
#include <utils/Panic.h>
#include <utils/debug.h>
#include <utils/CString.h>
#include <utils/ostream.h>
#include <utils/Invocable.h>
#include <utility>
+4 -51
View File
@@ -784,46 +784,6 @@ const char* toString(DescriptorFlags flags) {
return "NONE";
}
const char* toString(DescriptorType type) {
#define DESCRIPTOR_TYPE_CASE(TYPE) \
case DescriptorType::TYPE: { \
return #TYPE; \
}
switch (type) {
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_DEPTH)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_ARRAY_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_ARRAY_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_ARRAY_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_ARRAY_DEPTH)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_DEPTH)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_ARRAY_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_ARRAY_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_ARRAY_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_CUBE_ARRAY_DEPTH)
DESCRIPTOR_TYPE_CASE(SAMPLER_3D_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_3D_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_3D_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_ARRAY_FLOAT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_ARRAY_INT)
DESCRIPTOR_TYPE_CASE(SAMPLER_2D_MS_ARRAY_UINT)
DESCRIPTOR_TYPE_CASE(SAMPLER_EXTERNAL)
DESCRIPTOR_TYPE_CASE(UNIFORM_BUFFER)
DESCRIPTOR_TYPE_CASE(SHADER_STORAGE_BUFFER)
DESCRIPTOR_TYPE_CASE(INPUT_ATTACHMENT)
}
#undef DESCRIPTOR_TYPE_CASE
return "UNKNOWN";
}
void MetalDriver::createDescriptorSetLayoutR(
Handle<HwDescriptorSetLayout> dslh, DescriptorSetLayout&& info, utils::ImmutableCString&& tag) {
#if FILAMENT_METAL_DEBUG_LOG == 1
@@ -834,15 +794,14 @@ void MetalDriver::createDescriptorSetLayoutR(
labelStr = arg.c_str();
}
}, info.label);
std::sort(info.descriptors.begin(), info.descriptors.end(),
std::sort(info.bindings.begin(), info.bindings.end(),
[](const auto& a, const auto& b) { return a.binding < b.binding; });
DEBUG_LOG("createDescriptorSetLayoutR(dslh = %d, info = { label = %s,\n", dslh.getId(),
labelStr);
for (size_t i = 0; i < info.descriptors.size(); i++) {
for (size_t i = 0; i < info.bindings.size(); i++) {
DEBUG_LOG(" {binding = %d, type = %s, count = %d, stage = %s, flags = %s},\n",
info.descriptors[i].binding, toString(info.descriptors[i].type),
info.descriptors[i].count, toString(info.descriptors[i].stageFlags),
toString(info.descriptors[i].flags));
info.bindings[i].binding, toString(info.bindings[i].type), info.bindings[i].count,
toString(info.bindings[i].stageFlags), toString(info.bindings[i].flags));
}
DEBUG_LOG("})\n");
#endif
@@ -1814,12 +1773,6 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
}];
}
void MetalDriver::readTexture(Handle<HwTexture> src, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
// TODO: implement readTexture
scheduleDestroy(std::move(p));
}
void MetalDriver::readBufferSubData(backend::BufferObjectHandle boh,
uint32_t offset, uint32_t size, backend::BufferDescriptor&& p) {
// TODO: implement readBufferSubData
+1 -1
View File
@@ -1663,7 +1663,7 @@ id<MTLBuffer> MetalDescriptorSet::finalizeAndGetBuffer(MetalDriver* driver, Shad
case DescriptorType::SAMPLER_EXTERNAL: {
auto found = textures.find(binding.binding);
if (found == textures.end()) {
[encoder setTexture:getOrCreateEmptyTexture(driver->mContext) atIndex:binding.binding * 2];
[encoder setTexture:driver->mContext->emptyTexture atIndex:binding.binding * 2];
id<MTLSamplerState> sampler =
driver->mContext->samplerStateCache.getOrCreateState({});
[encoder setSamplerState:sampler atIndex:binding.binding * 2 + 1];
-5
View File
@@ -379,11 +379,6 @@ void NoopDriver::readPixels(Handle<HwRenderTarget> src,
scheduleDestroy(std::move(p));
}
void NoopDriver::readTexture(Handle<HwTexture> src, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
scheduleDestroy(std::move(p));
}
void NoopDriver::readBufferSubData(BufferObjectHandle boh,
uint32_t offset, uint32_t size, BufferDescriptor&& p) {
scheduleDestroy(std::move(p));
@@ -23,7 +23,6 @@
#include <private/utils/Tracing.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Logger.h>
namespace filament::backend {
@@ -48,9 +47,7 @@ GLuint OpenGLBlobCache::retrieve(BlobCacheKey* outKey, Platform& platform,
GLuint programId = 0;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
// TODO(exv): Don't copy these spec constants!
BlobCacheKey::SpecializationConstants constants(program.getSpecializationConstants());
BlobCacheKey key{ program.getCacheId(), std::move(constants) };
BlobCacheKey key{ program.getCacheId(), program.getSpecializationConstants() };
// FIXME: use a static buffer to avoid systematic allocation
// always attempt with 64 KiB
+12 -88
View File
@@ -2161,7 +2161,7 @@ void OpenGLDriver::createDescriptorSetR(Handle<HwDescriptorSet> dsh,
DEBUG_MARKER()
GLDescriptorSetLayout const* dsl = handle_cast<GLDescriptorSetLayout*>(dslh);
construct<GLDescriptorSet>(dsh, mContext, dslh, dsl);
mHandleAllocator.associateTagToHandle(dsh.getId(), std::move(tag));
mHandleAllocator.associateTagToHandle(dslh.getId(), std::move(tag));
}
void OpenGLDriver::mapBufferR(MemoryMappedBufferHandle mmbh,
@@ -3985,8 +3985,10 @@ void OpenGLDriver::stopCapture(int) {
// Read-back ops
// ------------------------------------------------------------------------------------------------
void OpenGLDriver::readPixelsFromBoundFramebuffer(uint32_t const x, uint32_t const y,
uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
void OpenGLDriver::readPixels(Handle<HwRenderTarget> src,
uint32_t const x, uint32_t const y, uint32_t width, uint32_t height,
PixelBufferDescriptor&& p) {
DEBUG_MARKER()
auto& gl = mContext;
GLenum const glFormat = getFormat(p.format);
@@ -4019,6 +4021,8 @@ void OpenGLDriver::readPixelsFromBoundFramebuffer(uint32_t const x, uint32_t con
* of the buffer.
*/
GLRenderTarget const* s = handle_cast<GLRenderTarget const*>(src);
using PBD = PixelBufferDescriptor;
// The PBO only needs to accommodate the area we're reading, with alignment.
@@ -4028,6 +4032,7 @@ void OpenGLDriver::readPixelsFromBoundFramebuffer(uint32_t const x, uint32_t con
if (UTILS_UNLIKELY(gl.isES2())) {
void* buffer = malloc(pboSize);
if (buffer) {
gl.bindFramebuffer(GL_FRAMEBUFFER, s->gl.fbo_read ? s->gl.fbo_read : s->gl.fbo);
glReadPixels(GLint(x), GLint(y), GLint(width), GLint(height), glFormat, glType, buffer);
CHECK_GL_ERROR()
@@ -4051,6 +4056,10 @@ void OpenGLDriver::readPixelsFromBoundFramebuffer(uint32_t const x, uint32_t con
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
// glReadPixel doesn't resolve automatically, but it does with the auto-resolve extension,
// which we're always emulating. So if we have a resolved fbo (fbo_read), use that instead.
gl.bindFramebuffer(GL_READ_FRAMEBUFFER, s->gl.fbo_read ? s->gl.fbo_read : s->gl.fbo);
GLuint pbo;
glGenBuffers(1, &pbo);
gl.bindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
@@ -4102,85 +4111,6 @@ void OpenGLDriver::readPixelsFromBoundFramebuffer(uint32_t const x, uint32_t con
#endif
}
void OpenGLDriver::readPixels(Handle<HwRenderTarget> src, uint32_t const x, uint32_t const y,
uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
DEBUG_MARKER()
auto& gl = mContext;
GLRenderTarget const* s = handle_cast<GLRenderTarget const*>(src);
if (UTILS_UNLIKELY(gl.isES2())) {
gl.bindFramebuffer(GL_FRAMEBUFFER, s->gl.fbo_read ? s->gl.fbo_read : s->gl.fbo);
readPixelsFromBoundFramebuffer(x, y, width, height, std::move(p));
return;
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
// glReadPixel doesn't resolve automatically, but it does with the auto-resolve extension,
// which we're always emulating. So if we have a resolved fbo (fbo_read), use that instead.
gl.bindFramebuffer(GL_READ_FRAMEBUFFER, s->gl.fbo_read ? s->gl.fbo_read : s->gl.fbo);
readPixelsFromBoundFramebuffer(x, y, width, height, std::move(p));
#endif
}
void OpenGLDriver::readTexture(Handle<HwTexture> src, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
DEBUG_MARKER()
auto& gl = mContext;
// readTexture() requires GLES 3.0+ features such as GL_READ_FRAMEBUFFER and
// glFramebufferTextureLayer, so we wrap it in FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2.
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (UTILS_UNLIKELY(gl.isES2())) {
// This is not supported on ES2 (at least not in this driver)
scheduleDestroy(std::move(p));
return;
}
GLTexture const* s = handle_cast<GLTexture*>(src);
assert_invariant(s);
assert_invariant(s->target != SamplerType::SAMPLER_3D);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
gl.bindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
GLenum const attachment = GL_COLOR_ATTACHMENT0;
switch (s->target) {
case SamplerType::SAMPLER_2D:
if (any(s->usage & TextureUsage::SAMPLEABLE)) {
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, attachment, GL_TEXTURE_2D, s->gl.id,
level);
} else {
glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, attachment, GL_RENDERBUFFER,
s->gl.id);
}
break;
case SamplerType::SAMPLER_CUBEMAP:
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, attachment,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer, s->gl.id, level);
break;
case SamplerType::SAMPLER_2D_ARRAY:
case SamplerType::SAMPLER_CUBEMAP_ARRAY:
glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, attachment, s->gl.id, level, layer);
break;
default:
// This should not happen (e.g. 3D textures are excluded)
break;
}
CHECK_GL_FRAMEBUFFER_STATUS(GL_READ_FRAMEBUFFER)
readPixelsFromBoundFramebuffer(x, y, width, height, std::move(p));
gl.unbindFramebuffer(GL_READ_FRAMEBUFFER);
glDeleteFramebuffers(1, &fbo);
#else
scheduleDestroy(std::move(p));
#endif
}
void OpenGLDriver::readBufferSubData(BufferObjectHandle boh,
uint32_t const offset, uint32_t size, BufferDescriptor&& p) {
UTILS_UNUSED_IN_RELEASE auto& gl = mContext;
@@ -4755,9 +4685,6 @@ void OpenGLDriver::bindDescriptorSet(
if (UTILS_UNLIKELY(!dsh)) {
mBoundDescriptorSets[set].dsh = dsh;
#ifndef NDEBUG
mBoundDescriptorSets[set].tag = "null";
#endif
mInvalidDescriptorSetBindings.set(set, true);
mInvalidDescriptorSetBindingOffsets.set(set, true);
return;
@@ -4779,9 +4706,6 @@ void OpenGLDriver::bindDescriptorSet(
// `offsets` data's lifetime will end when this function returns. We have to make a copy.
// (the data is allocated inside the CommandStream)
mBoundDescriptorSets[set].dsh = dsh;
#ifndef NDEBUG
mBoundDescriptorSets[set].tag = mHandleAllocator.getHandleTag(dsh.getId());
#endif
assert_invariant(offsets.data() != nullptr || ds->getDynamicBufferCount() == 0);
std::copy_n(offsets.data(), ds->getDynamicBufferCount(),
mBoundDescriptorSets[set].offsets.data());
@@ -363,8 +363,6 @@ private:
bool isDefaultFramebuffer) noexcept;
// Common methods
void readPixelsFromBoundFramebuffer(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
PixelBufferDescriptor&& p);
void createTextureCommon(Handle<HwTexture> th, SamplerType target, uint8_t levels,
TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, uint32_t depth,
TextureUsage usage, utils::ImmutableCString&& tag);
@@ -409,9 +407,6 @@ private:
struct {
DescriptorSetHandle dsh;
std::array<uint32_t, CONFIG_UNIFORM_BINDING_COUNT> offsets;
#ifndef NDEBUG
utils::ImmutableCString tag;
#endif
} mBoundDescriptorSets[MAX_DESCRIPTOR_SET_COUNT] = {};
void clearWithRasterPipe(TargetBufferFlags clearFlags,
@@ -120,33 +120,27 @@ bool PlatformEGL::isOpenGL() const noexcept {
PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default;
Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) {
static constexpr int kMaxNumEGLDevices = 32;
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
EGLint major, minor;
EGLBoolean initialized = false;
EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor);
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
PFNEGLGETPLATFORMDISPLAYEXTPROC const getPlatformDisplay =
PFNEGLGETPLATFORMDISPLAYEXTPROC(eglGetProcAddress("eglGetPlatformDisplay"));
if (eglQueryDevicesEXT != nullptr && getPlatformDisplay != nullptr) {
EGLint numDevices = 0;
EGLDeviceEXT eglDevices[kMaxNumEGLDevices];
if (eglQueryDevicesEXT(kMaxNumEGLDevices, eglDevices, &numDevices)) {
for (int i = 0; i < numDevices && !initialized; ++i) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevices[i], nullptr);
if (!initialized) {
EGLDeviceEXT eglDevice;
EGLint numDevices;
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
if (eglQueryDevicesEXT != nullptr) {
eglQueryDevicesEXT(1, &eglDevice, &numDevices);
if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplay"))) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
}
}
if (!initialized) {
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
if (UTILS_UNLIKELY(!initialized)) {
LOG(ERROR) << "eglInitialize failed";
return nullptr;
@@ -31,19 +31,22 @@ namespace filament::backend {
VulkanBufferProxy::VulkanBufferProxy(VulkanContext const& context, VmaAllocator allocator,
VulkanStagePool& stagePool, VulkanBufferCache& bufferCache, VulkanBufferBinding binding,
BufferUsage usage, uint32_t numBytes)
: mStagingBufferBypassEnabled(context.stagingBufferBypassEnabled()),
mAllocator(allocator),
mStagePool(stagePool),
mBufferCache(bufferCache),
mBuffer(mBufferCache.acquire(binding, numBytes)),
mLastReadAge(0),
mUsage(usage) {}
: mStagingBufferBypassEnabled(context.stagingBufferBypassEnabled()),
mAllocator(allocator),
mStagePool(stagePool),
mBufferCache(bufferCache),
mBuffer(mBufferCache.acquire(binding, numBytes)),
mLastReadAge(0),
mUsage(usage) {}
void VulkanBufferProxy::loadFromCpu(VulkanCommandBuffer& commands, const void* cpuData,
uint32_t byteOffset, uint32_t numBytes) {
// This means that the buffer is not currently in use by the GPU
bool const isAvailable = mBuffer->getCount() == 1;
// This means that we're recording a write into a command buffer without a previous read, so it
// should be safe to
// 1) Do a direct memcpy in UMA mode
// 2) Skip adding a barrier (to protect the write from writing over a read).
bool const isAvailable = commands.age() != mLastReadAge;
// Keep track of the VulkanBuffer usage
commands.acquire(mBuffer);
@@ -78,13 +81,9 @@ void VulkanBufferProxy::loadFromCpu(VulkanCommandBuffer& commands, const void* c
memcpy(stage->mapping(), cpuData, numBytes);
vmaFlushAllocation(mAllocator, stage->memory(), stage->offset(), numBytes);
// This means that we're recording a write into a command buffer with a previous read, so it
// needs to add a barrier (to protect the write from writing over a read).
bool const issueBarrier = commands.age() == mLastReadAge;
// If there was a previous read, then we need to make sure the following write is properly
// synced with the previous read.
if (issueBarrier) {
if (!isAvailable) {
VkAccessFlags srcAccess = 0;
VkPipelineStageFlags srcStage = 0;
if (getBinding() == VulkanBufferBinding::UNIFORM) {
@@ -133,7 +132,7 @@ void VulkanBufferProxy::loadFromCpu(VulkanCommandBuffer& commands, const void* c
dstAccessMask |= VK_ACCESS_INDEX_READ_BIT;
dstStageMask |= VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;
} else if (getBinding() == VulkanBufferBinding::UNIFORM) {
dstAccessMask |= VK_ACCESS_UNIFORM_READ_BIT;
dstAccessMask |= VK_ACCESS_SHADER_READ_BIT;
dstStageMask |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
} else if (getBinding() == VulkanBufferBinding::SHADER_STORAGE) {
// TODO: implement me
@@ -62,7 +62,7 @@ uint32_t appendBindings(VkDescriptorSetLayoutBinding* toBind, VkDescriptorType t
uint32_t appendSamplerBindings(VkDescriptorSetLayoutBinding* toBind,
fvkutils::SamplerBitmask const& mask, fvkutils::SamplerBitmask const& external,
utils::FixedCapacityVector<std::pair<uint64_t, VkSampler>> const& immutableSamplers) {
utils::FixedCapacityVector<VkSampler> const& immutableSamplers) {
using Bitmask = fvkutils::SamplerBitmask;
uint32_t count = 0;
Bitmask alreadySeen;
@@ -92,7 +92,7 @@ uint32_t appendSamplerBindings(VkDescriptorSetLayoutBinding* toBind,
.descriptorCount = 1,
.stageFlags = stages,
.pImmutableSamplers = external[index] && immutableSamplerCount > immutableIndex
? &(immutableSamplers[immutableIndex++].second)
? &immutableSamplers[immutableIndex++]
: nullptr,
};
}
@@ -100,14 +100,14 @@ uint32_t appendSamplerBindings(VkDescriptorSetLayoutBinding* toBind,
return count;
}
uint64_t computeImmutableSamplerHash(
utils::FixedCapacityVector<std::pair<uint64_t, VkSampler>> const& samplers) {
uint64_t computeImmutableSamplerHash(utils::FixedCapacityVector<VkSampler> const& samplers) {
size_t const size = samplers.size();
if (size == 0) {
return 0;
} else if (size == 1) {
return (uint64_t) samplers[0];
}
//64bit + 64bit per element = 4 words
return utils::hash::murmur3((uint32_t*) samplers.data(), samplers.size() * 4, 0);
return utils::hash::murmur3((uint32_t*) samplers.data(), samplers.size() * 2, 0);
}
} // anonymous namespace
@@ -128,7 +128,7 @@ void VulkanDescriptorSetLayoutCache::terminate() noexcept {
VkDescriptorSetLayout VulkanDescriptorSetLayoutCache::getVkLayout(
VulkanDescriptorSetLayout::Bitmask const& bitmasks,
fvkutils::SamplerBitmask externalSamplers,
utils::FixedCapacityVector<std::pair<uint64_t, VkSampler>> immutableSamplers) {
utils::FixedCapacityVector<VkSampler> immutableSamplers) {
LayoutKey key = {
.bitmask = bitmasks,
.immutableSamplerHash = computeImmutableSamplerHash(immutableSamplers),
@@ -47,7 +47,7 @@ public:
// This method is meant to be used with external samplers
VkDescriptorSetLayout getVkLayout(VulkanDescriptorSetLayout::Bitmask const& bitmasks,
fvkutils::SamplerBitmask externalSamplers,
utils::FixedCapacityVector<std::pair<uint64_t, VkSampler>> immutableSamplers = {});
utils::FixedCapacityVector<VkSampler> immutableSamplers = {});
private:
VkDevice mDevice;

Some files were not shown because too many files have changed in this diff Show More