[SOLVED] Odd Culling Behavior

This code fails to never cull my instanced particle geometry.

InstancedParticleGeometry geometry = ...
geometry.setCullHint(Spatial.CullHint.Never);
Node n = new Node();
n.attachChild(geometry);
rootNode.attachChild(n);

The geometry gets culled when the world bound leaves the camera frustum, even though many of the geometry’s instances are visible and I set the cull hint to never.

The next code snippet succeeds in never culling the particle geometry.

InstancedParticleGeometry geometry = ...
geometry.setCullHint(Spatial.CullHint.Never);
rootNode.attachChild(geometry);

Why is this? The only difference is there is a non-descript node between the geometry and the root. Is there something I’m misunderstanding about culling?

If you replace InstancedParticleGeometry with a regular geometry then I suspect you will see that everything will work as you expect.

Edit: meaning, if so then the problem is with InstancedParticleGeometry.

Well, I guess “see” is probably the wrong word :slight_smile: cause I can’t see a normal geometry when it gets culled anyway. But you’re probably right.

I’ve been through my code, but I couldn’t find anything that I haven’t already tried changing that would be a problem.

public class InstancedParticleGeometry extends Geometry {
    
    private ParticleGroup<ParticleData> group;
    private final Transform transform = new Transform();
    private int capacity = -1;
    
    
    public InstancedParticleGeometry(ParticleGroup group, Mesh mesh) {
        super();
        this.group = group;
        super.setMesh(mesh);
        setCullHint(Spatial.CullHint.Never);
        setIgnoreTransform(true);
    }
    
    private void initBuffers() {  
        capacity = group.capacity();
        FloatBuffer ib = BufferUtils.createFloatBuffer(capacity * 16);
        VertexBuffer ivb = MeshUtils.initializeVertexBuffer(mesh,
            VertexBuffer.Type.InstanceData,
            VertexBuffer.Usage.Stream,
            VertexBuffer.Format.Float,
            ib, 16
        );
        ivb.setInstanced(true);
        FloatBuffer cb = BufferUtils.createFloatBuffer(capacity * 4);
        VertexBuffer cvb = MeshUtils.initializeVertexBuffer(mesh,
            VertexBuffer.Type.Color,
            VertexBuffer.Usage.Stream,
            VertexBuffer.Format.Float,
            cb, 4
        );
        cvb.setInstanced(true);
    }
    
    @Override
    public void updateLogicalState(float tpf) {
        if (capacity != group.capacity()) {
            initBuffers();
        }
        VertexBuffer ivb = mesh.getBuffer(VertexBuffer.Type.InstanceData);
        FloatBuffer instances = (FloatBuffer)ivb.getData();
        instances.clear();
        VertexBuffer cvb = mesh.getBuffer(VertexBuffer.Type.Color);
        FloatBuffer colors = (FloatBuffer)cvb.getData();
        colors.clear();
        if (!group.isEmpty()) {
            for (ParticleData p : group) {
                transform.set(p.transform);
                transform.getScale().multLocal(p.size.get());
                MeshUtils.writeTransformMatrix(instances, transform.toTransformMatrix());
                MeshUtils.writeColor(colors, p.color.get());
            }
        } else {
            MeshUtils.writeMatrix4(instances, Matrix4f.IDENTITY, false);
            MeshUtils.writeColor(colors, ColorRGBA.BlackNoAlpha);
        }
        instances.flip();
        ivb.updateData(instances);
        colors.flip();
        cvb.updateData(colors);
        mesh.updateCounts();
        mesh.updateBound();
    }
    
    public void setParticleGroup(ParticleGroup group) {
        this.group = group;
    }
    
}

Ofc, the world bound only covers the actual mesh, not the instances.

/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.material.logic;

import com.jme3.asset.AssetManager;
import com.jme3.light.*;
import com.jme3.material.TechniqueDef;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.Caps;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.instancing.InstancedGeometry;
import com.jme3.shader.DefineList;
import com.jme3.shader.Shader;
import java.util.EnumSet;

