diff --git a/README.md b/README.md index c548dde..d20b4ba 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Screenshot](docs/screenshot.jpg) (Planet rendered using [Atmosphere Shader v0.4](https://godotengine.org/asset-library/asset/2002)) -Starlight is a Godot addon that renders 100,000 stars in realtime, with +Starlight is a Godot addon that renders 100 000 stars in realtime, with low performance cost. It's an alternative to using a skybox, and also may be relevant to anyone making a space game. @@ -18,23 +18,28 @@ Check out the demo in your web browser: https://tiffnix.com/starlight-demo/ see stars go by. - Exact position, luminosity, and temperature of each star can be configured by you. -- Default star generator is based on real main sequence stars (classes M through O). - Physically based light model: Using a [Point Spread Function (PSF)][1], rather than a texture that grows or shrinks with distance/brightness. - Based on [MultiMeshInstance3D][2] for performance. +- Uses a trick to avoid being clipped by the far plane, to let stars be + very far away. - Works with Forward+, Mobile, and Compatibility renderers. +- Comes with a random star generator based on main sequence stars (classes M through O). [1]: https://en.wikipedia.org/wiki/Point_spread_function [2]: https://docs.godotengine.org/en/stable/classes/class_multimeshinstance3d.html # Usage Guide -To get started, insert `Stars.tscn` into your scene. This will -automatically spawn 100,000 randomly generated stars. +To get started, insert `Stars.tscn` into your scene. By default, nothing +will be visible. You can attach the script `StarGenerator.gd` to +randomly generate stars. The default will be 10 000. -On the instance itself there are some properties you can configure that -affect the star spawning. These are: +## StarGenerator + +This script procedurally generates stars in a ball centered on the +origin. It has these properties: - `size`: Stars are spawned inside of a sphere of this radius. - `star_count`: The number of stars to create. @@ -47,40 +52,91 @@ affect the star spawning. These are: For further customization of the star generator, I recommend editing the script directly. -To make changes to the visual appearance you will need to edit the -Material. You can do this either by opening Stars.tscn, or by clicking -the dropdown arrow on the MultiMesh resource and click "Make unique". -You'll then need to do this again on the Mesh resource inside it. Expand -the Mesh, expand the Material, expand the Shader Parameters section. -Inside here you will find more properties to configure. +## StarManager / Stars.tscn + +There is only 1 script property exported, which is which shader to use. +The default points to `Star.gdshader`. You can fork the shader easily by +changing this. + +The script also forwards the shader parameters which you can edit +directly. + +If you want to load your own star catalog, or use a custom random star +generator, you will need to call `set_star_list()` with an array of `Star`. + +The Star constructor takes 3 arguments: + +1. `position`: 3D position in model space units. +2. `luminosity`: Luminosity in [solar luminosity][3]. Approximately 1.0 for Sol. +3. `temperature`: [Effective temperature][4] in Kelvin. Approximately 5778 for Sol. + +[3]: https://en.wikipedia.org/wiki/Solar_luminosity +[4]: https://en.wikipedia.org/wiki/Effective_temperature + +## Star.gdshader + +The following visual properties are exposed: - `emission_energy` - Multiplier for how bright stars should be. - Generally this is some extremely large number like `50000000000` - + Generally this is some extremely large number like `500000000` - you'll need to add or remove zeros until it looks right. -- `camera_vertical_fov` - The vertical camera FOV. By default in Godot - this is 70, but if you change it you may need to adjust it here. For - example, if you're zooming in the camera, you'll need to adjust this. +- `color_gamma` - How strongly colors should show through. A value of 1 + should be close to real life, while a very high value of 5-10 + resembles what you see in false-color images from telescopes. A value + of 3 or 4 is a good balance. - `billboard_size_deg` - This controls how much of the screen the PSF texture takes up, in degrees. For the default JWST PSF I recommend a - value of around 70. -- `min_size_ratio` - There is a performance optimization where the PSF texture - is cropped for stars that are dim. This will be 99.999% of stars. - Generally set this to some low value like 0.005. -- `debug_show_rects` - This can be useful while tweaking the values of - `billboard_size_deg` and `min_size_ratio`. -- `max_luminosity` - This is the point at which the cropping stops and - the full PSF texture is used. If your PSF looks cut off, you may need - to lower this. + value of around 90. - `meters_per_lightyear` - This is a scaling setting, you'll need to set it depending on how far away you want your stars to be. -- `distance_limit` - This acts as an upper bound on how bright stars can - be. Once you get closer to a star than this distance, it stops getting - any brighter. This setting can be used to prevent blowing out the PSF - texture. +- `luminosity_cap` - This is the maximum brightness a star can have. The + main usage of this is to prevent the PSF texture from being blown out + and showing as a white square. This mainly happens when very close to + stars. - `texture_emission` - This is the actual PSF texture. The default one is the PSF from the James Webb Space Telescope, because it looks cool. There are a few others in the `psf-textures` folder which can be used instead. +- `clamp_output` - Clamps the output from 0 to 1 when enabled. Can be + useful depending on how your HDR is setup. + +In order to have good performance, the PSF texture needs to be cropped +depending on how bright the star is on screen. The majority of stars +only appear as a couple of pixels in size, as opposed to covering almost +the entire screen for a star you're very close to. This cropping +behavior directly affects how much overdraw there is, which can +massively impact performance. You will need to adjust these properties +if you change the PSF texture from the default JWST one. To control this +behavior, the shader has these properties: + +- `min_size_ratio` - This is the minimum size that a star can render at. + This corresponds to the innermost bright spot of the PSF texture, and + is usually a very small value like 0.003. +- `max_luminosity` - This is the point at which the cropping stops and + the full PSF texture is used. To set this value, try to find the point + at which the PSF texture is fully visible, specifically things like + diffraction spikes reach the edge of the texture. Then adjust + max_luminosity until it's just below that point. +- `scaling_gamma` - Diffraction spikes usually fall in brightness + according to distance^2 from the center of the texture, which means a + value of 0.5 is ideal. You may need to use other values depending on + your PSF texture. For a perfect airy disk in particular, the falloff + is faster than quadratic. +- `debug_show_rects` - This can be useful while tweaking any of these + values. It helps visualize whether any stars are being over-cropped, + or if there is too much overdraw in your scene. + +Be careful when tweaking these values. You may want to reduce the star +count to something more manageable like 1000 or 10,000 while doing this. +Setting `scaling_gamma` to 0 by accident, for example, can crash Godot, +your graphics drivers, or even your entire PC. + +The shader also requires knowledge of the camera FOV to work correctly. +StarManager tries to automatically find the Camera3D of the current +viewport, but in some cases this auto-detection may be wrong, and you +will need to edit StarManager.gd for your needs. It also does not +display correctly in the editor due to it using different FOV settings +from the scene. # Credit diff --git a/addons/starlight/Star.gdshader b/addons/starlight/Star.gdshader index 29e3449..0ff7262 100644 --- a/addons/starlight/Star.gdshader +++ b/addons/starlight/Star.gdshader @@ -1,20 +1,24 @@ shader_type spatial; render_mode blend_add,depth_draw_never,cull_front,shadows_disabled,skip_vertex_transform,unshaded; - +// Visual properties: uniform sampler2D texture_emission : hint_default_black; uniform float emission_energy; uniform float billboard_size_deg : hint_range(0, 90) = 90; -uniform float min_size_ratio : hint_range(0, 1.0) = 1.0; -uniform float scaling_gamma : hint_range(0.0, 2.0) = 1.0; -uniform float max_luminosity; -uniform float meters_per_lightyear = 100.0; uniform float luminosity_cap = 1e300; -uniform float camera_vertical_fov = 70; +uniform float meters_per_lightyear = 100.0; uniform float color_gamma : hint_range(0.0, 10.0) = 1.0; -uniform bool debug_show_rects = false; uniform bool clamp_output = false; +// This should really be a godot shader builtin. +uniform float camera_vertical_fov = 70; + +// PSF cropping related uniforms: +uniform float min_size_ratio : hint_range(0, 1.0) = 1.0; +uniform float max_luminosity; +uniform float scaling_gamma : hint_range(0.0, 2.0) = 1.0; +uniform bool debug_show_rects = false; + varying vec3 STAR_COLOR; diff --git a/addons/starlight/StarGenerator.gd b/addons/starlight/StarGenerator.gd index 2b0bed4..fe2f1d6 100644 --- a/addons/starlight/StarGenerator.gd +++ b/addons/starlight/StarGenerator.gd @@ -1,9 +1,14 @@ @tool extends "res://addons/starlight/StarManager.gd" +## Procedurally generates main sequence stars and populates StarManager with them. +## Radius of a sphere in which to place stars. @export var size: float = 5000: set = _set_extents +## Number of stars to generate. @export var star_count: int = 10000: set = _set_star_count +## RNG seed, which can be used to re-roll the random generation. @export var rng_seed: int = 1234: set = _set_rng_seed +## If set to true, a Sol-like star will be placed at 0,0,0. @export var generate_at_origin: bool = false: set = _set_generate_at_origin diff --git a/addons/starlight/StarManager.gd b/addons/starlight/StarManager.gd index 9ea9463..027403e 100644 --- a/addons/starlight/StarManager.gd +++ b/addons/starlight/StarManager.gd @@ -5,6 +5,7 @@ extends Node3D @export var shader: Shader +## Class used to represent stars for StarManager. Passed to set_star_list(). class Star: # Position of the star. var position: Vector3 @@ -19,6 +20,11 @@ class Star: self.temperature = temperature +## Most stars have an "Effective temperature" which is the black body temperature that most closely +## matches their emitted spectra. +## +## This describes the color of the star as a single value, a temperature in Kelvin, from the range +## of ~500 through to 60,000 and beyond. static func blackbody_to_rgb(kelvin): var temperature = kelvin / 100.0 var red @@ -101,11 +107,14 @@ static func blackbody_to_rgb(kelvin): var material: ShaderMaterial var mesh: MultiMesh +# Hide these parameters because they're set by StarManager: var internal_shader_params = { 'camera_vertical_fov': true } +# This forwards the shader parameters, which would otherwise be inaccessible because the Material +# is generated at runtime. func _get_property_list(): var props = [] var shader_params := RenderingServer.get_shader_parameter_list(shader.get_rid()) @@ -137,6 +146,10 @@ func _set(p_key: StringName, value): material.set_shader_parameter(param_name, value) +# In order to render the stars without polluting the .tscn file with MultiMesh buffer data, this +# technique is used of creating the instance and adding it as a child in _init(). This somehow +# doesn't actually add the child to the scene in the editor, even though the code is running as a +# tool script. func _init(): material = ShaderMaterial.new() material.shader = shader @@ -174,6 +187,8 @@ func set_star_list(star_list: Array[Star]): func _process(_delta): var camera = get_viewport().get_camera_3d() var fov = 70 + # The camera can be null if the scene doesn't have one set. + # This value will also not match the editor camera. if camera: fov = camera.fov diff --git a/addons/starlight/Stars.tscn b/addons/starlight/Stars.tscn index 60088f5..d030da6 100644 --- a/addons/starlight/Stars.tscn +++ b/addons/starlight/Stars.tscn @@ -9,12 +9,12 @@ script = ExtResource("1_u877o") shader = ExtResource("2_6rhhm") shader_params/emission_energy = 1e+07 shader_params/billboard_size_deg = 90.0 -shader_params/min_size_ratio = 0.003 -shader_params/scaling_gamma = 0.5 -shader_params/max_luminosity = 100000.0 -shader_params/meters_per_lightyear = 100.0 shader_params/luminosity_cap = 4e+06 +shader_params/meters_per_lightyear = 100.0 shader_params/color_gamma = 4.0 -shader_params/debug_show_rects = false shader_params/clamp_output = false +shader_params/min_size_ratio = 0.003 +shader_params/max_luminosity = 100000.0 +shader_params/scaling_gamma = 0.5 +shader_params/debug_show_rects = false shader_params/texture_emission = ExtResource("3_wqe40")