This is an automated email from the ASF dual-hosted git repository.
jsorel pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 7c3fba2ec4 feat(Geometry): add scene model as geometry aggregation tree
7c3fba2ec4 is described below
commit 7c3fba2ec4a678eb561d1793be017829975fe9dc
Author: jsorel <[email protected]>
AuthorDate: Wed Feb 25 14:06:18 2026 +0100
feat(Geometry): add scene model as geometry aggregation tree
---
.../org/apache/sis/geometries/scene/Camera.java | 242 +++++++++
.../org/apache/sis/geometries/scene/Material.java | 597 +++++++++++++++++++++
.../org/apache/sis/geometries/scene/Model.java | 216 ++++++++
.../org/apache/sis/geometries/scene/Sampler.java | 171 ++++++
.../org/apache/sis/geometries/scene/SceneNode.java | 480 +++++++++++++++++
.../org/apache/sis/geometries/scene/Surface.java | 107 ++++
.../org/apache/sis/geometries/scene/Texture.java | 142 +++++
.../apache/sis/geometries/scene/package-info.java | 27 +
8 files changed, 1982 insertions(+)
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Camera.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Camera.java
new file mode 100644
index 0000000000..449083e2d6
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Camera.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.util.Objects;
+
+/**
+ * Base class for Camera types.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public abstract sealed class Camera permits Camera.Orthographic,
Camera.Perspective {
+
+ Camera(){
+
+ }
+
+ /**
+ * Orthographic camera definition.
+ */
+ public static final class Orthographic extends Camera {
+
+ private double xmag;
+ private double ymag;
+ private double zfar;
+ private double znear;
+
+ /**
+ * @return The floating-point horizontal magnification of the view.Must
not be zero.
+ */
+ public double getXmag() {
+ return xmag;
+ }
+
+ /**
+ * @param xmag The floating-point horizontal magnification of the
view.Must not be zero.
+ */
+ public void setXmag(double xmag) {
+ this.xmag = xmag;
+ }
+
+ /**
+ * @return The floating-point vertical magnification of the view.Must
not be zero.
+ */
+ public double getYmag() {
+ return ymag;
+ }
+
+ /**
+ * @param ymag The floating-point vertical magnification of the
view.Must not be zero.
+ */
+ public void setYmag(double ymag) {
+ this.ymag = ymag;
+ }
+
+ /**
+ * @return The floating-point distance to the far clipping plane.`zfar`
must be greater than `znear`.
+ */
+ public double getZfar() {
+ return zfar;
+ }
+
+ /**
+ * @param zfar The floating-point distance to the far clipping
plane.`zfar` must be greater than `znear`.
+ */
+ public void setZfar(double zfar) {
+ this.zfar = zfar;
+ }
+
+ /**
+ * @return The floating-point distance to the near clipping plane.
+ */
+ public double getZnear() {
+ return znear;
+ }
+
+ /**
+ * @param znear The floating-point distance to the near clipping plane.
+ */
+ public void setZnear(double znear) {
+ this.znear = znear;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 53 * hash + (int) (Double.doubleToLongBits(this.xmag) ^
(Double.doubleToLongBits(this.xmag) >>> 32));
+ hash = 53 * hash + (int) (Double.doubleToLongBits(this.ymag) ^
(Double.doubleToLongBits(this.ymag) >>> 32));
+ hash = 53 * hash + (int) (Double.doubleToLongBits(this.zfar) ^
(Double.doubleToLongBits(this.zfar) >>> 32));
+ hash = 53 * hash + (int) (Double.doubleToLongBits(this.znear) ^
(Double.doubleToLongBits(this.znear) >>> 32));
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Orthographic other = (Orthographic) obj;
+ if (Double.doubleToLongBits(this.xmag) !=
Double.doubleToLongBits(other.xmag)) {
+ return false;
+ }
+ if (Double.doubleToLongBits(this.ymag) !=
Double.doubleToLongBits(other.ymag)) {
+ return false;
+ }
+ if (Double.doubleToLongBits(this.zfar) !=
Double.doubleToLongBits(other.zfar)) {
+ return false;
+ }
+ if (Double.doubleToLongBits(this.znear) !=
Double.doubleToLongBits(other.znear)) {
+ return false;
+ }
+ return true;
+ }
+
+ }
+
+ /**
+ * Perspective camera definition.
+ */
+ public static final class Perspective extends Camera {
+
+ private Double aspectRatio;
+ private double yfov;
+ private Double zfar;
+ private double znear;
+
+ /**
+ * @return The floating-point aspect ratio of the field of view.
+ */
+ public Double getAspectRatio() {
+ return aspectRatio;
+ }
+
+ /**
+ * @param aspectRatio The floating-point aspect ratio of the field of
view.
+ */
+ public void setAspectRatio(Double aspectRatio) {
+ this.aspectRatio = aspectRatio;
+ }
+
+ /**
+ * @return The floating-point vertical field of view in
radians.(Required)
+ */
+ public double getYfov() {
+ return yfov;
+ }
+
+ /**
+ * @param yfov The floating-point vertical field of view in
radians.(Required)
+ */
+ public void setYfov(double yfov) {
+ this.yfov = yfov;
+ }
+
+ /**
+ * @return The floating-point distance to the far clipping plane.
+ */
+ public Double getZfar() {
+ return zfar;
+ }
+
+ /**
+ * @param zfar The floating-point distance to the far clipping plane.
+ */
+ public void setZfar(Double zfar) {
+ this.zfar = zfar;
+ }
+
+ /**
+ * @return The floating-point distance to the near clipping
plane.(Required)
+ */
+ public double getZnear() {
+ return znear;
+ }
+
+ /**
+ * @param znear The floating-point distance to the near clipping
plane.(Required)
+ */
+ public void setZnear(double znear) {
+ this.znear = znear;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 71 * hash + Objects.hashCode(this.aspectRatio);
+ hash = 71 * hash + (int) (Double.doubleToLongBits(this.yfov) ^
(Double.doubleToLongBits(this.yfov) >>> 32));
+ hash = 71 * hash + Objects.hashCode(this.zfar);
+ hash = 71 * hash + (int) (Double.doubleToLongBits(this.znear) ^
(Double.doubleToLongBits(this.znear) >>> 32));
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Perspective other = (Perspective) obj;
+ if (Double.doubleToLongBits(this.yfov) !=
Double.doubleToLongBits(other.yfov)) {
+ return false;
+ }
+ if (Double.doubleToLongBits(this.znear) !=
Double.doubleToLongBits(other.znear)) {
+ return false;
+ }
+ if (!Objects.equals(this.aspectRatio, other.aspectRatio)) {
+ return false;
+ }
+ if (!Objects.equals(this.zfar, other.zfar)) {
+ return false;
+ }
+ return true;
+ }
+
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Material.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Material.java
new file mode 100644
index 0000000000..257f22268c
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Material.java
@@ -0,0 +1,597 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+
+/**
+ * A material is a set of properties defining a visual representation.
+ * Materials on there own do not suffice to obtain the visual aspect
+ * of a model, they must be combined with a rendering technique.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class Material extends HashMap<String,Object> {
+
+ public static final String ALPHA_MODE_OPAQUE = "OPAQUE";
+ public static final String ALPHA_MODE_MASK = "MASK";
+ public static final String ALPHA_MODE_BLEND = "BLEND";
+
+ // PBR rendering technique
/////////////////////////////////////////////////
+
+ /** Boolean, default is false */
+ public static final String DOUBLESIDED = "doubleSided";
+ /** Double, The alpha cutoff value of the material. default is 0.5 */
+ public static final String ALPHACUTOFF = "alphaCutoff";
+ /** String alpha mode. default is opaque*/
+ public static final String ALPHAMODE = "alphaMode";
+ /** Color */
+ public static final String EMISSIVEFACTOR = "emissiveFactor";
+ /** Texture */
+ public static final String EMISSIVETEXTURE = "emissiveTexture";
+ /** Texture */
+ public static final String OCCLUSIONTEXTURE = "occlusionTexture";
+ public static final String OCCLUSIONSTRENGTH = "occlusionStrength";
+ /** Texture */
+ public static final String NORMALTEXTURE = "normalTexture";
+ public static final String NORMALSCALE = "normalScale";
+
+
+ /** Color */
+ public static final String PBR_BASECOLORFACTOR = "baseColorFactor";
+ /** Texture */
+ public static final String PBR_BASECOLORTEXTURE = "baseColorTexture";
+ /** Number */
+ public static final String PBR_METALLICFACTOR = "metallicFactor";
+ /** Number */
+ public static final String PBR_ROUGHNESSFACTOR = "roughnessFactor";
+ /** Texture */
+ public static final String PBR_METALLICROUGHNESSTEXTURE =
"metallicRoughnessTexture";
+
+ // PBR rendering technique
/////////////////////////////////////////////////
+
+ /** Color */
+ public static final String PBRSG_DIFFUSEFACTOR = "diffuseFactor";
+ /** Texture */
+ public static final String PBRSG_DIFFUSETEXTURE = "diffuseTexture";
+ /** Color */
+ public static final String PBRSG_SPECULARFACTOR = "specularFactor";
+ /** Scalar */
+ public static final String PBRSG_GLOSSINESSFACTOR = "glossinessFactor";
+ /** Texture */
+ public static final String PBRSG_SPECULAR_GLOSSINESS_TEXTURE =
"specularGlossinessTexture";
+
+
+ // Blinn-Phong rendering technique
/////////////////////////////////////////
+
+ /** Color */
+ public static final String BP_AMBIANTFACTOR = "ambiantFactor";
+ /** Texture */
+ public static final String BP_AMBIANTTEXTURE = "ambiantTexture";
+ /** Color */
+ public static final String BP_DIFFUSEFACTOR = "diffuseFactor";
+ /** Texture */
+ public static final String BP_DIFFUSETEXTURE = "diffuseTexture";
+ /** Color */
+ public static final String BP_SPECULARFACTOR = "specularFactor";
+ /** Texture */
+ public static final String BP_SPECULARTEXTURE = "specularTexture";
+
+ // Unlit rendering technique /////////////////////////////////////////
+ // See
https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_unlit/README.md
+
+ /** Boolean */
+ public static final String LIGHTS_UNLIT = "unlit";
+
+ // See
https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_ior/README.md
+ /** Double */
+ public static final String IOR = "ior";
+
+ // See
https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_transmission/README.md
+ /** Double */
+ public static final String TRANSMISSIONFACTOR = "transmissionFactor";
+ /** Texture */
+ public static final String TRANSMISSIONTEXTURE = "transmissionTexture";
+
+ // See
https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume/README.md
+ /** Double */
+ public static final String THICKNESSFACTOR = "thicknessFactor";
+ /** Texture */
+ public static final String THICKNESSTEXTURE = "thicknessTexture";
+ /** Double */
+ public static final String ATTENUATIONDISTANCE = "attenuationDistance";
+ /** Color */
+ public static final String ATTENUATIONCOLOR = "attenuationColor";
+
+
+ private String id;
+
+ public Material() {
+
+ }
+
+ /**
+ * @return material identifier
+ */
+ public String getIdentifier() {
+ return id;
+ }
+
+ /**
+ * @param id set material identifier
+ */
+ public void setIdentifier(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Default is OPAQUE
+ * @return alphaMode
+ */
+ public String getAlphaMode() {
+ String str = (String) get(ALPHAMODE);
+ return (str == null) ? ALPHA_MODE_OPAQUE : str;
+ }
+
+ /**
+ * Default is OPAQUE
+ * @param alphaMode
+ */
+ public void setAlphaMode(String alphaMode) {
+ put(ALPHAMODE, alphaMode);
+ }
+
+ /**
+ * Default value is 0.5
+ * @return alpha cutoff
+ */
+ public double getAlphaCutoff() {
+ Number ac = (Number) get(ALPHACUTOFF);
+ return (ac == null) ? 0.5 : ac.doubleValue();
+ }
+
+ /**
+ * Default value is 0.5
+ * @param cutoff
+ */
+ public void setAlphaCutoff(double cutoff) {
+ ArgumentChecks.ensureBetween("alpha cutoff", 0.0, 1.0, cutoff);
+ put(ALPHACUTOFF, cutoff);
+ }
+
+ /**
+ * Default value is false
+ * @return double sided.
+ */
+ public boolean isDoubleSided() {
+ return Boolean.TRUE.equals(get(DOUBLESIDED));
+ }
+
+ /**
+ * Default value is false
+ * @param doublesided
+ */
+ public void setDoubleSided(boolean doublesided) {
+ put(DOUBLESIDED, doublesided);
+ }
+
+ /**
+ * Default is BLACK (0,0,0)
+ * @return emmisive factor
+ */
+ public Color getEmissiveFactor() {
+ Color color = (Color) get(EMISSIVEFACTOR);
+ return (color == null) ? Color.BLACK : color;
+ }
+
+ /**
+ * Default is BLACK (0,0,0)
+ * @param color emmisive factor
+ */
+ public void setEmissiveFactor(Color color) {
+ put(EMISSIVEFACTOR, color);
+ }
+
+ public Texture getEmissiveTexture() {
+ return (Texture) get(EMISSIVETEXTURE);
+ }
+
+ public void setEmissiveTexture(Texture texture) {
+ put(EMISSIVETEXTURE, texture);
+ }
+
+ public Texture getOcclusionTexture() {
+ return (Texture) get(OCCLUSIONTEXTURE);
+ }
+
+ public void setOcclusionTexture(Texture texture) {
+ put(OCCLUSIONTEXTURE, texture);
+ }
+
+ /**
+ * Default is 1.0
+ * @return occlusion strength
+ */
+ public double getOcclusionStrength() {
+ Number strength = (Number) get(OCCLUSIONSTRENGTH);
+ return (strength == null) ? 1.0 : strength.doubleValue();
+ }
+
+ /**
+ * Default is 1.0
+ * @param strength occlusion strength between 0.0 and 1.0
+ */
+ public void setOcclusionStrength(double strength) {
+ ArgumentChecks.ensureBetween("occlusion strength", 0.0, 1.0, strength);
+ put(OCCLUSIONSTRENGTH, strength);
+ }
+
+ public Texture getNormalTexture() {
+ return (Texture) get(NORMALTEXTURE);
+ }
+
+ public void setNormalTexture(Texture texture) {
+ put(NORMALTEXTURE, texture);
+ }
+
+ /**
+ * Default is 1.0
+ * @return normal scale
+ */
+ public double getNormalScale() {
+ Number scale = (Number) get(NORMALSCALE);
+ return (scale == null) ? 1.0 : scale.doubleValue();
+ }
+
+ /**
+ * Default is 1.0
+ * @param scale normal scale
+ */
+ public void setNormalScale(double scale) {
+ ArgumentChecks.ensureBetween("normal scale", 0.0, 1.0, scale);
+ put(NORMALSCALE, scale);
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @return pbr base color factor
+ */
+ public Color getPBRBaseColorFactor() {
+ Color color = (Color) get(PBR_BASECOLORFACTOR);
+ return (color == null) ? Color.WHITE : color;
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @param color pbr base color factor
+ */
+ public void setPBRBaseColorFactor(Color color) {
+ put(PBR_BASECOLORFACTOR, color);
+ }
+
+ public Texture getPBRBaseColorTexture() {
+ return (Texture) get(PBR_BASECOLORTEXTURE);
+ }
+
+ public void setPBRBaseColorTexture(Texture texture) {
+ put(PBR_BASECOLORTEXTURE, texture);
+ }
+
+ /**
+ * Default is 1.0
+ * @return between 0.0 and 1.0
+ */
+ public double getPBRMetallicFactor() {
+ Number factor = (Number) get(PBR_METALLICFACTOR);
+ return (factor == null) ? 1.0 : factor.doubleValue();
+ }
+
+ /**
+ * Default is 1.0
+ * @param factor between 0.0 and 1.0
+ */
+ public void setPBRMetallicFactor(double factor) {
+ ArgumentChecks.ensureBetween("pbr metallic factor", 0.0, 1.0, factor);
+ put(PBR_METALLICFACTOR, factor);
+ }
+
+ /**
+ * Default is 1.0
+ * @return between 0.0 and 1.0
+ */
+ public double getPBRRoughnessFactor() {
+ Number roughness = (Number) get(PBR_ROUGHNESSFACTOR);
+ return (roughness == null) ? 1.0 : roughness.doubleValue();
+ }
+
+ /**
+ * Default is 1.0
+ * @param roughness between 0.0 and 1.0
+ */
+ public void setPBRRoughnessFactor(double roughness) {
+ ArgumentChecks.ensureBetween("PBR roughness factor", 0.0, 1.0,
roughness);
+ put(PBR_ROUGHNESSFACTOR, roughness);
+ }
+
+ public Texture getPBRMetallicRoughnessTexture() {
+ return (Texture) get(PBR_METALLICROUGHNESSTEXTURE);
+ }
+
+ public void setPBRMetallicRoughnessTexture(Texture texture) {
+ put(PBR_METALLICROUGHNESSTEXTURE, texture);
+ }
+
+ /**
+ * Default is WHITE (1,1,1,1)
+ * @return diffuse factor
+ */
+ public Color getPBRSGDiffuseFactor() {
+ Color color = (Color) get(PBRSG_DIFFUSEFACTOR);
+ return (color == null) ? Color.WHITE : color;
+ }
+
+ /**
+ * Default is WHITE (1,1,1,1)
+ * @param color diffuse factor
+ */
+ public void setPBRSGDiffuseFactor(Color color) {
+ put(PBRSG_DIFFUSEFACTOR, color);
+ }
+
+ /**
+ * Default is null
+ * @return diffuse texture
+ */
+ public Texture getPBRSGDiffuseTexture() {
+ return (Texture) get(PBRSG_DIFFUSETEXTURE);
+ }
+
+ /**
+ * Default is null
+ * @param texture diffuse
+ */
+ public void setPBRSGDiffuseTexture(Texture texture) {
+ put(PBRSG_DIFFUSETEXTURE, texture);
+ }
+
+ /**
+ * Default is 1.0
+ * @return between 0.0 and 1.0
+ */
+ public double getPBRSGGlossinessFactor() {
+ Number factor = (Number) get(PBRSG_GLOSSINESSFACTOR);
+ return (factor == null) ? 1.0 : factor.doubleValue();
+ }
+
+ /**
+ * Default is 1.0
+ * @param factor between 0.0 and 1.0
+ */
+ public void setPBRSGGlossinessFactor(double factor) {
+ ArgumentChecks.ensureBetween("pbrsg glossiness factor", 0.0, 1.0,
factor);
+ put(PBRSG_GLOSSINESSFACTOR, factor);
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @return specular factor
+ */
+ public Color getPBRSGSpecularFactor() {
+ Color color = (Color) get(PBRSG_SPECULARFACTOR);
+ return (color == null) ? Color.WHITE : color;
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @param color specular factor
+ */
+ public void setPBRSGSpecularFactor(Color color) {
+ put(PBRSG_SPECULARFACTOR, color);
+ }
+
+ /**
+ * Default is null
+ * @return specular glossiness texture
+ */
+ public Texture getPBRSGSpecularGlossinessTexture() {
+ return (Texture) get(PBRSG_SPECULAR_GLOSSINESS_TEXTURE);
+ }
+
+ /**
+ * Default is null
+ * @param texture specular glossiness
+ */
+ public void setPBRSGSpecularGlossinessTexture(Texture texture) {
+ put(PBRSG_SPECULAR_GLOSSINESS_TEXTURE, texture);
+ }
+
+ /**
+ * Default value is false
+ * @return true if lights should be disabled for this material.
+ */
+ public boolean isUnlit() {
+ return Boolean.TRUE.equals(get(LIGHTS_UNLIT));
+ }
+
+ /**
+ * Default value is false
+ * @param unlit if lights should be disabled for this material.
+ */
+ public void setUnlit(boolean unlit) {
+ put(LIGHTS_UNLIT, unlit);
+ }
+
+ /**
+ * Default value is 1.5
+ * @return index of refraction
+ */
+ public double getIOR() {
+ Number ac = (Number) get(IOR);
+ return (ac == null) ? 1.5 : ac.doubleValue();
+ }
+
+ /**
+ * Default value is 1.5
+ * @param ior
+ */
+ public void setIOR(double ior) {
+ ArgumentChecks.ensureBetween("ior", 1.0, 100.0, ior);
+ put(IOR, ior);
+ }
+
+ /**
+ * Default value is 0.0
+ * @return transmission factor
+ */
+ public double getTransmissionFactor() {
+ Number ac = (Number) get(TRANSMISSIONFACTOR);
+ return (ac == null) ? 0.0 : ac.doubleValue();
+ }
+
+ /**
+ * Default value is 0.0
+ * @param tf
+ */
+ public void setTransmissionFactor(double tf) {
+ put(TRANSMISSIONFACTOR, tf);
+ }
+
+ /**
+ * Default is null
+ * @return transmission texture
+ */
+ public Texture getTransmissionTexture() {
+ return (Texture) get(TRANSMISSIONTEXTURE);
+ }
+
+ /**
+ * Default is null
+ * @param texture transmission
+ */
+ public void setTransmissionTexture(Texture texture) {
+ put(TRANSMISSIONTEXTURE, texture);
+ }
+
+ /**
+ * Default value is 0.0
+ * @return thickness factor
+ */
+ public double getThicknessFactor() {
+ Number ac = (Number) get(THICKNESSFACTOR);
+ return (ac == null) ? 0.0 : ac.doubleValue();
+ }
+
+ /**
+ * Default value is 0.0
+ * @param tf
+ */
+ public void setThicknessFactor(double tf) {
+ put(THICKNESSFACTOR, tf);
+ }
+
+ /**
+ * Default is null
+ * @return thickness texture
+ */
+ public Texture getThicknessTexture() {
+ return (Texture) get(THICKNESSTEXTURE);
+ }
+
+ /**
+ * Default is null
+ * @param texture thickness
+ */
+ public void setThicknessTexture(Texture texture) {
+ put(THICKNESSTEXTURE, texture);
+ }
+
+ /**
+ * Default value is +Infinity
+ * @return volume attenuation distance
+ */
+ public double getAttenuationDistance() {
+ Number ac = (Number) get(ATTENUATIONDISTANCE);
+ return (ac == null) ? Double.POSITIVE_INFINITY : ac.doubleValue();
+ }
+
+ /**
+ * Default value is +Infinity
+ * @param ad volume attenuation distance
+ */
+ public void setAttenuationDistance(double ad) {
+ put(ATTENUATIONDISTANCE, ad);
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @return volume attenuation color
+ */
+ public Color getAttenuationColor() {
+ Color color = (Color) get(ATTENUATIONCOLOR);
+ return (color == null) ? Color.WHITE : color;
+ }
+
+ /**
+ * Default is WHITE (1,1,1)
+ * @param color volume attenuation color
+ */
+ public void setAttenuationColor(Color color) {
+ put(ATTENUATIONCOLOR, color);
+ }
+
+ public final class Matte extends Material {
+ //todo separate parameters to different type of materials
+ }
+
+ public final class BlinnPhong extends Material {
+ //todo separate parameters to different type of materials
+ }
+
+ public final class PhysicallyBased extends Material {
+ //todo separate parameters to different type of materials
+ }
+
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 83 * hash + Objects.hashCode(this.id);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Material other = (Material) obj;
+ if (!Objects.equals(this.id, other.id)) {
+ return false;
+ }
+ return super.equals(obj);
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Model.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Model.java
new file mode 100644
index 0000000000..4a1efeba06
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Model.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.sis.geometries.Geometries;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.mesh.MeshPrimitive;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * A 3D model is attached to a scene node with geometric and rendering
definitions.
+ *
+ * GLTF name this a Mesh.
+ * ANARI has no equivalent for it.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Model {
+
+ private CoordinateReferenceSystem crs;
+ private String name;
+ private final List<Double> morphWeights = new ArrayList<>();
+ private final List<Surface> components = new ArrayList<>();
+ //user properties
+ private Map<String,Object> properties;
+
+ /**
+ * Create a geometry with right hand 3D CRS.
+ */
+ public Model() {
+ this(Geometries.RIGHT_HAND_3D);
+ }
+
+ public Model(CoordinateReferenceSystem crs) {
+ this.crs = crs;
+ }
+
+ /**
+ * @param geometries must contain at least one geometry
+ */
+ public Model(Collection<? extends Geometry> geometries) {
+ this(geometries.toArray(MeshPrimitive[]::new));
+ }
+
+ /**
+ * @param geometries must contain at least one geometry
+ */
+ public Model(Geometry ... geometries) {
+ this(geometries[0].getCoordinateReferenceSystem());
+ for (Geometry p : geometries) {
+ this.components.add(new Surface(p));
+ }
+ }
+
+ /**
+ * @param geometries must contain at least one surface
+ */
+ public Model(Surface ... surfaces) {
+ this(surfaces[0].getGeometry().getCoordinateReferenceSystem());
+ for (Surface p : surfaces) {
+ this.components.add(p);
+ }
+ }
+
+ /**
+ * @return model name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name model name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return scene node coordinate system, never null.
+ */
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return crs;
+ }
+
+ /**
+ * This method will modify :
+ * - model surface crs if defined
+ * @param crs
+ */
+ public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) {
+ ArgumentChecks.ensureNonNull("crs", crs);
+ for (Surface surface : components) {
+ surface.getGeometry().setCoordinateReferenceSystem(crs);
+ }
+ this.crs = crs;
+ }
+
+ /**
+ *
+ * @return Model envelope, never null but can be set to NaN
+ */
+ public Envelope getEnvelope() {
+ GeneralEnvelope e = null;
+ for (Surface ms : components) {
+ GeneralEnvelope me = new
GeneralEnvelope(ms.getGeometry().getEnvelope());
+ //we ignore the model crs, often undefined
+ me.setCoordinateReferenceSystem(getCoordinateReferenceSystem());
+ if (!me.isAllNaN()) {
+ if (e == null) {
+ e = me;
+ } else {
+ e.add(me);
+ }
+ }
+ }
+ if (e == null) {
+ e = new GeneralEnvelope(getCoordinateReferenceSystem());
+ e.setToNaN();
+ }
+ return e;
+ }
+
+ /**
+ * @return Model morph target weights.
+ */
+ public List<Double> getMorphWeights() {
+ return morphWeights;
+ }
+
+ public List<Surface> getComponents() {
+ return components;
+ }
+
+ /**
+ * Set given material on all primitives.
+ * @param material
+ */
+ public void setMaterial(Material material) {
+ for (Surface p : getComponents()) {
+ p.setMaterial(material);
+ }
+ }
+
+ /**
+ * Map of properties for user needs.
+ * Those informations may be lost in model processes.
+ *
+ * @return Map, never null.
+ */
+ public synchronized Map<String, Object> userProperties() {
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+ return properties;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 59 * hash + Objects.hashCode(this.name);
+ hash = 59 * hash + Objects.hashCode(this.morphWeights);
+ return hash + super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Model other = (Model) obj;
+ if (!Objects.equals(this.name, other.name)) {
+ return false;
+ }
+ if (!Objects.equals(this.morphWeights, other.morphWeights)) {
+ return false;
+ }
+ if (!Objects.equals(this.components, other.components)) {
+ return false;
+ }
+ if (!Objects.equals(this.crs, other.crs)) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Sampler.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Sampler.java
new file mode 100644
index 0000000000..ad05ac0cb7
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Sampler.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Sampler {
+
+ public static enum Wrap {
+ CLAMP_TO_EDGE,
+ MIRRORED_REPEAT,
+ REPEAT
+ }
+
+ public static enum MagFilter {
+ /** system defined */
+ AUTO,
+ NEAREST,
+ LINEAR,
+ }
+
+ public static enum MinFilter {
+ /** system defined */
+ AUTO,
+ NEAREST,
+ LINEAR,
+ NEAREST_MIPMAP_NEAREST,
+ LINEAR_MIPMAP_NEAREST,
+ NEAREST_MIPMAP_LINEAR,
+ LINEAR_MIPMAP_LINEAR
+ }
+
+ /**
+ * Magnification filter.
+ */
+ private MagFilter magFilter = MagFilter.AUTO;
+ /**
+ * Minification filter.
+ */
+ private MinFilter minFilter = MinFilter.AUTO;
+ /**
+ * s wrapping mode.
+ * default value : REPEAT
+ */
+ private Wrap wrapS = Wrap.REPEAT;
+ /**
+ * t wrapping mode.
+ * default value : REPEAT
+ */
+ private Wrap wrapT = Wrap.REPEAT;
+
+ public Sampler() {
+ }
+
+ /**
+ * Magnification filter.
+ */
+ public MagFilter getMagFilter() {
+ return magFilter;
+ }
+
+ /**
+ * Magnification filter.
+ */
+ public void setMagFilter(MagFilter magFilter) {
+ ArgumentChecks.ensureNonNull("mag Filter", magFilter);
+ this.magFilter = magFilter;
+ }
+
+ /**
+ * Minification filter.
+ */
+ public MinFilter getMinFilter() {
+ return minFilter;
+ }
+
+ /**
+ * Minification filter.
+ */
+ public void setMinFilter(MinFilter minFilter) {
+ ArgumentChecks.ensureNonNull("min Filter", minFilter);
+ this.minFilter = minFilter;
+ }
+
+ /**
+ * s wrapping mode.
+ */
+ public Wrap getWrapS() {
+ return wrapS;
+ }
+
+ /**
+ * s wrapping mode.
+ */
+ public void setWrapS(Wrap wrapS) {
+ ArgumentChecks.ensureNonNull("wrap s", wrapS);
+ this.wrapS = wrapS;
+ }
+
+ /**
+ * t wrapping mode.
+ */
+ public Wrap getWrapT() {
+ return wrapT;
+ }
+
+ /**
+ * t wrapping mode.
+ */
+ public void setWrapT(Wrap wrapT) {
+ ArgumentChecks.ensureNonNull("wrap t", wrapT);
+ this.wrapT = wrapT;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 97 * hash + Objects.hashCode(this.magFilter);
+ hash = 97 * hash + Objects.hashCode(this.minFilter);
+ hash = 97 * hash + Objects.hashCode(this.wrapS);
+ hash = 97 * hash + Objects.hashCode(this.wrapT);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Sampler other = (Sampler) obj;
+ if (this.magFilter != other.magFilter) {
+ return false;
+ }
+ if (this.minFilter != other.minFilter) {
+ return false;
+ }
+ if (this.wrapS != other.wrapS) {
+ return false;
+ }
+ if (this.wrapT != other.wrapT) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/SceneNode.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/SceneNode.java
new file mode 100644
index 0000000000..feb3ff13c8
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/SceneNode.java
@@ -0,0 +1,480 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.sis.geometries.Geometries;
+import org.apache.sis.geometries.math.Similarity3D;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Utilities;
+import org.opengis.feature.Feature;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.TransformException;
+
+/**
+ * A scene node.
+ * This class is used by scenograph rendering pipelines.
+ * Each node has a transform and possible childrens.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class SceneNode {
+
+ private final Similarity3D parentToNode = new Similarity3D();
+ private SceneNode parent = null;
+ private final List<SceneNode> children = new
NotifiedCheckedList<SceneNode>(){
+ @Override
+ protected void notifyAdd(SceneNode item, int index) {
+ makeChild(item);
+ }
+
+ @Override
+ protected void notifyAdd(Collection<? extends SceneNode> items,
NumberRange<Integer> range) {
+ for (SceneNode n : items) {
+ makeChild(n);
+ }
+ }
+
+ @Override
+ protected void notifyChange(SceneNode oldItem, SceneNode newItem, int
index) {
+ unmakeChild(oldItem);
+ makeChild(newItem);
+ }
+
+ @Override
+ protected void notifyRemove(SceneNode item, int index) {
+ unmakeChild(item);
+ }
+
+ @Override
+ protected void notifyRemove(Collection<? extends SceneNode> items,
NumberRange<Integer> range) {
+ for (SceneNode n : items) {
+ unmakeChild(n);
+ }
+ }
+
+ private void makeChild(SceneNode node) {
+ if
(!Utilities.equalsIgnoreMetadata(getCoordinateReferenceSystem(),
node.getCoordinateReferenceSystem())) {
+ super.remove(node);
+ throw new IllegalArgumentException("Child node coordinate
system do not match scene coordinate system");
+ }
+
+ final SceneNode oldParent = node.getParent();
+ if (oldParent != null) oldParent.getChildren().remove(node);
+ node.setParent(SceneNode.this);
+ }
+
+ private void unmakeChild(SceneNode node) {
+ node.setParent(null);
+ }
+ };
+
+ private CoordinateReferenceSystem crs = Geometries.RIGHT_HAND_3D;
+ private Camera camera;
+ private Model model;
+ private String name;
+ private Feature feature;
+ //user properties
+ private Map<String,Object> properties;
+
+ /**
+ * Build a 3D Right handed coordinate reference system.
+ */
+ public SceneNode() {
+ }
+
+ /**
+ * Build scene node with given system.
+ * @param crs not null, must have three dimensions
+ */
+ public SceneNode(CoordinateReferenceSystem crs) {
+ setCoordinateReferenceSystem(crs);
+ }
+
+ /**
+ * Build scene node with given model.
+ * Model coordinate system is copied to scene node.
+ * @param model, not null
+ */
+ public SceneNode(Model model) {
+ ArgumentChecks.ensureNonNull("model", model);
+ setCoordinateReferenceSystem(model.getCoordinateReferenceSystem());
+ this.model = model;
+ }
+
+ /**
+ * @return node name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name node name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return scene node coordinate system, never null.
+ */
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return crs;
+ }
+
+ /**
+ * This method will modify :
+ * - model crs if defined
+ * - children nodes coordinate system recursively.
+ * @param crs
+ */
+ public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) {
+ if (parent != null) {
+ throw new IllegalArgumentException("Changing CRS can only be
called on root node");
+ }
+ ArgumentChecks.ensureNonNull("crs", crs);
+ ArgumentChecks.ensureCountBetween("dimension", true, 3, 3,
crs.getCoordinateSystem().getDimension());
+ setCrsNoCheck(crs);
+ }
+
+ private void setCrsNoCheck(CoordinateReferenceSystem crs) {
+ if (model != null) {
+ model.setCoordinateReferenceSystem(crs);
+ }
+ for (SceneNode sn : children) {
+ sn.setCrsNoCheck(crs);
+ }
+ this.crs = crs;
+ }
+
+ /**
+ * Get transform from parent node to this node.
+ *
+ * @return Transform, never null.
+ */
+ public Similarity3D getTransform() {
+ return parentToNode;
+ }
+
+ /**
+ * @return parent node
+ */
+ public SceneNode getParent() {
+ return parent;
+ }
+
+ /**
+ * Internal use only.
+ * Called by the new parent node only.
+ */
+ public void setParent(SceneNode parent) {
+ this.parent = parent;
+ }
+
+ /**
+ * @return modifiable list of children nodes.
+ */
+ public List<SceneNode> getChildren() {
+ return children;
+ }
+
+ /**
+ * @return 3D model attached, may be null
+ */
+ public Model getModel() {
+ return model;
+ }
+
+ /**
+ * @param model 3D model to attach, may be null
+ * model must have the same crs as the scene node.
+ */
+ public void setModel(Model model) {
+ if (model != null) {
+ if
(!Utilities.equalsIgnoreMetadata(getCoordinateReferenceSystem(),
model.getCoordinateReferenceSystem())) {
+ throw new IllegalArgumentException("Model coordinate system do
not match scene coordinate system");
+ }
+ }
+ this.model = model;
+ }
+
+ /**
+ * @return camera attached, may be null
+ */
+ public Camera getCamera() {
+ return camera;
+ }
+
+ /**
+ * @param camera to attach, may be null
+ */
+ public void setCamera(Camera camera) {
+ this.camera = camera;
+ }
+
+ /**
+ * @return Feature this node represent
+ * used to attach atttributes on scene models.
+ */
+ public Feature getFeature() {
+ return feature;
+ }
+
+ /**
+ * @param feature Feature this node represent.
+ */
+ public void setFeature(Feature feature) {
+ this.feature = feature;
+ }
+
+ /**
+ * Get this scene envelope, including child nodes.
+ *
+ * @param applyTransform true to transform the envelope with the node
transform.
+ * the envelope will be in the parent coordinate system.
+ * @return node envelope, can be null if no model 3d are attached in the
graph.
+ */
+ public Optional<Envelope> getEnvelope(boolean applyTransform) throws
NoninvertibleTransformException, TransformException {
+
+ GeneralEnvelope e = null;
+
+ // add model envelope
+ final Model model = getModel();
+ if (model != null) {
+ Envelope me = model.getEnvelope();
+ e = new GeneralEnvelope(me);
+ //we ignore the model crs, often undefined
+ e.setCoordinateReferenceSystem(getCoordinateReferenceSystem());
+ if (e.isAllNaN()) {
+ e = null;
+ }
+ }
+
+ // concatenate child node envelopes
+ for (SceneNode sn : children) {
+ Optional<Envelope> envelope = sn.getEnvelope(true);
+ if (envelope.isPresent()) {
+ final Envelope ce = envelope.get();
+ if
(!Utilities.equalsIgnoreMetadata(getCoordinateReferenceSystem(),
ce.getCoordinateReferenceSystem())) {
+ throw new IllegalArgumentException("A child node
coordinate system do not match scene coordinate system");
+ }
+ if (e == null) {
+ e = new GeneralEnvelope(ce);
+ } else {
+ e.add(ce);
+ }
+ }
+ }
+
+ // apply transform
+ if (applyTransform && e != null) {
+ final Similarity3D transform = getTransform();
+ if (!transform.isIdentity()) {
+ LinearTransform trs =
MathTransforms.linear(transform.toMatrix());
+ e.setEnvelope(Envelopes.transform(trs, e));
+ }
+ }
+
+ return Optional.ofNullable(e);
+ }
+
+ /**
+ * Map of properties for user needs.
+ * Those informations may be lost in node processes.
+ *
+ * @return Map, never null.
+ */
+ public synchronized Map<String, Object> userProperties() {
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+ return properties;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Node");
+ if (name != null) sb.append(' ').append(name).append(' ');
+ sb.append('(');
+ if (model != null) sb.append(" Model ");
+ if (camera != null) sb.append(" Camera ");
+ if (feature != null) sb.append(" Feature ");
+ sb.append(" Children[").append(children.size()).append("] ");
+ sb.append(')');
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 97 * hash + Objects.hashCode(this.parentToNode);
+ hash = 97 * hash + Objects.hashCode(this.camera);
+ hash = 97 * hash + Objects.hashCode(this.model);
+ hash = 97 * hash + Objects.hashCode(this.name);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final SceneNode other = (SceneNode) obj;
+ if (!Objects.equals(this.name, other.name)) {
+ return false;
+ }
+ if (!Objects.equals(this.parentToNode, other.parentToNode)) {
+ return false;
+ }
+ if (!Objects.equals(this.children, other.children)) {
+ return false;
+ }
+ if (!Objects.equals(this.camera, other.camera)) {
+ return false;
+ }
+ if (!Objects.equals(this.model, other.model)) {
+ return false;
+ }
+ return true;
+ }
+
+ private abstract class NotifiedCheckedList<E> extends ArrayList<E>{
+
+ public NotifiedCheckedList() {
+ super();
+ }
+
+ public NotifiedCheckedList(final int capacity) {
+ super(capacity);
+ }
+
+ protected abstract void notifyAdd(final E item, int index);
+
+ protected abstract void notifyAdd(final Collection<? extends E> items,
NumberRange<Integer> range);
+
+ protected abstract void notifyChange(final E oldItem, E newItem, int
index);
+
+ protected abstract void notifyRemove(final E item, int index);
+
+ protected abstract void notifyRemove(final Collection<? extends E>
items, NumberRange<Integer> range);
+
+ @Override
+ public boolean add(final E element) throws IllegalArgumentException,
UnsupportedOperationException {
+ if(element == null) return false;
+ final boolean added = super.add(element);
+ if (added) {
+ final int index = super.size() - 1;
+ notifyAdd(element, index);
+ }
+ return added;
+ }
+
+ @Override
+ public void add(final int index, final E element) throws
IllegalArgumentException, UnsupportedOperationException {
+ super.add(index, element);
+ notifyAdd(element, index);
+ }
+
+ @Override
+ public E set(int index, E element) throws IllegalArgumentException,
UnsupportedOperationException {
+ final E old = super.set(index, element);
+ notifyChange(old, element, index);
+ return old;
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends E> collection) throws
IllegalArgumentException, UnsupportedOperationException {
+ final int startIndex = super.size();
+ final boolean added = super.addAll(collection);
+ if (added) {
+ notifyAdd(collection, NumberRange.create(startIndex, true,
super.size()-1, true) );
+ }
+ return added;
+ }
+
+ @Override
+ public boolean addAll(final int index, final Collection<? extends E>
collection) throws IllegalArgumentException, UnsupportedOperationException {
+ final boolean added = super.addAll(index, collection);
+ if (added) {
+ notifyAdd(collection, NumberRange.create(index, true, index +
collection.size(), true) );
+ }
+ return added;
+ }
+
+ @Override
+ public boolean remove(final Object o) throws
UnsupportedOperationException {
+ final int index = super.indexOf(o);
+ if (index >= 0) {
+ super.remove(index);
+ notifyRemove((E) o, index );
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public E remove(final int index) throws UnsupportedOperationException {
+ final E removed = super.remove(index);
+ notifyRemove(removed, index );
+ return removed;
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> c) throws
UnsupportedOperationException {
+ //TODO handle remove by collection events if possible
+ // to avoid several calls to remove
+ boolean valid = false;
+ for(final Object i : c){
+ final boolean val = remove(i);
+ if(val) valid = val;
+ }
+ return valid;
+ }
+
+ @Override
+ public void clear() throws UnsupportedOperationException {
+ if(!isEmpty()){
+ final Collection<E> copy = new ArrayList<E>(this);
+ final NumberRange<Integer> range = NumberRange.create(0, true,
copy.size()-1, true);
+ super.clear();
+ notifyRemove(copy, range);
+ }
+ }
+
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Surface.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Surface.java
new file mode 100644
index 0000000000..e4daf2b40d
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Surface.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.util.Objects;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.util.ArgumentChecks;
+
+/**
+ * GLTF name this a Primitive.
+ * ANARI name this a Surface.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Surface {
+
+ private Geometry geometry;
+ private Material material;
+
+ public Surface(Geometry geometry) {
+ ArgumentChecks.ensureNonNull("geometry", geometry);
+ this.geometry = geometry;
+ this.material = new Material();
+ }
+
+ public Surface(Geometry geometry, Material material) {
+ ArgumentChecks.ensureNonNull("geometry", geometry);
+ ArgumentChecks.ensureNonNull("material", material);
+ this.geometry = geometry;
+ this.material = material;
+ }
+
+ /**
+ * Get geometry definition.
+ *
+ * @return Geometry never null
+ */
+ public Geometry getGeometry() {
+ return geometry;
+ }
+
+ public void setGeometry(Geometry geometry) {
+ ArgumentChecks.ensureNonNull("geometry", geometry);
+ this.geometry = geometry;
+ }
+
+ /**
+ * Get material definition.
+ *
+ * @return Material never null
+ */
+ public Material getMaterial() {
+ return material;
+ }
+
+ /**
+ * Set material definition.
+ * @param material not null;
+ */
+ public void setMaterial(Material material) {
+ ArgumentChecks.ensureNonNull("material", material);
+ this.material = material;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Surface other = (Surface) obj;
+ if (!Objects.equals(this.geometry, other.geometry)) {
+ return false;
+ }
+ if (!Objects.equals(this.material, other.material)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 71 * hash + Objects.hashCode(this.geometry);
+ hash = 71 * hash + Objects.hashCode(this.material);
+ return hash;
+ }
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Texture.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Texture.java
new file mode 100644
index 0000000000..01837a028a
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/Texture.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.geometries.scene;
+
+import java.awt.Point;
+import java.awt.image.ColorModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.image.PixelIterator;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Texture {
+
+ private RenderedImage image;
+ private String texCoord = AttributesType.ATT_TEXCOORD_0;
+ private Sampler sampler = new Sampler();
+
+ /**
+ * @return loaded image
+ */
+ public RenderedImage getImage() {
+ return image;
+ }
+
+ /**
+ * @param image loaded image
+ */
+ public void setImage(RenderedImage image) {
+ this.image = image;
+ }
+
+ /**
+ * @return The name texture's TEXCOORD attribute used for texture
coordinate mapping.
+ */
+ public String getTexCoord() {
+ return texCoord;
+ }
+
+ /**
+ * @param texCoord The name texture's TEXCOORD attribute used for texture
coordinate mapping.
+ */
+ public void setTexCoord(String texCoord) {
+ this.texCoord = texCoord;
+ }
+
+ public Sampler getSampler() {
+ return sampler;
+ }
+
+ public void setSampler(Sampler sampler) {
+ this.sampler = sampler;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 97 * hash + Objects.hashCode(this.image);
+ hash = 97 * hash + Objects.hashCode(this.texCoord);
+ hash = 97 * hash + Objects.hashCode(this.sampler);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Texture other = (Texture) obj;
+ if (!Objects.equals(this.texCoord, other.texCoord)) {
+ return false;
+ }
+ if (!( (this.image == other.image) || (this.image != null &&
compare(this.image, other.image)))) {
+ return false;
+ }
+ if (!Objects.equals(this.sampler, other.sampler)) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean compare(RenderedImage expected, RenderedImage
result) {
+ if (expected == result) {
+ return true;
+ }
+ final ColorModel expectedCm = expected.getColorModel();
+ final ColorModel resultCm = result.getColorModel();
+ if (!expectedCm.equals(resultCm)) {
+ return false;
+ }
+ final SampleModel expectedSm = expected.getSampleModel();
+ final SampleModel resultSm = result.getSampleModel();
+ if (!expectedSm.equals(resultSm)) {
+ return false;
+ }
+
+ final PixelIterator ite1 = PixelIterator.create(expected);
+ final PixelIterator ite2 = PixelIterator.create(result);
+ if (!ite1.getDomain().equals(ite2.getDomain())) {
+ return false;
+ }
+ final double[] pixel1 = new double[ite1.getNumBands()];
+ final double[] pixel2 = new double[ite2.getNumBands()];
+
+ pixelLoop:
+ while (ite1.next()) {
+ final Point position = ite1.getPosition();
+ ite2.moveTo(position.x, position.y);
+ ite1.getPixel(pixel1);
+ ite2.getPixel(pixel2);
+ if (!Arrays.equals(pixel1, pixel2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/package-info.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/package-info.java
new file mode 100644
index 0000000000..e52ef5bdb7
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/scene/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+ * This package contains a simplified version of a scene model.
+ *
+ * Based on specifications :
+ * <ul>
+ * <li>Khronos GLTF-2
https://github.com/KhronosGroup/glTF/tree/main/specification/2.0</li>
+ * <li>Khronos ANARI https://github.com/KhronosGroup/ANARI-SDK</li>
+ * </ul>
+ */
+package org.apache.sis.geometries.scene;