public class DefaultTechniqueDefLogic implements TechniqueDefLogic {

    protected final TechniqueDef techniqueDef;

    public DefaultTechniqueDefLogic(TechniqueDef techniqueDef) {
        this.techniqueDef = techniqueDef;
    }

    @Override
    public Shader makeCurrent(AssetManager assetManager, RenderManager renderManager,
            EnumSet<Caps> rendererCaps, LightList lights, DefineList defines) {
        return techniqueDef.getShader(assetManager, rendererCaps, defines);
    }

    public static void renderMeshFromGeometry(Renderer renderer, Geometry geom) {
        Mesh mesh = geom.getMesh();
        int lodLevel = geom.getLodLevel();
        if (geom instanceof InstancedGeometry) {
            InstancedGeometry instGeom = (InstancedGeometry) geom;
            int numVisibleInstances = instGeom.getNumVisibleInstances();
            if (numVisibleInstances > 0) {
                renderer.renderMesh(mesh, lodLevel, numVisibleInstances, instGeom.getAllInstanceData());
            }
        } else {
            renderer.renderMesh(mesh, lodLevel, 1, null);
        }
    }

    protected static ColorRGBA getAmbientColor(LightList lightList, boolean removeLights, ColorRGBA ambientLightColor) {
        ambientLightColor.set(0, 0, 0, 1);
        for (int j = 0; j < lightList.size(); j++) {
            Light l = lightList.get(j);
            if (l instanceof AmbientLight) {
                ambientLightColor.addLocal(l.getColor());
                if (removeLights) {
                    lightList.remove(l);
                }
            }
        }
        ambientLightColor.a = 1.0f;
        return ambientLightColor;
    }



    @Override
    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
        Renderer renderer = renderManager.getRenderer();
        renderer.setShader(shader);
        renderMeshFromGeometry(renderer, geometry);
    }
}

Maybe that’s the reason for the problem, instances only seem to work with InstancedGeometry.
Maybe you need to extend from InstancedGeometry and not just Geometry

You override updateLogicalState() but you never call super.updateLogicalState() so a bunch of stuff won’t get updated, controls won’t run, etc…

1 Like

The SDK gives a warning when I call super.updateLogicalState saying that updating is not needed in jme3. I tried adding it in anyway, but it did not fix the issue.

@FoxCC
Instancing is working fine for me, it’s just the culling that is messed up.

1 Like

I have found the issue (I believe).

The RenderManager, if a node is culled dynamically, skips over the node’s children and does not process them. This is fine if each child’s world bound accurately depicts its visible geometry, but if the world bound is not accurate (as in my case), it will lead to culling problems.

1 Like

The SDK is dumb. It gives this warning because JME2 used to require that you manually call updateLogicalState() yourself whenever you changed anything. In JME3 this is automatic so the SDK blindly warns anyone whenever they call this method for any reason… even when it’s a good one.

But whenever you override a method, you need to call the super class version or you break things. Just in java in general, that’s 99% true except in very specific cases.

So your code was definitely broken before whether it fixed this particular issue or not. And if node were working properly with cull hint of its children, your old code would have been broken because nothing would have told the node about the child change.

1 Like

Ok, that makes a lot more sense. :stuck_out_tongue: Thanks

To fix the culling problem, I see two solutions:

  1. Generate a world bound encompassing every instance completely, then the particle geometry could be culled dynamically.
  2. Change the culling system so that spatials set to never cull cannot be culled by parents set to dynamically cull.

Personally, I’m leaning toward solution 2, since this seems like an engine defect imo, and it would far easier and less expensive than generating a world bound for instanced particles. So, I went ahead and drafted some changes that should clear this up.

What do you think?

2 Likes

Though if you never want your particles to be culled anyway then you can just set your own bound to an infinite sphere and the problem is solved.

2 Likes

That solved it, but I’d still much rather use CullHint.Never and have it work.

Thanks anyways! :slight_smile:

1 Like