Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for exporing SkyBoxes #239

Merged
merged 2 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [Exporting Node Animations](#exporting-node-animations)
- [Exporting for Physics](#exporting-for-physics)
- [Exporting Vertex Colors](#exporting-vertex-colors)
- [Exporting SkyBoxes](#exporting-skyboxes)
- [Level of Detail (LOD)](#level-of-detail-lod)
- [Mesh Previewer](#mesh-previewer)
- [About](#about)
Expand Down Expand Up @@ -261,6 +262,13 @@ Check out the [Exporting for Physics](Physics.md) tutorial to see some technique
`blender2ogre` can import and export Vertex Colors, a way to assign colors to your mesh by vertex. It is also possible to asign alpha values to each vertex for transparency or other uses.
Check out the [Vertex Colors](VertexColors.md) tutorial to see how to create the Vertex Colors in Blender and how to export them.

### Exporting SkyBoxes
![skyboxes1.jpg](images/skyboxes/skyboxes1.jpg)

`blender2ogre` can generate SkyBoxes from HDRi Maps and export them in a format that can be used in OGRE, that is a Cube Map.
Check out the [SkyBoxes](SkyBoxes.md) tutorial to see how to create import the Environment Map in Blender and how to export it.


### Level of Detail (LOD)
Level of Detail or LOD is an optimization technique supported by OGRE, where meshes that are far away from the camera are replaced by meshes with lower vertex count.
Because you can get away with less detailed models when they are in the distance this optimizacion technique is very common, especially in games.
Expand Down
66 changes: 66 additions & 0 deletions SkyBoxes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

# Exporting SkyBoxes

![skyboxes1.jpg](images/skyboxes/skyboxes1.jpg)

## Documentation:
- https://en.wikipedia.org/wiki/Skybox_(video_games)
- https://ogrecave.github.io/ogre/api/latest/class_ogre_1_1_scene_manager.html#a370c23fff9cc351b150632c707a46700
- https://docs.blender.org/manual/en/latest/render/lights/world.html

## Introduction
It is possible to export an HDRi map as a Cube Map that can be used in OGRE for SkyBoxes or environment maps.

## Setting the Environment Map in Blender
1) Go to `World Properties` Tab \
![skyboxes2.png](images/skyboxes/skyboxes2.png)

2) Then under "Surface", make sure that `Use Nodes` is checked. Surface should say "Background".

3) Then, where it says `Color` click on the white dot and a menu should appear to select the texture for the environment map. \
![skyboxes3.png](images/skyboxes/skyboxes3.png)

4) Open a Picture or select one from the Blender cache. \
![skyboxes4.png](images/skyboxes/skyboxes4.png)

5) Then select `File`->`Export`->`Ogre3D (.scene and .mesh)` \
![skyboxes5.png](images/skyboxes/skyboxes5.png)

6) In the `General` Settings, make sure to enable the option `Export SkyBox` and set the resolution to some power of 2 value (e.g.: 512, 1024, 2048, etc.). \
The higher the resolution the more time it will take for the exporting process. \
![skyboxes6.png](images/skyboxes/skyboxes6.png)

The exporter will create six images, one for each of the faces of the "SkyBox":
- front: "<texture_name>_fr.png"
- back: "<texture_name>_bk.png"
- right: "<texture_name>_rt.png"
- left: "<texture_name>_lf.png"
- top: "<texture_name>_up.png"
- bottom: "<texture_name>_dn.png"
\
![skyboxes7.png](images/skyboxes/skyboxes7.png)

And a material with the same name as the environment texture.

## Using the SkyBox in OGRE
To load the SkyBox in OGRE it can be done with the DotScene Plugin, where you can use the .scene exported by `blender2ogre`.
\
The exported scene has the following section:
```
...
<environment >
<colourBackground b="0.050876" g="0.050876" r="0.050876" />
<skyBox active="true" material="symmetrical_garden_02" />
</environment>
...
```

To use it in code you can do the following:
```
String material = "symmetrical_garden_02";
Real distance = 5000;
bool drawFirst = true;
bool active = true;
String groupName = "Scene";
mSceneMgr->setSkyBox(true, material, distance, drawFirst, rotation, groupName);
```
Binary file added images/skyboxes/skyboxes1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/skyboxes/skyboxes7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions io_ogre/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
'FORCE_LIGHTS' : True,
'NODE_ANIMATION' : True,
#'NODE_KEYFRAMES' : False,
'EXPORT_SKYBOX': False,
'SKYBOX_RESOLUTION': 2048,

# Materials
'MATERIALS' : True,
Expand Down
240 changes: 218 additions & 22 deletions io_ogre/ogre/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
importlib.reload(mesh)
importlib.reload(skeleton)

import bpy, mathutils, os, getpass, math, logging
import bpy, mathutils, os, getpass, math, logging, datetime
from os.path import join
from . import material, materialv2json, node_anim, mesh, skeleton
from .. import bl_info, config, util
from ..report import Report
from ..util import *
from ..xml import *
from .material import *
from .materialv2json import *
Expand Down Expand Up @@ -181,7 +180,7 @@ def _flatten( _c, _f ):
logger.error("Unknown converter type '{}', will not generate materials".format(converter_type))
Report.errors.append("Unknown converter type '{}', will not generate materials".format(converter_type))

doc = ogre_document(materials)
doc = ogre_document(materials, path)

mesh_collision_prims = {}
mesh_collision_files = {}
Expand Down Expand Up @@ -393,7 +392,7 @@ def _ogre_node_helper( doc, ob, prefix='', pos=None, rot=None, scl=None ):

return o

def ogre_document(materials):
def ogre_document(materials, path):
now = time.time()
doc = RDocument()
scn = doc.createElement('scene')
Expand All @@ -414,49 +413,246 @@ def ogre_document(materials):

nodes = doc.createElement('nodes')
doc._scene_nodes = nodes
extern = doc.createElement('externals')
environ = doc.createElement('environment')
for n in (nodes,extern,environ):
external = doc.createElement('externals')
environment = doc.createElement('environment')
for n in (nodes, external, environment):
scn.appendChild( n )

# Extern files
# External files
for mat in materials:
if mat is None: continue
item = doc.createElement('item')
extern.appendChild( item )
external.appendChild( item )
item.setAttribute('type', 'material')
a = doc.createElement('file')
item.appendChild( a )
a.setAttribute('name', '%s.material'%material.material_name(mat))
a.setAttribute('name', '%s.material' % material.material_name(mat))

# Environ settings
# Environment settings
world = bpy.context.scene.world
if world: # multiple scenes - other scenes may not have a world
_c = [ ('colourBackground', world.color)]
for ctag, color in _c:
a = doc.createElement(ctag); environ.appendChild( a )
a = doc.createElement(ctag)
environment.appendChild( a )
a.setAttribute('r', '%3f' % color.r)
a.setAttribute('g', '%3f' % color.g)
a.setAttribute('b', '%3f' % color.b)

if world and world.mist_settings.use_mist:
a = doc.createElement('fog'); environ.appendChild( a )
a.setAttribute('linearStart', '%6f' % world.mist_settings.start )
fog = doc.createElement('fog')
environment.appendChild( fog )
fog.setAttribute('linearStart', '%6f' % world.mist_settings.start )
mist_falloff = world.mist_settings.falloff
if mist_falloff == 'QUADRATIC': a.setAttribute('mode', 'exp') # on DTD spec (none | exp | exp2 | linear)
elif mist_falloff == 'LINEAR': a.setAttribute('mode', 'linear')
else: a.setAttribute('mode', 'exp2')
#a.setAttribute('mode', world.mist_settings.falloff.lower() ) # not on DTD spec
a.setAttribute('linearEnd', '%6f' % (world.mist_settings.start + world.mist_settings.depth))
a.setAttribute('expDensity', world.mist_settings.intensity)

c = doc.createElement('colourDiffuse'); a.appendChild( c )
if mist_falloff == 'QUADRATIC': fog.setAttribute('mode', 'exp') # on DTD spec (none | exp | exp2 | linear)
elif mist_falloff == 'LINEAR': fog.setAttribute('mode', 'linear')
else: fog.setAttribute('mode', 'exp2')
#fog.setAttribute('mode', world.mist_settings.falloff.lower() ) # not on DTD spec
fog.setAttribute('linearEnd', '%6f' % (world.mist_settings.start + world.mist_settings.depth))
fog.setAttribute('expDensity', world.mist_settings.intensity)

c = doc.createElement('colourDiffuse')
fog.appendChild( c )
c.setAttribute('r', '%3f' % color.r)
c.setAttribute('g', '%3f' % color.g)
c.setAttribute('b', '%3f' % color.b)

skybox_name = dot_scene_skybox_export( path )
if skybox_name is not None:
skybox = doc.createElement('skyBox')
environment.appendChild( skybox )
skybox.setAttribute('material', skybox_name )
#skybox.setAttribute('distance', '5000')
#skybox.setAttribute('drawFirst', 'true')
skybox.setAttribute('active', 'true')

return doc

def dot_scene_skybox_export( path ):
if config.get('EXPORT_SKYBOX') is False:
return None

skybox_name = None
skybox_resolution = config.get('SKYBOX_RESOLUTION')
skybox_distance = 5000
skybox_imagepath = None
collection_name = "OgreSkyBox"
#path = "D:\\tmp\\SkyBox"

# Get the current scene
scene = bpy.context.scene

# Get the world used by the scene
world = scene.world

# Ensure that node use is enabled for the world
if world.use_nodes:
# Get the node tree of the world
nodes = world.node_tree.nodes

# Find the Background node (usually named 'Background')
background_node = nodes.get('Background')
if background_node:
# Access the 'Color' input
color_input = background_node.inputs['Color']

# Check if there is a link and if it's from a valid node
if color_input.is_linked:
linked_node = color_input.links[0].from_node
# Check if the node is an environment texture
if linked_node.type == 'TEX_ENVIRONMENT':
# Output some information about the image
logger.debug("SkyBox: Image linked as background:")
logger.debug("SkyBox: - Image Name: %s" % linked_node.image.name)
logger.debug("SkyBox: - Image Filepath: %s" % linked_node.image.filepath)
else:
logger.warning("Unable to create SkyBox: Linked node is not an environment texture. Node type: %s" % linked_node.type)
Report.warnings.append("Unable to create SkyBox: Linked node is not an environment texture. Node type: %s" % linked_node.type)
return None
else:
# Retrieve the static color if no image is linked
background_color = color_input.default_value
logger.warning("Unable to create SkyBox: Found background color instead of environment texture")
Report.warnings.append("Unable to create SkyBox: Found background color instead of environment texture")
return None
else:
logger.warning("Unable to create SkyBox: No Background node found")
Report.warnings.append("Unable to create SkyBox: No Background node found")
return None
else:
logger.warning("Unable to create SkyBox: World nodes are not enabled.")
Report.warnings.append("Unable to create SkyBox: World nodes are not enabled.")
return None

skybox_imagepath = linked_node.image.filepath
skybox_name = os.path.splitext(linked_node.image.name)[0]

logger.info("* Generating SkyBox: %s" % skybox_name)
logger.info("+ From Image: %s" % skybox_name)
logger.info("+ With resolution: %s" % skybox_resolution)

# Create a collection to render the SkyBox
skybox_collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(skybox_collection)

# Create camera to render the SkyBox
camera_name = "OgreSkyBox_CAM"

camera = bpy.data.cameras.new(camera_name)
camera_ob = bpy.data.objects.new(camera_name, camera)
#bpy.context.scene.objects.link(camera_ob)

#camera_ob = bpy.data.objects['CameraY']
skybox_collection.objects.link(camera_ob)

scene_camera_orig = scene.camera
scene.camera = camera_ob

# Set SkyBox camera settings
camera_ob.data.lens_unit = 'FOV'
camera_ob.data.angle = math.radians(90)

# Depth of Field
#camera_ob.data.dof.use_dof = True
#camera_ob.data.dof.focus_distance = 10.0

camera_ob.data.clip_start = 0.1
camera_ob.data.clip_end = 1000

# Backup scene settings
scene_res_x_orig = scene.render.resolution_x
scene_res_y_orig = scene.render.resolution_y
scene_rdr_perc_orig = scene.render.resolution_percentage
scene_filepath_orig = scene.render.filepath
scene_use_nodes_orig = scene.use_nodes
scene_file_format_orig = scene.render.image_settings.file_format

# Set scene settings for SkyBox rendering
scene.render.resolution_x = skybox_resolution
scene.render.resolution_y = skybox_resolution
scene.render.resolution_percentage = 100
scene.use_nodes = True
scene.render.image_settings.file_format = 'PNG'

# Create SkyBox camera orientations
front = mathutils.Euler((math.radians(90), 0, 0), 'XYZ')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are the orientations used internally by Ogre:
https://github.com/OGRECave/ogre/blob/6bcf1d7d4c510104f794d779e63d330c2c8d5ebb/OgreMain/src/OgreCompositorChain.cpp#L267

the blender orientations might be different due to axis flipping. Maybe we should disable axis flipping when it comes to .scene contents altogether for simplicity.

Copy link
Collaborator Author

@sercero sercero Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I'll see how to acomodate the axis swapping with this.

In my opinion the current systems works good, because when you export a .scene what you get is what you see in Blender. Things have the "same" position and orientation, at least visually. And to me and probably most people (I guess) that is the most intuitive output.
Besides this exporter has worked like that for a loong time.

The only issue is that by default lights in Blender point towards the "bottom" or down while in OGRE by default they point towards the "back" of the screen, so they need a correction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I'll see how to acomodate the axis swapping with this.

I just drew the results of axis swapping, and they are just all rotations. So you should be able to use orientation parameter of the skybox.

Besides this exporter has worked like that for a loong time.

I think it has never worked correctly :P see #240 where I tried to explain the issue in detail

back = mathutils.Euler((math.radians(90), 0, math.radians(180)), 'XYZ')
right = mathutils.Euler((math.radians(90), 0, -math.radians(90)), 'XYZ')
left = mathutils.Euler((math.radians(90), 0, math.radians(90)), 'XYZ')
top = mathutils.Euler((math.radians(180), 0, 0), 'XYZ')
bottom = mathutils.Euler((0, 0, 0), 'XYZ')

orientations = {"fr": front, "bk": back, "rt": right, "lf": left, "up": top, "dn": bottom}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should rather use the alternative naming conventions, as those are clearer:
https://github.com/OGRECave/ogre/blob/6bcf1d7d4c510104f794d779e63d330c2c8d5ebb/OgreMain/src/OgreTexture.cpp#L35

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean this ones: "_px", "_nx", "_py", "_ny", "_pz", "_nz"?


# Render only the SkyBox collection
for collection in bpy.data.collections:
if collection != skybox_collection:
collection.hide_render = True
else:
collection.hide_render = False

# Render one side of the skybox for each orientation
i = 0
for orientation in orientations:
camera_ob.rotation_euler = orientations[orientation]
image_name = os.path.join(path, skybox_name + "_" + orientation)
scene.render.filepath = image_name
logger.info("Exporting SkyBox image: %s" % image_name)
scene.render.use_compositing = True
bpy.ops.render.render(write_still = True)
#bpy.ops.render.render(animation=False, write_still=False, use_viewport=False, layer='', scene='')
#progressbar.update(i)
i = i + 1
percent = len(orientations) / i
bpy.context.window_manager.progress_update(percent * 100)

# Restore scene settings
scene.render.resolution_x = scene_res_x_orig
scene.render.resolution_y = scene_res_y_orig
scene.render.resolution_percentage = scene_rdr_perc_orig
scene.render.filepath = scene_filepath_orig
scene.use_nodes = scene_use_nodes_orig
scene.camera = scene_camera_orig
scene.render.image_settings.file_format = scene_file_format_orig

# Restore collection rendering
for collection in bpy.data.collections:
collection.hide_render = False

# Remove SkyBox camera
bpy.data.cameras.remove(camera)

# Destroy the collection created to render the SkyBox
bpy.context.scene.collection.children.unlink(skybox_collection)
bpy.data.collections.remove(skybox_collection)

w = util.IndentedWriter()
with w.iword('material').word(skybox_name).embed():
with w.iword('technique').embed():
with w.iword('pass').embed():
w.iline('lighting off')
w.iline('depth_write off')
with w.iword('texture_unit').embed():
w.iword('texture').word(skybox_name + ".png").word("cubic").nl()
w.iline('tex_address_mode clamp')
material_text = w.text

try:
mat_file_name = join(path, skybox_name + ".material")
with open(mat_file_name, 'wb') as fd:
logger.info("SkyBox: Exporting material to: %s" % mat_file_name)
b2o_ver = ".".join(str(i) for i in bl_info["version"])
fd.write(bytes('// generated by blender2ogre %s on %s\n' % (b2o_ver, datetime.now().replace(microsecond=0)), 'utf-8'))
fd.write(bytes(material_text, 'utf-8'))
except Exception as e:
logger.error("Unable to create SkyBox material file: %s" % mat_file_name)
logger.error(e)
Report.errors.append("Unable to create SkyBox material file: %s" % mat_file_name)
return None

return skybox_name


# Recursive Node export
def dot_scene_node_export( ob, path, doc=None, rex=None,
exported_meshes=[], meshes=[], mesh_collision_prims={}, mesh_collision_files={},
Expand Down
Loading
Loading