warning this code is a mess, for this demo code some parameters are adjusted.
for normal scenes it suppose generate tiles of instances in different areas thats what removal distance is used for, has the player would get further away it would remove old instances from the lists, and generate new instances around the player
a normal scene
max distance is set around 450 for a normal scene
removal distance is around 850
the culling function works better on normal scenes less flickering
currently the main problem is the culling function have yet to be able to get it much faster,
still works with around 20k instances, i havent been able to get quad trees or grids to work
# vertex shader:
v_shader = '''#version 330
struct p3d_DirectionalLightParameters {
vec4 color;
vec3 direction;
sampler2DShadow shadowMap;
mat4 shadowViewMatrix;
};
uniform p3d_DirectionalLightParameters my_directional_light;
uniform mat4 p3d_ModelViewProjectionMatrix;
uniform mat3 p3d_NormalMatrix;
uniform mat4 p3d_ModelViewMatrix;
in vec4 p3d_Vertex;
in vec3 p3d_Normal;
in vec2 p3d_MultiTexCoord0;
in vec4 offset;
in vec4 rotation; // Heading, Pitch, Roll
in vec4 scale;
out vec2 uv;
out vec4 shadow_uv;
out vec3 normal;
out vec4 fragPos;
mat4 rotationMatrixX(float angle) {
float c = cos(angle);
float s = sin(angle);
return mat4(
1.0, 0.0, 0.0, 0.0,
0.0, c, -s, 0.0,
0.0, s, c, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
mat4 rotationMatrixY(float angle) {
float c = cos(angle);
float s = sin(angle);
return mat4(
c, 0.0, s, 0.0,
0.0, 1.0, 0.0, 0.0,
-s, 0.0, c, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
mat4 rotationMatrixZ(float angle) {
float c = cos(angle);
float s = sin(angle);
return mat4(
c, -s, 0.0, 0.0,
s, c, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
void main() {
vec4 vertexPosition = p3d_Vertex;
vec3 transformedNormal = p3d_Normal;
// Apply uniform scale
vertexPosition *= scale;
// Convert degrees to radians
float angleH = radians(rotation.x); // Heading (yaw)
float angleP = radians(rotation.y); // Pitch
float angleR = radians(rotation.z); // Roll
// Create rotation matrices from Euler angles
mat4 rotationMatrix = rotationMatrixY(angleH) * rotationMatrixX(angleP) * rotationMatrixZ(angleR);
// Apply rotation
vertexPosition = rotationMatrix * vertexPosition;
transformedNormal = normalize(mat3(rotationMatrix) * p3d_Normal);
// Apply offset
vertexPosition += offset;
// Position
gl_Position = p3d_ModelViewProjectionMatrix * vertexPosition;
// Normal
normal = p3d_NormalMatrix * transformedNormal;
// UV
uv = p3d_MultiTexCoord0;
// Shadows
shadow_uv = my_directional_light.shadowViewMatrix * (p3d_ModelViewMatrix * vertexPosition);
// Frag position
fragPos = p3d_ModelViewMatrix * vertexPosition;
}'''
# fragment shader
f_shader = '''#version 330
struct p3d_DirectionalLightParameters {
vec4 color;
vec3 direction;
sampler2DShadow shadowMap;
mat4 shadowViewMatrix;
};
struct p3d_PointLightParameters {
vec4 color;
vec3 position;
samplerCube shadowMap;
vec3 attenuation;
};
struct p3d_SpotLightParameters {
vec4 color;
vec3 position;
vec3 spotDirection;
sampler2DShadow shadowMap;
mat4 shadowViewMatrix;
vec3 attenuation;
};
const int MAX_POINT_LIGHTS = 4;
const int MAX_SPOT_LIGHTS = 4;
uniform p3d_DirectionalLightParameters my_directional_light;
uniform p3d_PointLightParameters point_lights[MAX_POINT_LIGHTS];
uniform p3d_SpotLightParameters spot_lights[MAX_SPOT_LIGHTS];
uniform sampler2D p3d_Texture0;
uniform vec3 camera_pos;
uniform float shadow_blur;
uniform vec4 ambientLightColor;
uniform vec4 fogColor;
uniform float fogStart;
uniform float fogEnd;
uniform vec3 player_pos;
uniform bool enable_transparency;
uniform vec4 horizonColorb;
uniform int num_point_lights;
uniform int num_spot_lights;
in vec2 uv;
in vec4 shadow_uv;
in vec3 normal;
in vec4 fragPos;
out vec4 color;
float textureProjSoft(sampler2DShadow tex, vec4 uv, float bias, float blur) {
float result = textureProj(tex, uv, bias);
result += textureProj(tex, vec4(uv.xy + vec2(-0.326212, -0.405805) * blur, uv.z - bias, uv.w));
result += textureProj(tex, vec4(uv.xy + vec2(-0.840144, -0.073580) * blur, uv.z - bias, uv.w));
result += textureProj(tex, vec4(uv.xy + vec2(-0.695914, 0.457137) * blur, uv.z - bias, uv.w));
result += textureProj(tex, vec4(uv.xy + vec2(-0.203345, 0.620716) * blur, uv.z - bias, uv.w));
return result / 5.0; // Reduced number of samples
}
float calculatePointLightShadow(vec3 fragPos, vec3 lightPos, samplerCube shadowMap) {
vec3 lightToFrag = fragPos - lightPos;
float currentDepth = length(lightToFrag);
float shadow = texture(shadowMap, lightToFrag).r;
float bias = 0.05; // Adjust bias as needed
return currentDepth - bias > shadow ? 0.5 : 1.0;
}
void main() {
// Base color
vec3 ambient = ambientLightColor.rgb;
vec4 tex = texture(p3d_Texture0, uv);
// Calculate directional light contribution
vec3 dirLight = my_directional_light.color.rgb * max(dot(normalize(normal), my_directional_light.direction), 0.0);
float dirLightShadow = textureProjSoft(my_directional_light.shadowMap, shadow_uv, 0.0001, shadow_blur);
dirLightShadow = 0.5 + dirLightShadow * 0.5;
dirLight *= dirLightShadow;
// Calculate point light contributions with attenuation
vec3 totalPointLight = vec3(0.0);
for (int i = 0; i < num_point_lights; i++) {
vec3 lightDir = point_lights[i].position - fragPos.xyz;
float distance = length(lightDir);
vec3 attenuationFactors = point_lights[i].attenuation; // Fetch attenuation from struct
float attenuation = 1.0 / (attenuationFactors.x + attenuationFactors.y * distance + attenuationFactors.z * (distance * distance));
vec3 pointLight = point_lights[i].color.rgb * max(dot(normalize(normal), normalize(lightDir)), 0.0);
pointLight *= attenuation;
pointLight *= calculatePointLightShadow(fragPos.xyz, point_lights[i].position, point_lights[i].shadowMap);
totalPointLight += pointLight;
}
// Calculate spotlight contributions
vec3 totalSpotLight = vec3(0.0);
for (int i = 0; i < num_spot_lights; i++) {
vec3 spotDirection = normalize(spot_lights[i].spotDirection);
vec3 lightDir = spot_lights[i].position - fragPos.xyz;
float distance = length(lightDir);
vec3 attenuationFactors = spot_lights[i].attenuation;
float attenuation = 1.0 / (attenuationFactors.x + attenuationFactors.y * distance + attenuationFactors.z * (distance * distance)); // Use attenuation from struct
vec3 spotLight = spot_lights[i].color.rgb * max(dot(normalize(normal), -spotDirection), 0.0);
float theta = dot(normalize(fragPos.xyz - spot_lights[i].position), spotDirection);
float intensity = max(pow(theta, 10.0), 0.0); // Adjust the exponent to control the spotlight focus
spotLight *= intensity * attenuation;
totalSpotLight += spotLight;
}
// Combine all lighting
vec3 finalLight = dirLight + ambient + totalPointLight + totalSpotLight;
// Precompute fog factor
float heightFogFactor = clamp((fogEnd - length(fragPos.xyz.y)) / (fogEnd - fogStart), 0.0, 1.0);
float depthFogFactor = clamp((fogEnd - length(fragPos.xyz)) / (fogEnd - fogStart), 0.0, 1.0);
float fogFactor = min(heightFogFactor, depthFogFactor);
// Blend fog color with skybox color at the horizon
vec4 horizonColor = mix(fogColor, horizonColorb, 0.5);
vec4 foggedColor = mix(horizonColor, vec4(tex.rgb * finalLight, tex.a), fogFactor);
// Calculate distance from player position
float distance = length(fragPos.xyz - player_pos);
// Define a threshold for alpha
float alphaThreshold = 0.5;
// Adjust alpha based on distance with a hard cut-off
float alpha = enable_transparency ? (distance < 24.0 ? 0.0 : (tex.a > alphaThreshold ? 1.0 : 0.0)) : (tex.a > alphaThreshold ? 1.0 : 0.0);
// Apply the alpha to the fogged color
color = vec4(foggedColor.rgb, alpha);
}'''
import random
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
import time
import numpy as np
from direct.actor.Actor import Actor
from direct.task import Task
# Set the dimensions of the height map
height, width = 4486, 4486
# Generate the noise height map
noise_height_map = np.random.rand(height, width)
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
base.trackball.node().set_pos(4.7, 112.7, -9.7)
base.trackball.node().set_hpr(61.5281, 12.0915, -18.2124)
self.node = Actor('panda-model', {'walk' : 'panda-walk4'})
self.node.loop('walk')
self.node.setScale(0.01)
self.node.reparentTo(render)
# self.node.setPos(0,0,0)
#lighting
self.sun = DirectionalLight("Spot")
# print(dir(self.sun),'attributes')
self.sun_path = self.render.attachNewNode(self.sun)
self.sun_path.node().set_shadow_caster(True, 4096, 4096)
self.sun_path.node().set_color((0.9, 0.9, 0.8, 1.0))
# self.sun_path.node().showFrustum()
self.sun_path.node().get_lens().set_fov(40)
# self.sun_path.node().attenuation = (1, 0.001, 0.0001)
self.sun_path.node().get_lens().set_near_far(-400, 400)
self.sun_path.node().get_lens().set_film_size(400)
self.pivot = self.render.attachNewNode("pivot")
self.pivot.setPos(0, 0, 0) # Set the position of the pivot point
self.sun_path.setHpr(0, 0, 0)
self.sun_path.reparentTo(self.pivot)
self.render.setLight(self.sun_path)
#sun shader settings
self.render.set_shader_input('my_directional_light',self.sun_path)
self.render.set_shader_input("my_directional_light.direction", self.sun.getDirection())
self.instanceshader = Shader.make(Shader.SL_GLSL,v_shader, f_shader)
self.node.setShader(self.instanceshader)
# self.render.set_shader_input('my_point_light',self.point_path)
self.spotlight1 = Spotlight("spotlight1")
self.spotlight1.setColor((10, 10, 10, 1)) # Brighter light (RGB values higher than 1)
self.spotlight1.setAttenuation((1.0, 0.09, 0.032))
self.spotlight1.setMaxDistance(10)
self.spotlight1_path = self.render.attachNewNode(self.spotlight1)
# self.spotlight1_path.setPos(-16, -31, 28) # Example position
self.render.setLight(self.spotlight1_path)
# self.spotlight1.showFrustum()
self.spotlight2 = Spotlight("spotlight2")
self.spotlight2.setAttenuation((1.0, 0.09, 0.032))
self.spotlight2.setMaxDistance(10)
self.spotlight2_path = self.render.attachNewNode(self.spotlight2)
self.spotlight2_path.setPos(-22, -22, 26) # Example position
self.render.setLight(self.spotlight2_path)
# self.spotlight2.showFrustum()
self.render.set_shader_input('num_spot_lights', 2)
self.render.set_shader_input('my_spot_light', self.spotlight1_path)
self.render.set_shader_input('spot_lights[0]', self.spotlight1_path)
self.render.set_shader_input('spot_lights[1]', self.spotlight2_path)
# self.spotlight1_path.reparentTo(self.point_path)
self.render.set_shader_input(f'spot_lights[1].color', LVecBase4(0, 0, 0, 0))#off
self.render.set_shader_input(f'spot_lights[0].color', LVecBase4(0, 0, 0, 0))#off
for i in range(2,4):
self.render.set_shader_input(f'spot_lights[{i}]',self.spotlight1_path)
self.render.set_shader_input(f'spot_lights[{i}].color', LVecBase4(0, 0, 0, 0))#off
# Initialize point lights
self.point1 = PointLight("Point1")
self.point1.setAttenuation((1.0, 0.09, 0.032))
self.point1.setMaxDistance(50)
self.point1_path = self.render.attachNewNode(self.point1)
self.point1_path.setPos(-16, -31, 29) # Example position
self.render.setLight(self.point1_path)
self.point2 = PointLight("Point2")
self.point2.setAttenuation((0.1, 0.43, 0.044))
self.point2.setMaxDistance(2)
self.point2_path = self.render.attachNewNode(self.point2)
self.point2_path.setPos(-22, -22, 29) # Example position
self.render.setLight(self.point2_path)
#point light
self.point = PointLight("Point")
self.point.setAttenuation((1.0, 0.09, 0.032)) # Adjust these values to control the attenuation
self.point.setMaxDistance(50) # Set the maximum distance of the light's influence
# self.point.showFrustum()
self.point_path = self.render.attachNewNode(self.point)
self.render.set_shader_input('num_point_lights', 2)
# Set shader inputs for existing lights
self.render.set_shader_input('point_lights[0]',self.point_path)
self.render.set_shader_input('point_lights[1]',self.point2_path)
self.render.set_shader_input('point_lights[1].color', LVecBase4(0, 0, 0, 0))#off
self.render.set_shader_input('point_lights[0].color', LVecBase4(0, 0, 0, 0))#off
for i in range(2,4):
self.render.set_shader_input(f'point_lights[{i}]',self.point1_path)
self.render.set_shader_input(f'point_lights[{i}].color', LVecBase4(0, 0, 0, 0))#off
self.render.set_shader_input('shadow_blur',0.0005)
self.render.set_shader_input('player_pos',(0,0,0))
self.render.set_shader_input('enable_transparency',False)
self.render.setShaderInput("fogColor", (0.5, 0.5, 0.5, 1.0)) # Set the fog color
self.render.setShaderInput("fogStart", 300.0) # Set the fog start distance
self.render.setShaderInput("fogEnd", 410.0) # Set the fog end distance
self.render.setShaderInput("ambientLightColor", (0.1, 0.1, 0.1, 1.0))
self.horizon_colorday = Vec4(0.529, 0.808, 0.980, 1)
self.render.setShaderInput("horizonColorb", self.horizon_colorday)
camera = base.cam
lens = camera.node().getLens()
lens.setNear(1)
lens.setFar(700.0)
positionlist=self.position_gen(2000,12,4486,4486,noise_height_map)
self.offsetpositionlistbush=[]
self.scalelistbush=[]
self.rotationlistbush=[]
#instancing
self.removed_instances = []
self.total_instances = 0
self.instance_parent = None
self.poswriter1=None
self.rotwriter1=None
self.scalewriter1=None
(self.total_instances,
self.poswriter1,
self.rotwriter1,
self.scalewriter1,
self.offsetpositionlistbush,
self.scalelistbush,
self.rotationlistbush) = self.setup_instancing(
noise_height_map,
self.node,
positionlist,
fromvalue=0,
tovalue=2000,
new_location=(0, 0, 0),
zlevel=None,
total_instances=self.total_instances,
poswriter=self.poswriter1,
rotwriter=self.rotwriter1,
scalewriter=self.scalewriter1,
zrotonly=True
)
self.task = self.taskMgr.add(self.updateTask, "update")
def updateTask(self, task):
# if self.clearing == False:
# for i, item in enumerate(self.loadedfiles):
# index = i * 6 # Calculate the base index for each file.
# print(self.all_lists[index])
poswriter_list = self.poswriter1
rotwriter_list = self.rotwriter1
scalewriter_list = self.scalewriter1
position_list = self.offsetpositionlistbush
scale_list = self.scalelistbush
rotation_list = self.rotationlistbush
#radius of 20
visible_instances = self.cull_and_update_instances(self.node, base.cam, 20, poswriter_list, scalewriter_list, rotwriter_list, position_list, rotation_list, scale_list, max_distance=12050,removal_distance=109050)
print(visible_instances)
if visible_instances < 1:
self.node.hide()
else:
self.node.show()
self.node.setInstanceCount(visible_instances)
return Task.cont
def cull_and_update_instances(self, node, camera, radius, poswriter, scalewriter, rotwriter, positionlist, rotationlist, scalelist, max_distance=450, removal_distance=850):
lens = camera.node().getLens()
lensBounds = lens.makeBounds()
visible_instances = 0
removal_indices = []
poswriter.setRow(0)
rotwriter.setRow(0)
scalewriter.setRow(0)
camera_pos = camera.getPos(node)
bounds = BoundingSphere()
node_mat = node.getMat(camera) # Cache the node's transformation
for i, (pos, rotation, scale) in enumerate(zip(positionlist, rotationlist, scalelist)):
instance_position = LPoint3f(*pos)
distance_from_camera = (instance_position - camera_pos).length()
if distance_from_camera > removal_distance:
# Flag the index for removal
removal_indices.append(i)
continue
newcenter = instance_position
rotationin = LVecBase4f(rotation[0], rotation[1], rotation[2], 1)
scalein = LVecBase4f(scale[0], scale[1], scale[2], 1)
bounds.setCenter(newcenter)
bounds.setRadius(radius)
bounds.xform(node_mat) # Apply cached transformation
if lensBounds.contains(bounds) and distance_from_camera <= max_distance:
poswriter.add_data3(newcenter)
rotwriter.add_data4(rotationin)
scalewriter.add_data4(scalein)
visible_instances += 1
# Remove flagged instances
for idx in sorted(removal_indices, reverse=True):
del positionlist[idx]
del rotationlist[idx]
del scalelist[idx]
return visible_instances
def position_gen(self, value=1000, seed=29, area_width=486, area_height=486, height_map=None, min_distance=10):
random.seed(seed)
grid_size = min_distance
grid_width = area_width // grid_size
grid_height = area_height // grid_size
occupied_positions = []
for i in range(value):
while True:
grid_x = random.randint(0, grid_width - 1)
grid_y = random.randint(0, grid_height - 1)
x = grid_x * grid_size + random.uniform(0, grid_size)
y = grid_y * grid_size + random.uniform(0, grid_size)
z = height_map[int(x)][int(y)]
pos = (int(x), int(y), z)
if pos not in occupied_positions:
occupied_positions.append(pos)
break
return occupied_positions
def calculate_normal(self, x, y, height_map):
# Get the height of the neighbors or the vertex itself if it's an edge
height_x_plus_1 = height_map[x + 1][y] if x < len(height_map) - 1 else height_map[x][y]
height_x_minus_1 = height_map[x - 1][y] if x > 0 else height_map[x][y]
height_y_plus_1 = height_map[x][y + 1] if y < len(height_map[0]) - 1 else height_map[x][y]
height_y_minus_1 = height_map[x][y - 1] if y > 0 else height_map[x][y]
dx = (height_x_plus_1 - height_x_minus_1) / 2.0
dy = (height_y_plus_1 - height_y_minus_1) / 2.0
normal = Vec3(-dx, -dy, 1).normalized()
return normal
def normal_to_euler(self,normal):
# Normalize the normal vector
normal = normal / np.linalg.norm(normal)
# Calculate the Euler angles (Heading, Pitch, Roll)
pitch = np.arcsin(normal[1])
heading = -np.arctan2(normal[0], normal[2]) # Invert the heading
roll = 0
return heading, pitch, roll
def setup_instancing(self, heightmapsgotfunc, nodefunc, positionlist=[], seed=29, fromvalue=0, tovalue=250, new_location=(0, 0, 0), zlevel=True, total_instances=0, poswriter=None, rotwriter=None, scalewriter=None, zrotonly=False):
random.seed(seed)
gnode = nodefunc.find("**/+GeomNode").node()
offsetpositionlist=[]
scalelist=[]
rotationlist=[]
vdata = gnode.modifyGeom(0).modifyVertexData()
format = GeomVertexFormat(gnode.getGeom(0).getVertexData().getFormat())
if not format.hasColumn("offset"):
iformat = GeomVertexArrayFormat()
iformat.setDivisor(1)
iformat.addColumn("offset", 4, Geom.NT_stdfloat, Geom.C_other)
iformat.addColumn("rotation", 4, Geom.NT_stdfloat, Geom.C_other) # Add rotation column
iformat.addColumn("scale", 4, Geom.NT_stdfloat, Geom.C_other) # Add scale column
format.addArray(iformat)
format = GeomVertexFormat.registerFormat(format)
vdata.setFormat(format)
sorted_positions = positionlist if zlevel is None else sorted(positionlist, key=lambda pos: pos[2], reverse=zlevel)
if poswriter is None:
poswriter = GeomVertexWriter(vdata.modifyArray(2), 0)
if rotwriter is None:
rotwriter = GeomVertexWriter(vdata.modifyArray(2), 1) # Writer for rotation
if scalewriter is None:
scalewriter = GeomVertexWriter(vdata.modifyArray(2), 2) # Writer for scale
self.instance_parent = NodePath('instances')
for i in range(fromvalue, tovalue):
placeholder = self.instance_parent.attachNewNode(f'Instance_{i}')
x, y, z = sorted_positions[i]
placeholder.setPos(x, y, z)
poswriter.add_data3(x + new_location[0], y + new_location[1], z + new_location[2])
offsetpositionlist.append([x + new_location[0], y + new_location[1], z + new_location[2]])
normal = self.calculate_normal(x, y, heightmapsgotfunc)
# Step 2: Convert normal to Euler angles
heading, pitch, roll = self.normal_to_euler(normal)
placeholder.lookAt(placeholder.getPos() + normal)
h, p, r = placeholder.getHpr()
if not zrotonly:
if p >= 45:
random_heading = random.uniform(0, 360)
# placeholder.setHpr(h + random_heading, 0, 0)
heading, pitch, roll = 0,0,roll+random_heading
placeholder.setHpr(0, 0, r+heading)
# heading, pitch, roll = heading/10,pitch/10,roll+random_heading
else:
random_heading = random.uniform(0, 50)
# placeholder.setHpr(h + 90 + random_heading, p / 3, r)
heading, pitch, roll = heading/2,pitch/2,roll+ 90 +random_heading
placeholder.setHpr(h+roll, p+pitch, r+heading)
else:
random_heading = random.uniform(0, 360)
heading, pitch, roll = heading,pitch,roll+ 90 +random_heading
placeholder.setHpr(h+roll, p+pitch, r+heading)
# h, p, r = placeholder.getHpr()
placeholder.setHpr(h, p-90, r)
# instance = self.tree.instanceTo(placeholder)
# instance.setTag('instance', 'true')
rotwriter.add_data4(heading*45, pitch*45, roll*360, 1)
# rotwriter.add_data4(h, p, r, 1)
scale = random.uniform(0.9, 1.5)
placeholder.setScale(scale)
scalewriter.add_data4(scale, scale, scale, 1)
rotationlist.append([heading*45, pitch*45, roll*360, 1])
scalelist.append([scale, scale, scale, 1])
# Move the whole area to the new location
self.instance_parent.setPos(new_location)
self.instance_parent.reparentTo(self.render)
total_instances += (tovalue - fromvalue)
nodefunc.setInstanceCount(total_instances)
nodefunc.node().setBounds(OmniBoundingVolume())
nodefunc.node().setFinal(True)
return total_instances, poswriter, rotwriter, scalewriter,offsetpositionlist,scalelist,rotationlist
app = MyApp()
app.run()