Files
fs2open.github.com/code/model/modelrender.cpp
T
b26b9cf743 Add Vulkan rendering backend, with HDR10 output and raytraced shadows (#7554)
* Shadow overhaul round 2: PCSS, dynamic cascades, cockpit/instanced shadows

Squashed snapshot of lafiel/shadow_overhaul_2's continuation work
(d3922aa87..010f9d2d9), which has no separate upstream PR of its own.

* Add Vulkan rendering backend

Squashed snapshot of the Vulkan backend as vendored into this branch
(ce2d1e85f..6ce539463, authored by Mara van der Laan with build/CI
fixes by Taylor Richards). This work has its own upstream PR #7553
(notimaginative:vulkan-pr-new) and is not otherwise original to true-hdr.

* Add true HDR output, RT shadows, and PCSS shadows for Vulkan

Squashed snapshot of true-hdr's own integration work: true HDR
framebuffer output, wiring the shadow overhaul into the Vulkan
backend (raytraced + cascaded shadow maps), logging refactors, log
filter UI, and CI/build fixes.

* refactor: update Vulkan logging calls to use `nprintf` for scoped logging

* fix: correct shadow cascade count in Vulkan logging calls

* fix: fall back to PATH-resolved ccache when CCACHE_PATH is stale

CI hardcodes /usr/local/bin/ccache, but macOS runners on Apple Silicon
hosts install Homebrew (and ccache) under /opt/homebrew instead,
causing all four Mac configs to fail with "No such file or directory".

* Fix vulkan texture readback to correctly handle transparent pipelines

* Put the pixel swizzles into a util file

* OOB safety check

* Fix fog handling

* Fix MSAA in Vulkan

* fix: enable SMAA for vulkan

* adapt the post-processing AA shaders to hdr

* fix clang-tidy warnings: concatenate nested namespaces, static methods

pixel_swizzle.{h,cpp}: collapse `namespace graphics { namespace util {`
into `namespace graphics::util {` (modernize-concat-nested-namespaces).

VulkanState.h/VulkanDraw.h: getClearColor()/setClearColor() only touch
gr_screen and other globals, no instance state, so make them static
(readability-convert-member-functions-to-static) and drop the
top-level const on getClearColor()'s return type
(readability-const-return-type).

* fix clang-tidy warning

* Extend raytraced shadows to all directional lights

RT shadows previously only shadowed the first directional light in the
scene, even though multi-sun missions push more than one directional
light into the engine. The TLAS raytraced shadows query against is
already light-agnostic, so this extends both the deferred lighting
pass and the forward MODEL shader to shadow every directional light
(capped by a new Max Raytraced Shadow Lights option, configurable via
the in-game Options menu or the Lab's Scene rendering options).

Cascaded shadow maps are untouched: they're built for a single light
only, so the deferred pass still limits itself to one shadowed
directional light whenever CSM (not raytracing) is the active method.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016DA6JHJ3twz2oHGQcZ1Udc

* refactor: improve shader flag handling and render target finalization in Vulkan backend

- Consolidate shader flag setup logic in VulkanPostProcessingLighting to simplify configuration.
- Fix dead field usage in VulkanDraw by ensuring correct image view selection for environment maps.
- Add mip chain regeneration in VulkanRenderer::endRenderTarget to ensure correct mip levels for cubemaps.

* fix: ensure correct mip chain initialization in cubemap rendering

- Add mip chain generation to VulkanDraw for cubemaps with multiple mip levels.
- Fix potential issue with uninitialized mip levels above level 0 in deferred-f.sdr sampling paths.

* fix: don't crash on startup when environment locale is unavailable

std::locale("") pulls in the process's environment locale categories
(LC_CTYPE, LC_MONETARY, LC_PAPER, etc.), and throws std::runtime_error
if any of them names a locale not installed on the host (e.g.
mismatched regional-format overrides). Since SCP_default_locale is
constructed during static initialization, before main() and before any
of our own exception handling exists, an uncaught throw here aborted
the whole process before the game even started. Fall back to the
classic "C" locale instead, which is sufficient since this locale is
only ever used for ASCII case-folding (SCP_toupper/tolower/isdigit).

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* `feat: add raytraced shadows for local lights and optimize TLAS management`

* Make the Commandline authoritative for -res and -vulkan / -opengl

* address BMagnu's comments on the original PR

* update prebuilt version hash to 62c89ef6

* refactor: standardize graphics API handling and document Vulkan limitations in qtFRED

- Updated `AboutDialogModel.cpp` to use `GraphicsAPI` enum for better clarity.
- Ensured qtFRED falls back to OpenGL when Vulkan is requested.
- Added detailed documentation in `qtfred/README.md` regarding Vulkan backend limitations and requirements.

* chore: include cmdline library in main and mission management modules

* docs: add known issue for blank render viewport under Wayland in README.md

* fix CI issues

* chore: add `GR_DEBUG_SCOPE` markers across Vulkan post-processing and rendering paths for improved debugging

* feat: improve Vulkan HDR pipeline with separate encode paths for SDR and HDR10

- Added an HDR10-specific output encode pass in `VulkanRenderer` and `VulkanPostProcessor` that applies PQ/BT.2020 transfer.
- Introduced an optimized SDR fallback path with direct blit support for devices that can skip shader draws.
- Updated `m_sceneLdrCompressed` to provide [0,1]-clamped proxies for HDR edge detection.
- Refactored `copyImageToImage` and added `blitImageToImage` to allow format conversions and optimized transfers.
- Replaced `hdr_mode` with explicit HDR10 flags and data structures for clearer state handling.

* refactor: encapsulate SMAA processing and refactor VulkanPostProcessor

- Moved SMAA-specific logic into `VulkanSMAA` class for better separation of concerns.
- Simplified `initSMAA`, `shutdownSMAA`, and `executeSMAA` logic by leveraging `VulkanLDR` and `PostProcessContext` abstractions.
- Improved initialization and cleanup code with clearer resource lifecycle management.
- Updated debug markers and messages to reference `VulkanSMAA`.
- Enhanced flexibility of fullscreen triangle rendering API with additional parameters (`sampleCount`, `bindGlobalSet`).

* Update Vulkan porting credits in AboutDialogModel

* fix CI issues again

* fix: correct Y-flip in viewport setup for Vulkan light volumes

- Adjusted viewport height to negative value to account for G-buffer render pass's negative Y-axis convention.
- Ensured proper alignment of point and tube light volumes with actual light positions.

* feat: add user-adjustable gamma correction to SDR and HDR10 output paths

- Added gamma correction to HDR10 path, applying user settings before PQ/BT.2020 encode.
- Replaced the SDR blit-only path with an encode pass that applies gamma correction, ensuring consistent behavior across devices.
- Refactored VulkanPostProcessor and associated shaders to support gamma slider adjustments for both SDR and HDR output.

* fix: ensure proper uniform and cull mode setup for Vulkan sphere rendering

- Added uniform buffer setup and default material uniforms in `vulkan_draw_sphere`.
- Disabled face culling in material definition for sphere rendering to align behavior with OpenGL.

* Ensure memory flushes after UBO updates and add explicit pipeline barrier for MoltenVK compatibility.

* fix: add clear pass for SMAA edge detection to prevent persistent artifacts

* feat: add Vulkan debug-utils messenger, opt-in sync validation, frame diagnostics

Groundwork for the renderer-review fix series (.agents/plans/
vulkan-renderer-review-fixes.md, Phase 0):

- Register a VK_EXT_debug_utils messenger (preferred) instead of only the
  deprecated VK_EXT_debug_report callback, which is kept as a fallback for
  loaders without debug-utils. The messenger config is also chained into
  instance creation so vkCreateInstance-time messages are captured.
  Validation errors/warnings now go to the main log via mprintf.
- New -gr_sync_validation cmdline flag (implies -gr_debug): chains
  VkValidationFeaturesEXT with synchronization validation into instance
  creation when VK_LAYER_KHRONOS_validation is available. This is the
  regression net for the synchronization fixes in the following commits.
- Frame stats now report descriptor sets/writes allocated per frame and the
  total pipeline count (descriptor-manager counters; DescriptorWriter::flush
  moved out-of-line to report into the manager).

Co-Authored-By: Claude Fable 5 <[email protected]>

* refactor: unify Vulkan render-pass begins, clamp in-pass clears, add per-frame UBO ring

Phase 1 of the renderer-review fix series: shared helpers that the
correctness fixes build on.

- New VulkanRenderer::beginTrackedRenderPass(PassBeginDesc): every render-pass
  begin on the frame command buffer now records vkCmdBeginRenderPass and
  updates the state tracker (pass, color attachment count, sample count,
  render area, viewport) in one place. Converted: setupFrame,
  beginSceneRendering (both branches), resumeSceneRendering,
  endSceneRendering, beginRenderTarget (viewport kept for gr_set_viewport),
  resumeSwapChainPass, plus new resumeScenePassAfterCopy() shared by
  copyEffectTexture/copySceneDepthForParticles and
  rebeginSwapChainPassAfterReadback() shared by both readbackFramebuffer
  exits (~200 lines of duplicated begin/clear/viewport blocks removed).
- The state tracker records the active render area; all four
  vkCmdClearAttachments sites clamp their rects against it and skip empty
  results. Previously gr_screen-sized clear rects could exceed a smaller
  off-screen render target's render area, which is invalid API usage.
- New PerFrameUboRing utility (VulkanPerFrameUbo.h/.cpp): a persistently
  mapped uniform buffer with one slot region per frame in flight and a
  self-flushing alloc(). Used by the next commit to fix the cross-frame
  UBO races.

Deliberate behavior deltas (all safe-direction): the helper always sets
colorAttachmentCount/sampleCount (some sites previously inherited stale
values), and resumeSwapChainPass/resumeSceneRendering now also reset the
standard flipped viewport instead of leaving the previous one.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix: eliminate Vulkan cross-frame UBO/buffer races, make gr_sync honest

Phase 2 of the renderer-review fix series: with two frames in flight, several
host-visible resources were rewritten by the CPU while the previous frame's
GPU work could still be reading them.

- The post-processing scratch UBO is now a PerFrameUboRing (per-frame-in-
  flight slot regions, persistently mapped, self-flushing alloc). All the
  per-pass map/unmap/cursor bookkeeping in bloom/LDR/fog/SMAA is deleted;
  the cursor resets exactly once per frame via the new
  VulkanPostProcessor::beginFrame() hook in setupFrame.
  This also fixes two latent bugs: (a) fog consumed scratch slots mid-scene
  and bloom's later same-frame cursor reset overwrote them before submission
  whenever fog+bloom were both active; (b) volumetric_fog_data (288 bytes)
  overflowed the 256-byte slots - the memcpy spilled into the neighboring
  slot and the shader's bound range ended before the aspect/fov/noise
  fields. Slots are now 512 bytes (static_assert at the fog call site) and
  the per-frame budget is 32 slots.
- The dedicated tonemap, output-encode, and MSAA-resolve UBOs (each a single
  instance rewritten per frame, i.e. racy; the MSAA one also never flushed)
  are deleted entirely - those passes build their structs on the stack and
  allocate from the scratch ring.
- Static-buffer rewrites orphan instead of racing: getVkBuffer() stamps a
  last-used frame number, and a full-content updateBufferData() on a Static
  buffer used within the last MAX_FRAMES_IN_FLIGHT frames defers the old
  VkBuffer to the deletion queue and writes a fresh one. Offset writes
  (GPU-heap appends to fresh regions) and PersistentMapping buffers (the
  engine holds their mapped pointer and syncs via gr_sync) are exempt.
- gr_sync_wait now honors its contract: VulkanRenderFrame::waitForFinish
  takes a timeout and reports expiry without consuming the fence;
  VulkanRenderer::waitForFrame returns false for fences taken during the
  still-recording frame (previously it waited on a stale or idle slot and
  reported success) and resolves the frame->slot mapping correctly. This
  makes UniformBufferManager's segment-fence throttling actually protective
  on Vulkan for the first time.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Refactor framebuffer and target lifecycle: centralized creation/resizing logic, improved resource management across Vulkan post-processing subsystems.

* Improve Vulkan cross-frame sync with explicit access masks and validation updates

- Unified Vulkan validation flag handling in setup, separating engine debug output from synchronization validation.
- Specified access masks and stage flags to address cross-frame WRITE_AFTER_WRITE vulnerabilities, notably in G-buffer, post-processing, and MSAA resolve paths.
- Adjusted subpass dependencies and image layout transitions to prevent read-after-write and write-after-write hazards, detected using `-gr_sync_validation`.
- Fixed uniform buffer and barrier transitions to ensure proper ordering in render passes across frames.

* Refactor Vulkan render-pass dependencies and sync stages for clarity and future testing

- Clarified dependency logic for cross-pass and cross-frame synchronization, ensuring explicit stage and access flag comments match observed behavior.
- Adjusted subpass dependencies to decouple render-pass compatibility definitions from dependency assumptions.
- Highlighted MoltenVK-specific issues and retained workarounds with notes for re-testing on macOS hardware.
- Updated synchronization barriers and validation notes to address historical inconsistencies across platforms.

* Remove transfer queue support; consolidate all operations into the graphics queue

- Removed transfer queue handling, simplifying buffer and queue logic to use the graphics queue for all transfer operations.
- Updated buffer sharing modes and removed related code paths and parameters for transfer queue families.
- Introduced exclusive buffer access, noting async transfer and queue ownership transfers are future work.
- Limited enabled device features to only those actively used by the renderer, reducing unnecessary feature requests (e.g., robustBufferAccess).

* Refactor frame advancement and descriptor management in VulkanRenderer

- Removed `endFrame()` from `VulkanDescriptorManager`, replacing it with `setCurrentFrame(frameIndex)` for explicit frame synchronization.
- Centralized frame index advancement within `VulkanRenderer` to ensure consistency across components.
- Invalidated cached descriptor set bindings at render-pass boundaries to handle raw recorder staleness correctly.
- Optimized pipeline descriptor rebind logic by removing redundant clears during pipeline changes.

* Refactor `PipelineConfig::hash` and Global descriptor set management for improved hashing and memoization efficiency

- Replaced inefficient shift-based hashing in `PipelineConfig::hash` with a robust `hashCombine` strategy to minimize bucket collisions.
- Introduced `maskBits` helper to streamline bitmask creation for boolean fields.
- Implemented memoization for Global (Set 0) descriptor sets, reducing redundant rebuilds across frames and improving performance.
- Added dirty state tracking (`m_globalSetDirty`) and invalidation logic for Global set updates following relevant input changes (e.g., TLAS or UBO modifications).

* Refactor Vulkan post-processing output logic: extracted shared `encodeToSwapChainPass` method to streamline HDR and SDR output paths.

* Optimize Vulkan descriptor set management with per-frame memoization

- Added memoization for Material (Set 1) and PerDraw (Set 2) descriptor sets to reuse cached sets across frames if inputs remain unchanged.
- Implemented precise input tracking for Material and PerDraw descriptor sets to detect and rebuild only when necessary.
- Reduced redundant Vulkan descriptor allocations and writes, improving performance on repeated or batched draw calls.

* Fix Vulkan access masks for G-buffer and scene pass transitions

- Added `eColorAttachmentRead` to access masks in multiple barriers to ensure proper synchronization for load operations (e.g., `loadOp=eLoad`) across render passes.
- Prevented `READ_AFTER_WRITE` synchronization validation issues in deferred and multi-pass rendering scenarios.
- Updated comments to clarify the reasoning behind the access mask changes and load dependencies.

* Refactor Vulkan resource management and indexed draw logic

- Consolidated destruction queue methods in `VulkanDeletionQueue` using a shared `queueHandle` template to reduce redundancy.
- Introduced `ModelDrawParams` structure and `computeModelDrawParams` function to unify indexed draw parameters for models and shadows.
- Simplified draw logic by eliminating repeated calculation of indexed draw parameters and improving code reuse.

* Add `releaseAnimationSlotRef` for ref-count handling of shared animation texture arrays

- Introduced `releaseAnimationSlotRef` to manage reference counting for shared animation texture-array slots, preventing premature destruction of shared resources.
- Updated `flushTextures` and related methods to integrate `releaseAnimationSlotRef` for shared slot cleanup.
- Refactored staging buffer creation into `createStagingBuffer` for 3D texture uploads and shared path consistency.
- Enhanced texture upload logic with improved tracking and logging for re-upload cases, aiding preloading gap analysis.
- Added instrumentation for on-demand mid-frame texture uploads in `FrameStats`.

* Refactor Vulkan resource and texture management, extend debug logging

- Updated smart pointer usage for consistency across Vulkan resource managers (`std::make_unique`).
- Removed redundant `cubeImageView` member and related cleanup logic; unified sampling view management.
- Added per-instance debug log counters (avoiding static state) for Vulkan texture uploads and bindings, improving visibility during initialization and run-time.
- Optimized `VulkanRenderer` property/feature getters by caching physical device properties and features during device creation.
- Improved memory region flushing logic for out-of-date offsets, aligning with Vulkan Memory Allocator behavior.
- Refactored render pass state tracking to reapply dynamic state reliably after mid-frame render passes.
- Enhanced validation and error handling for mismatched update texture formats.

* fix CI build warning

* Fix on-demand YUV texture locking and set alpha uniform for movie rendering in Vulkan

- Corrected bitmap lock type from `BMP_TEX_OTHER` to `BMP_AABITMAP` for YUV textures during on-demand loading.
- Updated movie rendering to bind alpha from the `MovieData` UBO (set 2, binding 4) for Vulkan, aligning behavior with OpenGL implementation.

* revert inadvertent changes introduced by a squash commit

* address ci issues

* remove wip stuff from code comments that make no sense to people who aren't me

* Remove unused LD_LIBRARY_PATH export and fix shader shadow map sampling.

* Refactor batched rendering uniform setup in Vulkan

- Removed unused fields from `genericData` uniform block, simplifying shader definitions.
- Updated Vulkan material uniform handling to align with OpenGL's batched rendering logic.
- Introduced `batched_data` struct for streamlined UBO updates, improving maintainability and clarity.

* Add `-hdr` command-line option for HDR nits configuration

- Introduced `-hdr` parameter to set paper white and peak luminance in nits, overriding in-game HDR settings.
- Added input validation with error feedback for incorrect format.

* Remove unused `MODEL_SDR_FLAG_SHADOW_MAP` macro from shader flags

* clean up unrelated changes

* Invalidate Vulkan state tracker bindings after external command buffer modifications

- Added `invalidateExternalBindings()` to `VulkanStateTracker` to handle stale pipelines, descriptors, and dynamic states caused by external Vulkan commands (e.g., ImGui renders mid-pass).
- Updated ImGui rendering flow to call `invalidateExternalBindings()` post-draw, ensuring consistent pipeline state for subsequent engine renders.
- Adjusted image usage flags to account for `eTransferSrc` in texture readbacks.
- Fixed barrier `dstAccessMask` and pipeline stage flags to ensure proper synchronization for image transitions.

* Add a few more generically useful lines to the log

* Simplify Qt deployment logic and improve handling for system-installed Qt on Linux

* add cmdline headers to make qtFred compile

* use VK_KHR_synchronization2 for sync and barrier purposes

* Simplify and clean up Qt deployment logic in CMake for qtfred

* update get_bitmap_from_texture to match the new API

* Enhance `-hdr` command-line option to force HDR output and support optional nits configuration with improved input validation and overrides.

* Improve Vulkan robustness, add device feature checks, and refine memory/logging.

- Added depth clamping support gated by device feature checks, with fallback where unsupported.
- Improved deferred lighting UBO overflow handling with user-accessible warnings and buffer size adjustments.
- Enhanced Vulkan logging for validation, allocation, and debugging:
  - Automatic logging for descriptor pool exhaustion/fragmentation.
  - Vertex format and resource destruction checks.
  - Notifications for generic layout transitions and volumetric texture uploads.
- Consolidated shader logic to ensure Vulkan and OpenGL parity.

* Remove HDR10 encode shader and consolidate gamma correction logic for SDR and HDR10. Replace `Verify` assertions with `Assert`

* Add braces to conditional in object lighting logic for clarity

* Refine Vulkan memory allocation checks with `Verification` for improved error handling and messaging.

* Refine VulkanBuffer resize logic to preserve contents with CPU-side copy and add safeguards for non-coherent memory scenarios.

* Add detailed comments explaining descriptor array capacity constants in VulkanDescriptorManager

* Gate shadow support on depth clamp feature availability and clarify its necessity with improved comments.

---------

Co-authored-by: BMagnu <[email protected]>
Co-authored-by: Mara van der Laan <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-07-21 16:58:21 +00:00

3300 lines
98 KiB
C++

/*
* Copyright (C) Freespace Open 2013. All rights reserved.
*
* All source code herein is the property of Freespace Open. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "model/modelrender.h"
#include "asteroid/asteroid.h"
#include "cmdline/cmdline.h"
#include "decals/decals.h"
#include "gamesequence/gamesequence.h"
#include "globalincs/systemvars.h"
#include "math/floating.h"
#include "graphics/light.h"
#include "graphics/matrix.h"
#include "graphics/shadows.h"
#include "graphics/tmapper.h"
#include "graphics/uniforms.h"
#include "io/timer.h"
#include "jumpnode/jumpnode.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "math/staticrand.h"
#include "missionui/missionscreencommon.h"
#include "mod_table/mod_table.h"
#include "nebula/neb.h"
#include "particle/particle.h"
#include "prop/prop.h"
#include "parse/encrypt.h"
#include "render/3dinternal.h"
#include "render/batching.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "starfield/starfield.h"
#include "tracing/tracing.h"
#include "weapon/weapon.h"
#include <algorithm>
extern int Model_texturing;
extern int Model_polys;
extern int tiling;
extern float model_radius;
extern bool Scene_framebuffer_in_frame;
color Wireframe_color;
int Lab_object_detail_level = -1; // Used to display the detail level in the lab
extern void interp_generate_arc_segment(SCP_vector<vec3d> &arc_segment_points, const vec3d *v1, const vec3d *v2, ubyte depth_limit, ubyte depth);
int model_render_determine_elapsed_time(int objnum, uint64_t flags);
SCP_unordered_map<cached_ui_render_instance_key, cached_ui_render_instance_entry, cached_ui_render_instance_key_hash> Cached_ui_render_instance_cache;
int Ui_render_instance_cache_last_processed_framecount = -1;
constexpr int UI_RENDER_INSTANCE_CACHE_UNUSED_FRAMES_GRACE = 5;
size_t model_hash_subsystem_name_list_for_cache(const SCP_vector<SCP_string>& subsystem_names)
{
if (subsystem_names.empty()) {
return 0;
}
SCP_vector<SCP_string> normalized_names;
normalized_names.reserve(subsystem_names.size());
for (const auto& name : subsystem_names) {
auto normalized = name;
SCP_tolower(normalized);
normalized_names.push_back(std::move(normalized));
}
std::sort(normalized_names.begin(), normalized_names.end());
size_t seed = 0;
boost::hash_combine(seed, static_cast<uint32_t>(normalized_names.size()));
for (const auto& name : normalized_names) {
boost::hash_combine(seed, hash_fnv1a(name));
}
return seed;
}
// Returns TriStateBool::TRUE_ if a new instance was created, TriStateBool::FALSE_ if an existing instance was returned,
// or TriStateBool::UNKNOWN_ if there was an error (and model_instance_out will be set to -1 in this case)
TriStateBool model_get_cached_ui_render_instance(int model_num, int* model_instance_out, size_t instance_data_hash)
{
Assertion(model_instance_out != nullptr, "model_instance_out must not be null!");
if (model_instance_out == nullptr) {
return TriStateBool::UNKNOWN_;
}
cached_ui_render_instance_key key;
key.model_num = model_num;
key.state_instance_id = gameseq_get_state_instance_id();
key.instance_data_hash = instance_data_hash;
auto& entry = Cached_ui_render_instance_cache[key];
auto created_new = false;
if (entry.model_instance < 0) {
entry.model_instance = model_create_instance(model_objnum_special::OBJNUM_NONE, model_num);
if (entry.model_instance < 0) {
Warning(LOCATION, "Failed to create cached UI render model instance for model id %d.", model_num);
Cached_ui_render_instance_cache.erase(key);
*model_instance_out = -1;
return TriStateBool::UNKNOWN_;
}
created_new = true;
}
entry.last_used_framecount = Framecount;
*model_instance_out = entry.model_instance;
return created_new ? TriStateBool::TRUE_ : TriStateBool::FALSE_;
}
void model_process_cached_ui_render_instances()
{
if (Ui_render_instance_cache_last_processed_framecount == Framecount) {
return;
}
Ui_render_instance_cache_last_processed_framecount = Framecount;
for (auto it = Cached_ui_render_instance_cache.begin(); it != Cached_ui_render_instance_cache.end();) {
// This should never trigger but if it does it means something has gone very wrong with the cache management logic
Assertion(it->second.model_instance >= 0, "Corrupted UI render instance cache entry found");
if (it->second.model_instance < 0 || it->second.last_used_framecount < 0 ||
(Framecount - it->second.last_used_framecount) > UI_RENDER_INSTANCE_CACHE_UNUSED_FRAMES_GRACE) {
if (it->second.model_instance >= 0) {
model_delete_instance(it->second.model_instance);
}
it = Cached_ui_render_instance_cache.erase(it);
} else {
++it;
}
}
}
void model_clear_cached_ui_render_instances()
{
for (auto& instance : Cached_ui_render_instance_cache) {
if (instance.second.model_instance >= 0) {
model_delete_instance(instance.second.model_instance);
}
}
Cached_ui_render_instance_cache.clear();
Ui_render_instance_cache_last_processed_framecount = -1;
}
model_render_params::model_render_params() :
Model_flags(MR_NORMAL),
Debug_flags(0),
Objnum(-1),
Detail_level_locked(-1),
Depth_scale(1500.0f),
Attached_model_draw_distance(-1.0f),
Warp_bitmap(-1),
Warp_alpha(-1.0f),
Xparent_alpha(1.0f),
Forced_bitmap(-1),
Insignia_bitmap(-1),
Replacement_textures(nullptr),
Team_color_set(false),
Clip_plane_set(false),
Animated_effect(-1),
Animated_timer(0.0f),
Thruster_info(),
Use_alpha_mult(false),
Alpha_mult(1.0f)
{
Warp_scale.xyz.x = 1.0f;
Warp_scale.xyz.y = 1.0f;
Warp_scale.xyz.z = 1.0f;
Clip_normal = vmd_zero_vector;
Clip_pos = vmd_zero_vector;
if ( !Model_texturing )
Model_flags |= MR_NO_TEXTURING;
if ( !Model_polys ) {
Model_flags |= MR_NO_POLYS;
}
gr_init_color(&Color, 0, 0, 0);
}
uint64_t model_render_params::get_model_flags() const
{
return Model_flags;
}
uint model_render_params::get_debug_flags() const
{
return Debug_flags;
}
int model_render_params::get_object_number() const
{
return Objnum;
}
int model_render_params::get_detail_level_lock() const
{
return Detail_level_locked;
}
float model_render_params::get_depth_scale() const
{
return Depth_scale;
}
float model_render_params::get_attached_model_draw_distance() const
{
return Attached_model_draw_distance;
}
int model_render_params::get_warp_bitmap() const
{
return Warp_bitmap;
}
float model_render_params::get_warp_alpha() const
{
return Warp_alpha;
}
const vec3d& model_render_params::get_warp_scale() const
{
return Warp_scale;
}
const color& model_render_params::get_color() const
{
return Color;
}
float model_render_params::get_alpha() const
{
return Xparent_alpha;
}
int model_render_params::get_forced_bitmap() const
{
return Forced_bitmap;
}
int model_render_params::get_insignia_bitmap() const
{
return Insignia_bitmap;
}
std::shared_ptr<const model_texture_replace> model_render_params::get_replacement_textures() const
{
return Replacement_textures;
}
const team_color& model_render_params::get_team_color() const
{
return Current_team_color;
}
const vec3d& model_render_params::get_clip_plane_pos() const
{
return Clip_pos;
}
const vec3d& model_render_params::get_clip_plane_normal() const
{
return Clip_normal;
}
int model_render_params::get_animated_effect_num() const
{
return Animated_effect;
}
float model_render_params::get_animated_effect_timer() const
{
return Animated_timer;
}
void model_render_params::set_animated_effect(int effect_num, float timer)
{
Animated_effect = effect_num;
Animated_timer = timer;
}
void model_render_params::set_clip_plane(const vec3d &pos, const vec3d &normal)
{
Clip_plane_set = true;
Clip_normal = normal;
Clip_pos = pos;
}
bool model_render_params::is_clip_plane_set() const
{
return Clip_plane_set;
}
void model_render_params::set_team_color(const team_color &clr)
{
Team_color_set = true;
Current_team_color = clr;
}
void model_render_params::set_team_color(const SCP_string &team, const SCP_string &secondaryteam, fix timestamp, int fadetime)
{
Team_color_set = model_get_team_color(&Current_team_color, team, secondaryteam, timestamp, fadetime);
}
bool model_render_params::is_team_color_set() const
{
return Team_color_set;
}
void model_render_params::set_replacement_textures(std::shared_ptr<const model_texture_replace> textures)
{
Replacement_textures = std::move(textures);
}
void model_render_params::set_replacement_textures(int modelnum, const SCP_vector<texture_replace>& replacement_textures)
{
auto textures = std::make_shared<model_texture_replace>();
polymodel* pm = model_get(modelnum);
for (const auto& tr : replacement_textures)
{
for (int i = 0; i < pm->n_textures; ++i)
{
texture_map *tmap = &pm->maps[i];
int tnum = tmap->FindTexture(tr.old_texture);
if (tnum > -1)
(*textures)[i * TM_NUM_TYPES + tnum] = bm_load(tr.new_texture);
}
}
Replacement_textures = std::move(textures);
}
void model_render_params::set_insignia_bitmap(int bitmap)
{
Insignia_bitmap = bitmap;
}
void model_render_params::set_forced_bitmap(int bitmap)
{
Forced_bitmap = bitmap;
}
void model_render_params::set_alpha(float alpha)
{
Xparent_alpha = alpha;
}
void model_render_params::set_color(const color &clr)
{
Color = clr;
}
void model_render_params::set_color(int r, int g, int b)
{
gr_init_color( &Color, r, g, b );
}
void model_render_params::set_warp_params(int bitmap, float alpha, const vec3d &scale)
{
Warp_bitmap = bitmap;
Warp_alpha = alpha;
Warp_scale = scale;
}
void model_render_params::set_depth_scale(float scale)
{
Depth_scale = scale;
}
void model_render_params::set_attached_model_draw_distance(float distance)
{
Attached_model_draw_distance = distance;
}
void model_render_params::set_debug_flags(uint flags)
{
Debug_flags = flags;
}
void model_render_params::set_object_number(int num)
{
Objnum = num;
}
void model_render_params::set_flags(uint64_t flags)
{
Model_flags = flags;
}
void model_render_params::set_detail_level_lock(int detail_level_lock)
{
Detail_level_locked = detail_level_lock;
}
void model_render_params::set_thruster_info(const mst_info &info)
{
Thruster_info = info;
CLAMP(Thruster_info.length.xyz.z, 0.1f, 1.0f);
}
const mst_info& model_render_params::get_thruster_info() const
{
return Thruster_info;
}
void model_render_params::set_outline_thickness(float thickness) {
Outline_thickness = thickness;
}
float model_render_params::get_outline_thickness() const {
return Outline_thickness;
}
bool model_render_params::uses_thick_outlines() const {
return Outline_thickness > 0.0f;
}
void model_render_params::set_alpha_mult(float alpha) {
Alpha_mult = alpha;
Use_alpha_mult = true;
}
bool model_render_params::is_alpha_mult_set() const
{
return Use_alpha_mult;
}
float model_render_params::get_alpha_mult() const
{
return Alpha_mult;
}
void model_batch_buffer::reset()
{
Submodel_matrices.clear();
Current_offset = 0;
}
void model_batch_buffer::set_num_models(int n_models)
{
matrix4 init_mat;
vm_matrix4_set_identity(&init_mat);
Current_offset = Submodel_matrices.size();
for ( int i = 0; i < n_models; ++i ) {
Submodel_matrices.push_back(init_mat);
}
}
void model_batch_buffer::set_model_transform(const matrix4 &transform, int model_id)
{
Submodel_matrices[Current_offset + model_id] = transform;
}
void model_batch_buffer::add_matrix(const matrix4 &mat)
{
Submodel_matrices.push_back(mat);
}
size_t model_batch_buffer::get_buffer_offset() const
{
return Current_offset;
}
void model_batch_buffer::allocate_memory()
{
auto size = Submodel_matrices.size() * sizeof(matrix4);
if ( Mem_alloc == NULL || Mem_alloc_size < size ) {
if ( Mem_alloc != NULL ) {
vm_free(Mem_alloc);
}
Mem_alloc = vm_malloc(size);
}
Mem_alloc_size = size;
memcpy(Mem_alloc, &Submodel_matrices[0], size);
}
void model_batch_buffer::submit_buffer_data()
{
if ( Submodel_matrices.empty() ) {
return;
}
allocate_memory();
gr_update_transform_buffer(Mem_alloc, Mem_alloc_size);
}
model_draw_list::model_draw_list()
{
reset();
}
void model_draw_list::add_arc(const vec3d *v1, const vec3d *v2, const SCP_vector<vec3d> *persistent_arc_points, const color *primary, const color *secondary, float arc_width, ubyte segment_depth)
{
arc_effect new_arc;
new_arc.transform = get_transform();
new_arc.v1 = *v1;
new_arc.v2 = *v2;
new_arc.primary = *primary;
new_arc.secondary = *secondary;
new_arc.width = arc_width;
new_arc.segment_depth = segment_depth;
new_arc.persistent_arc_points = persistent_arc_points;
Arcs.push_back(new_arc);
}
void model_draw_list::set_light_filter(const vec3d *pos, float rad)
{
_lights.setLightFilter(pos, rad);
Current_lights_set = _lights.bufferLights();
}
void model_draw_list::add_buffer_draw(const model_material *render_material, const indexed_vertex_source *vert_src, const vertex_buffer *buffer, size_t texi, uint tmap_flags)
{
queued_buffer_draw draw_data;
draw_data.render_material = *render_material;
{
auto possibly_deferred = draw_data.render_material.get_depth_mode() == ZBUFFER_TYPE_FULL
&& gr_is_capable(gr_capability::CAPABILITY_DEFERRED_LIGHTING) && light_deferred_enabled();
if (possibly_deferred) {
draw_data.render_material.set_fog();
}
draw_data.render_material.set_deferred_lighting(possibly_deferred ? Deferred_lighting : false);
draw_data.render_material.set_high_dynamic_range(High_dynamic_range);
draw_data.render_material.set_shadow_receiving(Shadow_quality != ShadowQuality::Disabled);
}
if (tmap_flags & TMAP_FLAG_BATCH_TRANSFORMS && buffer->flags & VB_FLAG_MODEL_ID) {
vm_matrix4_set_identity(&draw_data.transform);
draw_data.scale.xyz.x = 1.0f;
draw_data.scale.xyz.y = 1.0f;
draw_data.scale.xyz.z = 1.0f;
draw_data.transform_buffer_offset = _batchBuffer.get_buffer_offset();
draw_data.render_material.set_batching(true);
} else {
draw_data.transform = get_transform();
draw_data.scale = _scale;
draw_data.transform_buffer_offset = INVALID_SIZE;
draw_data.render_material.set_batching(false);
}
draw_data.sdr_flags = draw_data.render_material.get_shader_flags();
draw_data.vert_src = vert_src;
draw_data.buffer = buffer;
draw_data.texi = texi;
draw_data.flags = tmap_flags;
draw_data.lights = Current_lights_set;
push_element(std::move(draw_data));
}
void model_draw_list::render_buffer(const queued_buffer_draw &render_elements)
{
GR_DEBUG_SCOPE("Render buffer");
TRACE_SCOPE(tracing::RenderBuffer);
gr_bind_uniform_buffer(uniform_block_type::ModelData, render_elements.uniform_buffer_offset,
sizeof(graphics::model_uniform_data), _dataBuffer.bufferHandle());
gr_render_model(const_cast<model_material*>(&render_elements.render_material), const_cast<indexed_vertex_source*>(render_elements.vert_src), const_cast<vertex_buffer*>(render_elements.buffer), render_elements.texi);
}
vec3d model_draw_list::get_view_position() const
{
matrix basis_world;
matrix4 transform_mat = _transforms.get_transform();
matrix orient;
vec3d pos;
vm_matrix4_get_orientation(&orient, &transform_mat);
vm_matrix4_get_offset(&pos, &transform_mat);
vm_matrix_x_matrix(&basis_world, &Object_matrix, &orient);
vec3d eye_pos_local;
vm_vec_sub(&eye_pos_local, &Eye_position, &pos);
vec3d return_val;
vm_vec_rotate(&return_val, &eye_pos_local, &basis_world);
return return_val;
}
void model_draw_list::init()
{
reset();
for (auto& l : Lights) {
if ( l.type == Light_Type::Directional || !Deferred_lighting ) {
_lights.addLight(&l);
}
}
}
void model_draw_list::render_all(gr_zbuffer_type depth_mode)
{
GR_DEBUG_SCOPE("Render draw list");
TRACE_SCOPE(tracing::SubmitDraws);
Assertion(_initialized, "init_render must be called before any render_all call!");
_lights.resetLightState();
for (int render_index : _keys) {
if ( depth_mode == ZBUFFER_TYPE_DEFAULT || _elements[render_index].render_material.get_depth_mode() == depth_mode ) {
render_buffer(_elements[render_index]);
}
}
gr_alpha_mask_set(0, 1.0f);
}
void model_draw_list::render_arc(const arc_effect &arc)
{
g3_start_instance_matrix(&arc.transform);
model_render_arc(&arc.v1, &arc.v2, arc.persistent_arc_points, &arc.primary, &arc.secondary, arc.width, arc.segment_depth);
g3_done_instance(true);
}
void model_draw_list::render_arcs()
{
int mode = gr_zbuffer_set(GR_ZBUFF_READ);
for ( size_t i = 0; i < Arcs.size(); ++i ) {
render_arc(Arcs[i]);
}
gr_zbuffer_set(mode);
}
void model_draw_list::add_insignia(const model_render_params *params, const polymodel *pm, int detail_level, int bitmap_num)
{
insignia_draw_data new_insignia;
new_insignia.transform = get_transform();
new_insignia.pm = pm;
new_insignia.detail_level = detail_level;
new_insignia.bitmap_num = bitmap_num;
new_insignia.clip = params->is_clip_plane_set();
new_insignia.clip_normal = params->get_clip_plane_normal();
new_insignia.clip_position = params->get_clip_plane_pos();
Insignias.push_back(new_insignia);
}
void model_draw_list::render_insignia(const insignia_draw_data &insignia_info)
{
if ( insignia_info.clip ) {
vec3d tmp;
vec3d pos;
vm_matrix4_get_offset(&pos, &insignia_info.transform);
vm_vec_sub(&tmp, &pos, &insignia_info.clip_position);
vm_vec_normalize(&tmp);
if ( vm_vec_dot(&tmp, &insignia_info.clip_normal) < 0.0f) {
return;
}
}
g3_start_instance_matrix(&insignia_info.transform);
model_render_insignias(&insignia_info);
g3_done_instance(true);
}
void model_draw_list::render_insignias() const
{
for (const auto& ins : Insignias) {
render_insignia(ins);
}
}
void model_draw_list::add_outline(const vertex* vert_array, int n_verts, const color *clr)
{
outline_draw draw_info;
draw_info.vert_array = vert_array;
draw_info.n_verts = n_verts;
draw_info.clr = *clr;
draw_info.transform = get_transform();
Outlines.push_back(draw_info);
}
void model_draw_list::render_outlines()
{
gr_clear_states();
for ( size_t i = 0; i < Outlines.size(); ++i ) {
render_outline(Outlines[i]);
}
}
void model_draw_list::render_outline(const outline_draw &outline_info)
{
g3_start_instance_matrix(&outline_info.transform);
material material_instance;
material_instance.set_depth_mode(ZBUFFER_TYPE_READ);
material_instance.set_blend_mode(ALPHA_BLEND_ALPHA_BLEND_ALPHA);
material_instance.set_color(outline_info.clr);
g3_render_primitives(&material_instance, const_cast<vertex*>(outline_info.vert_array), outline_info.n_verts, PRIM_TYPE_LINES, false);
g3_done_instance(true);
}
bool model_draw_list::sort_draw_pair(int a, int b) const
{
auto draw_call_a = &_elements[a];
auto draw_call_b = &_elements[b];
if ( draw_call_a->sdr_flags != draw_call_b->sdr_flags ) {
return draw_call_a->sdr_flags < draw_call_b->sdr_flags;
}
if ( draw_call_a->vert_src->Vbuffer_handle != draw_call_b->vert_src->Vbuffer_handle ) {
return draw_call_a->vert_src->Vbuffer_handle.value() < draw_call_b->vert_src->Vbuffer_handle.value();
}
if ( draw_call_a->vert_src->Ibuffer_handle != draw_call_b->vert_src->Ibuffer_handle ) {
return draw_call_a->vert_src->Ibuffer_handle.value() < draw_call_b->vert_src->Ibuffer_handle.value();
}
if ( draw_call_a->render_material.get_texture_map(TM_BASE_TYPE) != draw_call_b->render_material.get_texture_map(TM_BASE_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_BASE_TYPE) < draw_call_b->render_material.get_texture_map(TM_BASE_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_SPECULAR_TYPE) != draw_call_b->render_material.get_texture_map(TM_SPECULAR_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_SPECULAR_TYPE) < draw_call_b->render_material.get_texture_map(TM_SPECULAR_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_SPEC_GLOSS_TYPE) != draw_call_b->render_material.get_texture_map(TM_SPEC_GLOSS_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_SPEC_GLOSS_TYPE) < draw_call_b->render_material.get_texture_map(TM_SPEC_GLOSS_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_GLOW_TYPE) != draw_call_b->render_material.get_texture_map(TM_GLOW_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_GLOW_TYPE) < draw_call_b->render_material.get_texture_map(TM_GLOW_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_NORMAL_TYPE) != draw_call_b->render_material.get_texture_map(TM_NORMAL_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_NORMAL_TYPE) < draw_call_b->render_material.get_texture_map(TM_NORMAL_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_HEIGHT_TYPE) != draw_call_b->render_material.get_texture_map(TM_HEIGHT_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_HEIGHT_TYPE) < draw_call_b->render_material.get_texture_map(TM_HEIGHT_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_AMBIENT_TYPE) != draw_call_b->render_material.get_texture_map(TM_AMBIENT_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_AMBIENT_TYPE) < draw_call_b->render_material.get_texture_map(TM_AMBIENT_TYPE);
}
if ( draw_call_a->render_material.get_texture_map(TM_MISC_TYPE) != draw_call_b->render_material.get_texture_map(TM_MISC_TYPE) ) {
return draw_call_a->render_material.get_texture_map(TM_MISC_TYPE) < draw_call_b->render_material.get_texture_map(TM_MISC_TYPE);
}
return draw_call_a->lights.index_start < draw_call_b->lights.index_start;
}
void model_draw_list::build_uniform_buffer() {
GR_DEBUG_SCOPE("Build model uniform buffer");
TRACE_SCOPE(tracing::BuildModelUniforms);
_dataBuffer = gr_get_uniform_buffer(uniform_block_type::ModelData, _keys.size());
for (auto render_index : _keys) {
auto& queued_draw = _elements[render_index];
if ( queued_draw.render_material.is_lit() ) {
_lights.setLights(&queued_draw.lights);
} else {
_lights.resetLightState();
}
auto element = _dataBuffer.aligner().addTypedElement<graphics::model_uniform_data>();
graphics::uniforms::convert_model_material(element,
queued_draw.render_material,
queued_draw.transform,
queued_draw.scale,
queued_draw.transform_buffer_offset);
queued_draw.uniform_buffer_offset = _dataBuffer.getCurrentAlignerOffset();
}
TRACE_SCOPE(tracing::UploadModelUniforms);
_dataBuffer.submitData();
}
model_draw_list::~model_draw_list() {
reset();
}
void model_render_add_lightning(model_draw_list *scene, const model_render_params* interp, const polymodel *pm, const submodel_instance *smi )
{
float width = 0.9f;
color primary, secondary;
if ( interp->get_model_flags() & MR_SHOW_OUTLINE_PRESET ) {
return;
}
extern int Interp_lightning;
if ( !Interp_lightning ) {
return;
}
for (auto &arc: smi->electrical_arcs) {
// pick a color based upon arc type
switch ( arc.type ) {
// "normal", FreeSpace 1 style arcs
case MARC_TYPE_DAMAGED:
if ( Random::flip_coin() ) {
primary = Arc_color_damage_p1;
} else {
primary = Arc_color_damage_p2;
}
secondary = Arc_color_damage_s1;
// try and scale the size a bit so that it looks equally well on smaller vessels
width = Arc_width_default_damage;
if (pm->rad < Arc_width_no_multiply_over_radius_damage) {
width *= (pm->rad * Arc_width_radius_multiplier_damage);
if (width < Arc_width_minimum_damage) {
width = Arc_width_minimum_damage;
}
}
break;
case MARC_TYPE_SCRIPTED:
case MARC_TYPE_SHIP:
if ( Random::flip_coin() ) {
primary = arc.primary_color_1;
} else {
primary = arc.primary_color_2;
}
secondary = arc.secondary_color;
width = arc.width;
break;
// "EMP" style arcs
case MARC_TYPE_EMP:
if ( Random::flip_coin() ) {
primary = Arc_color_emp_p1;
} else {
primary = Arc_color_emp_p2;
}
secondary = Arc_color_emp_s1;
// try and scale the size a bit so that it looks equally well on smaller vessels
width = Arc_width_default_emp;
if (pm->rad < Arc_width_no_multiply_over_radius_emp) {
width *= (pm->rad * Arc_width_radius_multiplier_emp);
if (width < Arc_width_minimum_emp) {
width = Arc_width_minimum_emp;
}
}
break;
default:
UNREACHABLE("Unknown arc type of %d found in model_render_add_lightning(), please contact an SCP coder!", arc.type);
}
// render the actual arc segment
if (width > 0.0f)
scene->add_arc(&arc.endpoint_1, &arc.endpoint_2, arc.persistent_arc_points, &primary, &secondary, width, arc.segment_depth);
}
}
float model_render_determine_depth(int obj_num, int model_num, const matrix* orient, const vec3d* pos, int detail_level_locked, const vec3d* eye_pos)
{
if (eye_pos == nullptr)
eye_pos = &Eye_position;
vec3d closest_pos;
float depth = model_find_closest_point( &closest_pos, model_num, -1, orient, pos, eye_pos );
if ( detail_level_locked < 0 ) {
switch (Detail.detail_distance) {
case 0: // lowest
depth /= The_mission.ai_profile->detail_distance_mult[0];
break;
case 1: // lower than normal
depth /= The_mission.ai_profile->detail_distance_mult[1];
break;
case 2: // default
depth /= The_mission.ai_profile->detail_distance_mult[2];
break;
case 3: // above normal
depth /= The_mission.ai_profile->detail_distance_mult[3];
break;
case 4: // even more normal
depth /= The_mission.ai_profile->detail_distance_mult[4];
break;
}
// nebula ?
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
depth *= neb2_get_lod_scale(obj_num);
}
}
return depth;
}
int model_render_determine_detail(float depth, int model_num, int detail_level_locked)
{
int tmp_detail_level = Game_detail_level;
polymodel *pm = model_get(model_num);
Assert( pm->n_detail_levels < MAX_MODEL_DETAIL_LEVELS );
int i;
if ( pm->n_detail_levels > 1 ) {
if ( detail_level_locked >= 0 ) {
i = detail_level_locked+1;
} else {
static_assert(MAX_DETAIL_VALUE == 4, "MAX_DETAIL_VALUE is assumed to be 4 in SystemVars.cpp");
for ( i = 0; i < pm->n_detail_levels; i++ ) {
if ( depth <= pm->detail_depth[i] ) {
break;
}
}
// If no valid detail depths specified, use highest.
if ( (i > 1) && (pm->detail_depth[i-1] < 1.0f) ) {
i = 1;
}
}
int detail_level = i - 1 - tmp_detail_level;
if ( detail_level < 0 ) {
return 0;
} else if ( detail_level >= pm->n_detail_levels ) {
return pm->n_detail_levels - 1;
}
return detail_level;
} else {
return 0;
}
}
void model_render_buffers(model_draw_list* scene, model_material *rendering_material, const model_render_params* interp, const vertex_buffer *buffer, const polymodel *pm, int mn, int detail_level, uint tmap_flags)
{
bsp_info *submodel = nullptr;
const uint64_t model_flags = interp->get_model_flags();
const uint debug_flags = interp->get_debug_flags();
const int obj_num = interp->get_object_number();
Assert(buffer != nullptr);
Assert(detail_level >= 0);
if ( (mn >= 0) && (mn < pm->n_models) ) {
submodel = &pm->submodel[mn];
}
bool render_as_thruster = (submodel != nullptr) && submodel->flags[Model::Submodel_flags::Is_thruster] && (model_flags & MR_SHOW_THRUSTERS);
vec3d scale;
if ( render_as_thruster ) {
scale.xyz.x = 1.0f;
scale.xyz.y = 1.0f;
scale.xyz.z = 1.0f;
rendering_material->set_thrust_scale(interp->get_thruster_info().length.xyz.z);
} else {
scale = interp->get_warp_scale();
rendering_material->set_thrust_scale();
}
scene->set_scale(&scale);
if ( tmap_flags & TMAP_FLAG_BATCH_TRANSFORMS && (mn >= 0) && (mn < pm->n_models) ) {
scene->add_submodel_to_batch(mn);
return;
}
int elapsed_time = model_render_determine_elapsed_time(obj_num, model_flags);
texture_info tex_replace[TM_NUM_TYPES];
int no_texturing = model_flags & MR_NO_TEXTURING;
int forced_texture = -2;
float forced_alpha = 1.0f;
int forced_blend_filter = GR_ALPHABLEND_NONE;
if ( interp->get_forced_bitmap() >= 0 ) {
forced_texture = interp->get_forced_bitmap();
} else if ( interp->get_warp_bitmap() >= 0 ) {
forced_texture = interp->get_warp_bitmap();
forced_alpha = interp->get_warp_alpha();
forced_blend_filter = GR_ALPHABLEND_FILTER;
} else if ( render_as_thruster ) {
if ( ( interp->get_thruster_info().primary_bitmap >= 0 ) && ( interp->get_thruster_info().length.xyz.z > 0.0f ) ) {
forced_texture = interp->get_thruster_info().primary_bitmap;
} else {
forced_texture = -1;
}
forced_alpha = 1.2f;
forced_blend_filter = GR_ALPHABLEND_FILTER;
} else if ( model_flags & MR_ALL_XPARENT ) {
forced_alpha = interp->get_alpha();
forced_blend_filter = GR_ALPHABLEND_FILTER;
}
int texture_maps[TM_NUM_TYPES] = { -1 };
size_t buffer_size = buffer->tex_buf.size();
const auto& replacement_textures = interp->get_replacement_textures();
for ( size_t i = 0; i < buffer_size; i++ ) {
int tmap_num = buffer->tex_buf[i].texture;
auto tmap = &pm->maps[tmap_num];
int rt_begin_index = tmap_num*TM_NUM_TYPES;
float alpha = 1.0f;
texture_maps[TM_BASE_TYPE] = -1;
texture_maps[TM_GLOW_TYPE] = -1;
texture_maps[TM_SPECULAR_TYPE] = -1;
texture_maps[TM_NORMAL_TYPE] = -1;
texture_maps[TM_HEIGHT_TYPE] = -1;
texture_maps[TM_MISC_TYPE] = -1;
texture_maps[TM_SPEC_GLOSS_TYPE] = -1;
texture_maps[TM_AMBIENT_TYPE] = -1;
if (forced_texture != -2) {
texture_maps[TM_BASE_TYPE] = forced_texture;
alpha = forced_alpha;
if (interp->get_warp_bitmap() >= 0) {
texture_maps[TM_GLOW_TYPE] = forced_texture;
}
} else if ( !no_texturing ) {
// pick the texture, animating it if necessary
if ( (replacement_textures != nullptr) && ((*replacement_textures)[rt_begin_index + TM_BASE_TYPE] == REPLACE_WITH_INVISIBLE) ) {
// invisible textures aren't rendered, but we still have to skip assigning the underlying model texture
texture_maps[TM_BASE_TYPE] = -1;
} else if ( (replacement_textures != nullptr) && ((*replacement_textures)[rt_begin_index + TM_BASE_TYPE] >= 0) ) {
// an underlying texture is replaced with a real new texture
tex_replace[TM_BASE_TYPE] = texture_info((*replacement_textures)[rt_begin_index + TM_BASE_TYPE]);
texture_maps[TM_BASE_TYPE] = model_interp_get_texture(&tex_replace[TM_BASE_TYPE], elapsed_time);
} else {
// we just use the underlying texture
texture_maps[TM_BASE_TYPE] = model_interp_get_texture(&tmap->textures[TM_BASE_TYPE], elapsed_time);
}
if ( texture_maps[TM_BASE_TYPE] < 0 ) {
continue;
}
// doing glow maps?
if ( !(model_flags & MR_NO_GLOWMAPS) ) {
auto tglow = &tmap->textures[TM_GLOW_TYPE];
if ( (replacement_textures != nullptr) && ((*replacement_textures)[rt_begin_index + TM_GLOW_TYPE] >= 0) ) {
tex_replace[TM_GLOW_TYPE] = texture_info((*replacement_textures)[rt_begin_index + TM_GLOW_TYPE]);
texture_maps[TM_GLOW_TYPE] = model_interp_get_texture(&tex_replace[TM_GLOW_TYPE], elapsed_time);
} else if (tglow->GetTexture() >= 0) {
// shockwaves are special, their current frame has to come out of the shockwave code to get the timing correct
if ( (obj_num >= 0) && (Objects[obj_num].type == OBJ_SHOCKWAVE) && (tglow->GetNumFrames() > 1) ) {
texture_maps[TM_GLOW_TYPE] = tglow->GetTexture() + shockwave_get_framenum(Objects[obj_num].instance, tglow->GetTexture());
} else {
texture_maps[TM_GLOW_TYPE] = model_interp_get_texture(tglow, elapsed_time);
}
}
}
if (!(debug_flags & MR_DEBUG_NO_SPEC)) {
if (replacement_textures != nullptr && (*replacement_textures)[rt_begin_index + TM_SPECULAR_TYPE] >= 0) {
tex_replace[TM_SPECULAR_TYPE] = texture_info((*replacement_textures)[rt_begin_index + TM_SPECULAR_TYPE]);
texture_maps[TM_SPECULAR_TYPE] = model_interp_get_texture(&tex_replace[TM_SPECULAR_TYPE], elapsed_time);
}
else {
texture_maps[TM_SPECULAR_TYPE] = model_interp_get_texture(&tmap->textures[TM_SPECULAR_TYPE], elapsed_time);
}
}
if ( replacement_textures != nullptr && (*replacement_textures)[rt_begin_index + TM_SPEC_GLOSS_TYPE] >= 0 ) {
tex_replace[TM_SPEC_GLOSS_TYPE] = texture_info((*replacement_textures)[rt_begin_index + TM_SPEC_GLOSS_TYPE]);
texture_maps[TM_SPEC_GLOSS_TYPE] = model_interp_get_texture(&tex_replace[TM_SPEC_GLOSS_TYPE], elapsed_time);
} else {
texture_maps[TM_SPEC_GLOSS_TYPE] = model_interp_get_texture(&tmap->textures[TM_SPEC_GLOSS_TYPE], elapsed_time);
}
if (detail_level < 2 || Detail.detail_distance > 2) {
// likewise, etc.
auto norm_map = &tmap->textures[TM_NORMAL_TYPE];
auto height_map = &tmap->textures[TM_HEIGHT_TYPE];
auto ambient_map = &tmap->textures[TM_AMBIENT_TYPE];
auto misc_map = &tmap->textures[TM_MISC_TYPE];
if (replacement_textures != nullptr) {
const auto& replacement_textures_deref = *replacement_textures;
if (replacement_textures_deref[rt_begin_index + TM_NORMAL_TYPE] >= 0) {
tex_replace[TM_NORMAL_TYPE] = texture_info(replacement_textures_deref[rt_begin_index + TM_NORMAL_TYPE]);
norm_map = &tex_replace[TM_NORMAL_TYPE];
}
if (replacement_textures_deref[rt_begin_index + TM_HEIGHT_TYPE] >= 0) {
tex_replace[TM_HEIGHT_TYPE] = texture_info(replacement_textures_deref[rt_begin_index + TM_HEIGHT_TYPE]);
height_map = &tex_replace[TM_HEIGHT_TYPE];
}
if (replacement_textures_deref[rt_begin_index + TM_AMBIENT_TYPE] >= 0) {
tex_replace[TM_AMBIENT_TYPE] = texture_info(replacement_textures_deref[rt_begin_index + TM_AMBIENT_TYPE]);
ambient_map = &tex_replace[TM_AMBIENT_TYPE];
}
if (replacement_textures_deref[rt_begin_index + TM_MISC_TYPE] >= 0) {
tex_replace[TM_MISC_TYPE] = texture_info(replacement_textures_deref[rt_begin_index + TM_MISC_TYPE]);
misc_map = &tex_replace[TM_MISC_TYPE];
}
}
if (debug_flags & MR_DEBUG_NO_DIFFUSE) texture_maps[TM_BASE_TYPE] = -1;
if (debug_flags & MR_DEBUG_NO_GLOW) texture_maps[TM_GLOW_TYPE] = -1;
if (debug_flags & MR_DEBUG_NO_SPEC) texture_maps[TM_SPECULAR_TYPE] = -1;
if (debug_flags & MR_DEBUG_NO_REFLECT) texture_maps[TM_SPEC_GLOSS_TYPE] = -1;
if (!(debug_flags & MR_DEBUG_NO_MISC)) texture_maps[TM_MISC_TYPE] = model_interp_get_texture(misc_map, elapsed_time);
if (!(debug_flags & MR_DEBUG_NO_NORMAL) && Detail.lighting > 0) texture_maps[TM_NORMAL_TYPE] = model_interp_get_texture(norm_map, elapsed_time);
if (!(debug_flags & MR_DEBUG_NO_AMBIENT) && Detail.lighting > 0) texture_maps[TM_AMBIENT_TYPE] = model_interp_get_texture(ambient_map, elapsed_time);
if (!(debug_flags & MR_DEBUG_NO_HEIGHT) && Detail.lighting > 1) texture_maps[TM_HEIGHT_TYPE] = model_interp_get_texture(height_map, elapsed_time);
}
} else {
alpha = forced_alpha;
}
if ( (texture_maps[TM_BASE_TYPE] == -1) && !no_texturing && !(debug_flags & MR_DEBUG_NO_DIFFUSE) ) {
continue;
}
bool use_blending = false;
// trying to get transparent textures-Bobboau
if (tmap->is_transparent) {
// for special shockwave/warp map usage
alpha = (interp->get_warp_alpha() != -1.0f) ? interp->get_warp_alpha() : 0.8f;
use_blending = true;
} else if ( buffer->flags & VB_FLAG_TRANS ) {
use_blending = true;
}
if (rendering_material->is_alpha_mult_active() && !(model_flags & MR_SKYBOX)) {
use_blending = true;
}
if (forced_blend_filter != GR_ALPHABLEND_NONE) {
use_blending = true;
}
bool use_depth_test;
if ( use_blending ) {
use_depth_test = true;
} else {
use_depth_test = !((model_flags & MR_NO_ZBUFFER) || (model_flags & MR_ALL_XPARENT));
}
gr_alpha_blend blend_mode = model_render_determine_blend_mode(texture_maps[TM_BASE_TYPE], use_blending);
gr_zbuffer_type depth_mode = material_determine_depth_mode(use_depth_test, use_blending);
if (rendering_material->is_alpha_mult_active() && !(model_flags & MR_SKYBOX)) {
blend_mode = ALPHA_BLEND_PREMULTIPLIED;
}
rendering_material->set_depth_mode(depth_mode);
rendering_material->set_blend_mode(depth_mode == ZBUFFER_TYPE_FULL ? ALPHA_BLEND_NONE : blend_mode);
color clr = interp->get_color();
model_render_determine_color(&clr, alpha, blend_mode, no_texturing ? true : false, rendering_material->is_desaturated());
rendering_material->set_color(clr);
if ( (tmap_flags & TMAP_FLAG_TEXTURED) && (buffer->flags & VB_FLAG_UV1) ) {
rendering_material->set_texture_map(TM_BASE_TYPE, texture_maps[TM_BASE_TYPE]);
if ( texture_maps[TM_BASE_TYPE] >= 0 && bm_has_alpha_channel(texture_maps[TM_BASE_TYPE]) ) {
rendering_material->set_texture_type(material::TEX_TYPE_XPARENT);
}
rendering_material->set_texture_map(TM_GLOW_TYPE, texture_maps[TM_GLOW_TYPE]);
rendering_material->set_texture_map(TM_SPECULAR_TYPE, texture_maps[TM_SPECULAR_TYPE]);
rendering_material->set_texture_map(TM_SPEC_GLOSS_TYPE, texture_maps[TM_SPEC_GLOSS_TYPE]);
rendering_material->set_texture_map(TM_NORMAL_TYPE, texture_maps[TM_NORMAL_TYPE]);
rendering_material->set_texture_map(TM_HEIGHT_TYPE, texture_maps[TM_HEIGHT_TYPE]);
rendering_material->set_texture_map(TM_AMBIENT_TYPE, texture_maps[TM_AMBIENT_TYPE]);
rendering_material->set_texture_map(TM_MISC_TYPE, texture_maps[TM_MISC_TYPE]);
}
scene->add_buffer_draw(rendering_material, &pm->vert_source, buffer, i, tmap_flags);
}
}
void model_render_children_buffers(model_draw_list* scene, model_material *rendering_material, const model_render_params* interp, const polymodel* pm, const polymodel_instance *pmi, int mn, int detail_level, uint tmap_flags, bool trans_buffer)
{
int i;
if ( (mn < 0) || (mn >= pm->n_models) ) {
Int3();
return;
}
bsp_info *sm = &pm->submodel[mn];
submodel_instance *smi = nullptr;
if ( pmi != nullptr ) {
smi = &pmi->submodel[mn];
if ( smi->blown_off ) {
return;
}
}
const uint64_t model_flags = interp->get_model_flags();
if (sm->flags[Model::Submodel_flags::Is_thruster]) {
if ( !( model_flags & MR_SHOW_THRUSTERS ) ) {
return;
}
rendering_material->set_lighting(false);
}
vec3d view_pos = scene->get_view_position();
if ( !model_render_check_detail_box(&view_pos, pm, mn, model_flags) ) {
return;
}
// Get submodel rotation/translation data and use it to put together a matrix and a vector
// describing the final position of the submodel relative to its parent
matrix submodel_orient = vmd_identity_matrix;
vec3d submodel_offset = sm->offset;
if ( smi != nullptr ) {
submodel_orient = smi->canonical_orient;
vm_vec_add2(&submodel_offset, &smi->canonical_offset);
}
scene->push_transform(&submodel_offset, &submodel_orient);
if ( (model_flags & MR_SHOW_OUTLINE || model_flags & MR_SHOW_OUTLINE_HTL || model_flags & MR_SHOW_OUTLINE_PRESET) &&
sm->outline_buffer != nullptr ) {
color outline_color = interp->get_color();
scene->add_outline(sm->outline_buffer.get(), sm->n_verts_outline, &outline_color);
} else {
if ( trans_buffer && sm->trans_buffer.flags & VB_FLAG_TRANS ) {
model_render_buffers(scene, rendering_material, interp, &sm->trans_buffer, pm, mn, detail_level, tmap_flags);
} else {
model_render_buffers(scene, rendering_material, interp, &sm->buffer, pm, mn, detail_level, tmap_flags);
}
}
if ( smi != nullptr && !smi->electrical_arcs.empty() ) {
model_render_add_lightning( scene, interp, pm, smi );
}
i = sm->first_child;
while ( i >= 0 ) {
if ( !pm->submodel[i].flags[Model::Submodel_flags::Is_thruster] ) {
model_render_children_buffers( scene, rendering_material, interp, pm, pmi, i, detail_level, tmap_flags, trans_buffer );
}
i = pm->submodel[i].next_sibling;
}
if ( sm->flags[Model::Submodel_flags::Is_thruster] ) {
rendering_material->set_lighting(true);
}
scene->pop_transform();
}
float model_render_determine_light_factor(const model_render_params* interp, const vec3d *pos, uint64_t flags)
{
if ( flags & MR_IS_ASTEROID ) {
// Dim it based on distance
float depth = vm_vec_dist_quick( pos, &Eye_position );
if ( depth > interp->get_depth_scale() ) {
float temp_light = interp->get_depth_scale()/depth;
if ( temp_light > 1.0f ) {
return 1.0f;
}
return temp_light;
}
}
return 1.0f;
}
float model_render_determine_box_scale()
{
float box_scale = 1.2f;
// scale the render box settings based on the "Model Detail" slider
switch ( Detail.detail_distance ) {
case 0: // 1st dot is 20%
box_scale = 0.2f;
break;
case 1: // 2nd dot is 50%
box_scale = 0.5f;
break;
case 2: // 3rd dot is 80%
box_scale = 0.8f;
break;
case 3: // 4th dot is 100% (this is the default setting for "High" and "Very High" settings)
box_scale = 1.0f;
break;
case 4: // 5th dot (max) is 120%
default:
box_scale = 1.2f;
break;
}
return box_scale;
}
// Goober5000
// Returns milliseconds since texture animation started.
int model_render_determine_elapsed_time(int objnum, uint64_t flags)
{
if ( objnum >= 0 ) {
object *objp = &Objects[objnum];
if ( objp->type == OBJ_SHIP ) {
return timestamp_since(Ships[objp->instance].base_texture_anim_timestamp);
}
} else if ( flags & MR_SKYBOX ) {
return timestamp_since(Skybox_timestamp);
}
// by default, assume texture animation started at the beginning of the mission
return timestamp_get_mission_time_in_milliseconds();
}
bool model_render_determine_autocenter(vec3d *auto_back, const polymodel *pm, int detail_level, uint64_t flags)
{
if ( flags & MR_AUTOCENTER ) {
// standard autocenter using data in model
if ( pm->flags & PM_FLAG_AUTOCEN ) {
*auto_back = pm->autocenter;
vm_vec_scale(auto_back, -1.0f);
return true;
} else if ( flags & MR_IS_MISSILE ) {
// fake autocenter if we are a missile and don't already have autocen info
auto_back->xyz.x = -( (pm->submodel[pm->detail[detail_level]].max.xyz.x + pm->submodel[pm->detail[detail_level]].min.xyz.x) / 2.0f );
auto_back->xyz.y = -( (pm->submodel[pm->detail[detail_level]].max.xyz.y + pm->submodel[pm->detail[detail_level]].min.xyz.y) / 2.0f );
auto_back->xyz.z = -( (pm->submodel[pm->detail[detail_level]].max.xyz.z + pm->submodel[pm->detail[detail_level]].min.xyz.z) / 2.0f );
return true;
}
}
return false;
}
void model_render_determine_color(color *clr, float alpha, gr_alpha_blend blend_mode, bool no_texturing, bool desaturate)
{
clr->alpha = static_cast<ubyte>((alpha * 255.0f));
if ( no_texturing || desaturate ) {
// don't override the given color if we're not texturing or we're desaturating
return;
}
if ( blend_mode == ALPHA_BLEND_ADDITIVE ) {
clr->red = clr->green = clr->blue = clr->alpha;
clr->alpha = 255;
} else {
clr->red = clr->green = clr->blue = 255;
}
}
gr_alpha_blend model_render_determine_blend_mode(int base_bitmap, bool blending)
{
if ( blending ) {
if ( base_bitmap >= 0 && bm_has_alpha_channel(base_bitmap) ) {
return ALPHA_BLEND_PREMULTIPLIED;
}
return ALPHA_BLEND_ADDITIVE;
}
return ALPHA_BLEND_ALPHA_BLEND_ALPHA;
}
bool model_render_check_detail_box(const vec3d *view_pos, const polymodel *pm, int submodel_num, uint64_t flags)
{
Assert(pm != NULL);
bsp_info *model = &pm->submodel[submodel_num];
float box_scale = model_render_determine_box_scale();
if (model->flags[Model::Submodel_flags::Do_not_scale_detail_distances]) {
box_scale = 1.0f;
}
if ( !( flags & MR_FULL_DETAIL ) && model->use_render_box ) {
vec3d box_min, box_max, offset;
if (model->flags[Model::Submodel_flags::Use_render_box_offset]) {
offset = model->render_box_offset;
} else {
model_find_submodel_offset(&offset, pm, submodel_num);
}
vm_vec_copy_scale(&box_min, &model->render_box_min, box_scale);
vm_vec_copy_scale(&box_max, &model->render_box_max, box_scale);
if ( (-model->use_render_box + in_box(&box_min, &box_max, &offset, view_pos)) ) {
return false;
}
}
if ( !(flags & MR_FULL_DETAIL) && model->use_render_sphere ) {
float sphere_radius = model->render_sphere_radius * box_scale;
// TODO: doesn't consider submodel rotations yet -zookeeper
vec3d offset;
if (model->flags[Model::Submodel_flags::Use_render_sphere_offset]) {
offset = model->render_sphere_offset;
} else {
model_find_submodel_offset(&offset, pm, submodel_num);
}
if ( (-model->use_render_sphere + in_sphere(&offset, sphere_radius, view_pos)) ) {
return false;
}
}
return true;
}
void submodel_render_immediate(const model_render_params *render_info, const polymodel *pm, const polymodel_instance *pmi, int submodel_num, const matrix *orient, const vec3d *pos)
{
Assert(!pmi || pm->id == pmi->model_num);
model_draw_list model_list;
model_list.init();
submodel_render_queue(render_info, &model_list, pm, pmi, submodel_num, orient, pos);
model_list.init_render();
model_list.render_all();
gr_zbias(0);
gr_zbuffer_set(ZBUFFER_TYPE_READ);
gr_set_cull(0);
gr_set_fill_mode(GR_FILL_MODE_SOLID);
gr_clear_states();
gr_reset_lighting();
}
void submodel_render_queue(const model_render_params *render_info, model_draw_list *scene, const polymodel *pm, const polymodel_instance *pmi, int submodel_num, const matrix *orient, const vec3d *pos)
{
Assert(!pmi || pm->id == pmi->model_num);
model_material rendering_material;
//MONITOR_INC( NumModelsRend, 1 );
if ( !( Game_detail_flags & DETAIL_FLAG_MODELS ) ) return;
if ( render_info->is_clip_plane_set() ) {
rendering_material.set_clip_plane(render_info->get_clip_plane_normal(), render_info->get_clip_plane_pos());
}
if ( render_info->is_team_color_set() ) {
rendering_material.set_team_color(render_info->get_team_color());
}
uint64_t flags = render_info->get_model_flags();
// Set the flags we will pass to the tmapper
uint tmap_flags = TMAP_FLAG_GOURAUD | TMAP_FLAG_RGB;
// if we're in nebula mode
if( ( The_mission.flags[Mission::Mission_Flags::Fullneb] ) && ( Neb2_render_mode != NEB2_RENDER_NONE ) ) {
tmap_flags |= TMAP_FLAG_PIXEL_FOG;
}
if ( !( flags & MR_NO_TEXTURING ) ) {
tmap_flags |= TMAP_FLAG_TEXTURED;
if ( ( pm->flags & PM_FLAG_ALLOW_TILING ) && tiling )
tmap_flags |= TMAP_FLAG_TILED;
if ( !( flags & MR_NO_CORRECT ) ) {
tmap_flags |= TMAP_FLAG_CORRECT;
}
}
if ( render_info->get_animated_effect_num() >= 0 ) {
tmap_flags |= TMAP_ANIMATED_SHADER;
rendering_material.set_animated_effect(render_info->get_animated_effect_num(), render_info->get_animated_effect_timer());
}
bool is_outlines_only_htl = (flags & MR_NO_POLYS) && (flags & MR_SHOW_OUTLINE_HTL);
//set to true since D3d and OGL need the api matrices set
scene->push_transform(pos, orient);
vec3d auto_back = ZERO_VECTOR;
bool set_autocen = model_render_determine_autocenter(&auto_back, pm, render_info->get_detail_level_lock(), flags);
if ( set_autocen ) {
scene->push_transform(&auto_back, NULL);
}
if (is_outlines_only_htl) {
rendering_material.set_fill_mode(GR_FILL_MODE_WIRE);
color outline_color = render_info->get_color();
rendering_material.set_color(outline_color);
tmap_flags &= ~TMAP_FLAG_RGB;
} else {
rendering_material.set_fill_mode(GR_FILL_MODE_SOLID);
}
rendering_material.set_light_factor(1.0f);
if ( !( flags & MR_NO_LIGHTING ) ) {
scene->set_light_filter(pos, pm->submodel[submodel_num].rad);
rendering_material.set_lighting(true);
} else {
rendering_material.set_lighting(false);
}
// fixes disappearing HUD in OGL - taylor
rendering_material.set_cull_mode(true);
// RT - Put this here to fog debris
if( tmap_flags & TMAP_FLAG_PIXEL_FOG ) {
unsigned char r, g, b;
neb2_get_fog_color(&r, &g, &b);
rendering_material.set_fog(r, g, b);
} else {
rendering_material.set_fog();
}
rendering_material.set_depth_bias(0);
vec3d view_pos = scene->get_view_position();
if ( model_render_check_detail_box(&view_pos, pm, submodel_num, flags) ) {
model_render_buffers(scene, &rendering_material, render_info, &pm->submodel[submodel_num].buffer, pm, submodel_num, 0, tmap_flags);
if ( pm->flags & PM_FLAG_TRANS_BUFFER && pm->submodel[submodel_num].trans_buffer.flags & VB_FLAG_TRANS ) {
model_render_buffers(scene, &rendering_material, render_info, &pm->submodel[submodel_num].trans_buffer, pm, submodel_num, 0, tmap_flags);
}
}
if ( pmi && !pmi->submodel[submodel_num].electrical_arcs.empty() ) {
model_render_add_lightning( scene, render_info, pm, &pmi->submodel[submodel_num] );
}
if ( set_autocen ) {
scene->pop_transform();
}
scene->pop_transform();
}
//Renders the sprite for a glowpoint.
void model_render_glowpoint_bitmap(int point_num, const vec3d *pos, const matrix *orient, const glow_point_bank *bank, const glow_point_bank_override *gpo, const polymodel *pm, const polymodel_instance *pmi, const ship* shipp, bool use_depth_buffer)
{
glow_point *gpt = &bank->points[point_num];
vec3d loc_offset = gpt->pnt;
vec3d loc_norm = gpt->norm;
vec3d world_pnt;
vec3d world_norm;
vec3d tempv;
vec3d submodel_static_offset; // The associated submodel's static offset in the ship's frame of reference
bool submodel_rotation = false;
if ( bank->submodel_parent > 0 && pm->submodel[bank->submodel_parent].flags[Model::Submodel_flags::Can_move] ) {
model_find_submodel_offset(&submodel_static_offset, pm, bank->submodel_parent);
submodel_rotation = true;
}
if ( submodel_rotation ) {
vm_vec_sub(&loc_offset, &gpt->pnt, &submodel_static_offset);
tempv = loc_offset;
if (pmi) {
if (IS_VEC_NULL(&loc_norm)) { // zero vectors are allowed for glowpoint norms
model_instance_local_to_global_point(&loc_offset, &tempv, pm, pmi, bank->submodel_parent);
}
else {
vec3d tempn = loc_norm;
model_instance_local_to_global_point_dir(&loc_offset, &loc_norm, &tempv, &tempn, pm, pmi, bank->submodel_parent);
}
}
}
vm_vec_unrotate(&world_pnt, &loc_offset, orient);
vm_vec_add2(&world_pnt, pos);
vm_vec_unrotate(&world_norm, &loc_norm, orient);
if (shipp != nullptr) {
// don't render if its on the wrong side of the portal
WarpEffect* warp_effect = nullptr;
if ((shipp->is_arriving()) && (shipp->warpin_effect != nullptr)
&& Warp_params[shipp->warpin_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpin_effect.get();
}
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr)
&& Warp_params[shipp->warpout_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpout_effect.get();
}
if (warp_effect != nullptr && point_is_clipped_by_warp(&world_pnt, warp_effect))
return;
}
switch ((gpo && gpo->type_override)?gpo->type:bank->type)
{
case 0:
{
float d = 1.0f;
float pulse = model_render_get_point_activation(bank, gpo);
if (pulse == 0.0f)
return;
if ( IS_VEC_NULL(&world_norm) ) {
d = 1.0f; //if given a nul vector then always show it
} else {
vm_vec_sub(&tempv,&View_position,&world_pnt);
vm_vec_normalize(&tempv);
d = vm_vec_dot(&tempv,&world_norm);
d -= 0.25;
}
float w = gpt->radius;
if (d > 0.0f) {
vertex p;
d *= 3.0f;
if (d > 1.0f)
d = 1.0f;
// fade them in the nebula as well
nebula_handle_alpha(d, &world_pnt, Neb2_fog_visibility_glowpoint);
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
//vec3d npnt;
//vm_vec_add(&npnt, &loc_offset, pos);
w *= 1.5; //make it bigger in a nebula (but fullneb only)
}
g3_transfer_vertex(&p, &world_pnt);
p.r = p.g = p.b = p.a = (ubyte)(255.0f * MAX(d,0.0f));
if((gpo && gpo->glow_bitmap_override)?(gpo->glow_bitmap > -1):(bank->glow_bitmap > -1)) {
int bitmap_id = (gpo && gpo->glow_bitmap_override) ? gpo->glow_bitmap : bank->glow_bitmap;
if ( use_depth_buffer ) {
batching_add_volume_bitmap(bitmap_id, &p, 0, (w * 0.5f), d * pulse);
} else {
batching_add_bitmap(bitmap_id, &p, 0, (w * 0.5f), d * pulse);
}
}
}
break;
}
case 1:
{
vertex verts[4];
vec3d fvec, top1, bottom1, top2, bottom2, start, end;
vm_vec_add2(&loc_norm, &loc_offset);
vm_vec_rotate(&start, &loc_offset, orient);
vm_vec_rotate(&end, &loc_norm, orient);
vm_vec_sub(&fvec, &end, &start);
vm_vec_normalize(&fvec);
moldel_calc_facing_pts(&top1, &bottom1, &fvec, &loc_offset, gpt->radius, 1.0f, &View_position);
moldel_calc_facing_pts(&top2, &bottom2, &fvec, &loc_norm, gpt->radius, 1.0f, &View_position);
g3_transfer_vertex(&verts[0], &bottom1);
g3_transfer_vertex(&verts[1], &bottom2);
g3_transfer_vertex(&verts[2], &top2);
g3_transfer_vertex(&verts[3], &top1);
verts[0].texture_position.u = 0.0f;
verts[0].texture_position.v = 0.0f;
verts[1].texture_position.u = 1.0f;
verts[1].texture_position.v = 0.0f;
verts[2].texture_position.u = 1.0f;
verts[2].texture_position.v = 1.0f;
verts[3].texture_position.u = 0.0f;
verts[3].texture_position.v = 1.0f;
vm_vec_sub(&tempv,&View_position,&loc_offset);
vm_vec_normalize(&tempv);
if ( The_mission.flags[Mission::Mission_Flags::Fullneb] ) {
batching_add_quad(bank->glow_neb_bitmap, verts);
} else {
batching_add_quad(bank->glow_bitmap, verts);
}
break;
}
}
}
//adds the glowpoint's lights, if any, to the lights vector.
void model_render_glowpoint_add_light(int point_num, const vec3d *pos, const matrix *orient, const glow_point_bank *bank, const glow_point_bank_override *gpo, const polymodel *pm, const polymodel_instance *pmi, const ship* shipp)
{
if(Detail.lighting <= 3 || !Deferred_lighting || gpo==nullptr || !gpo->is_lightsource) {
return;
}
glow_point *gpt = &bank->points[point_num];
vec3d loc_offset = gpt->pnt;
vec3d loc_norm = gpt->norm;
vec3d world_pnt;
vec3d world_norm;
vec3d tempv;
vec3d submodel_static_offset; // The associated submodel's static offset in the ship's frame of reference
bool submodel_rotation = false;
if ( bank->submodel_parent > 0 && pm->submodel[bank->submodel_parent].flags[Model::Submodel_flags::Can_move] && shipp != nullptr ) {
model_find_submodel_offset(&submodel_static_offset, pm, bank->submodel_parent);
submodel_rotation = true;
}
if ( submodel_rotation ) {
vm_vec_sub(&loc_offset, &gpt->pnt, &submodel_static_offset);
tempv = loc_offset;
if (!pmi){
model_local_to_global_point(&loc_offset, &tempv, pm, bank->submodel_parent);
} else if (IS_VEC_NULL(&loc_norm)) { // zero vectors are allowed for glowpoint norms
model_instance_local_to_global_point(&loc_offset, &tempv, pm, pmi, bank->submodel_parent);
} else {
vec3d tempn = loc_norm;
model_instance_local_to_global_point_dir(&loc_offset, &loc_norm, &tempv, &tempn, pm, pmi, bank->submodel_parent);
}
}
vm_vec_unrotate(&world_pnt, &loc_offset, orient);
vm_vec_add2(&world_pnt, pos);
vm_vec_unrotate(&world_norm, &loc_norm, orient);
if (shipp != nullptr && shipp->warpout_effect != nullptr) {
// don't render if its on the wrong side of the portal
WarpEffect* warp_effect = nullptr;
if ((shipp->is_arriving()) && Warp_params[shipp->warpin_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpin_effect.get();
}
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && Warp_params[shipp->warpout_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpout_effect.get();
}
if (warp_effect != nullptr && point_is_clipped_by_warp(&world_pnt, warp_effect))
return;
}
if( (gpo->type_override?gpo->type:bank->type)==0)
{
float pulse = model_render_get_point_activation(bank, gpo);
if (pulse == 0.0f)
return;
vec3d lightcolor;
//fade between mix and normal color depending on pulse state
lightcolor.xyz.x =pulse * gpo->light_color.xyz.x + (1.0f-pulse) * gpo->light_mix_color.xyz.x;
lightcolor.xyz.y =pulse * gpo->light_color.xyz.y + (1.0f-pulse) * gpo->light_mix_color.xyz.y;
lightcolor.xyz.z =pulse * gpo->light_color.xyz.z + (1.0f-pulse) * gpo->light_mix_color.xyz.z;
float light_radius = gpt->radius * gpo->radius_multi;
if ( gpo->lightcone ) {
vec3d cone_dir_rot;
vec3d cone_dir_model;
vec3d cone_dir_world;
vec3d cone_dir_screen;
if ( gpo->rotating ) {
vm_rot_point_around_line(&cone_dir_rot, &gpo->cone_direction, PI * timestamp() * 0.000033333f * gpo->rotation_speed, &vmd_zero_vector, &gpo->rotation_axis);
} else {
cone_dir_rot = gpo->cone_direction;
}
if (pmi)
model_instance_local_to_global_dir(&cone_dir_model, &cone_dir_rot, pm, pmi, bank->submodel_parent);
else
cone_dir_model = cone_dir_rot;
vm_vec_unrotate(&cone_dir_world, &cone_dir_model, orient);
vm_vec_rotate(&cone_dir_screen, &cone_dir_world, &Eye_matrix);
cone_dir_screen.xyz.z = -cone_dir_screen.xyz.z;
light_add_cone(
&world_pnt, &cone_dir_screen, gpo->cone_angle, gpo->cone_inner_angle, gpo->dualcone, 1.0f, light_radius, gpo->intensity,
lightcolor.xyz.x, lightcolor.xyz.y, lightcolor.xyz.z, gpt->radius);
} else {
light_add_point(&world_pnt, 1.0f, light_radius, gpo->intensity, lightcolor.xyz.x, lightcolor.xyz.y, lightcolor.xyz.z, gpt->radius);
}
}
}
//Returns the current brightness percentage of a glowpoint, from 1.0f to 0.0f
float model_render_get_point_activation(const glow_point_bank* bank, const glow_point_bank_override* gpo)
{
if(gpo == nullptr || !(gpo->pulse_type)){
return 1.0f;
}
float pulse = 1.0f;
int period = 0;
if (gpo->pulse_period_override) {
period = gpo->pulse_period;
} else {
if (gpo->on_time_override) {
period = 2 * gpo->on_time;
} else {
period = 2 * bank->on_time;
}
}
if (period == 0)
return 0.0f;
int x = 0;
int disp_time = gpo->disp_time_override ? gpo->disp_time : bank->disp_time;
int on_time = gpo->on_time_override ? gpo->on_time : bank->on_time;
int off_time = gpo->off_time_override ? gpo->off_time : bank->off_time;
if ( off_time) {
x = (timestamp() - disp_time) % ( on_time + off_time ) - off_time;
} else {
x = (timestamp() - disp_time) % period;
}
switch (gpo->pulse_type) {
case PULSE_SIN:
pulse = gpo->pulse_bias + gpo->pulse_amplitude * pow(sin(PI2 / period * x),gpo->pulse_exponent);
break;
case PULSE_COS:
pulse = gpo->pulse_bias + gpo->pulse_amplitude * pow(cos(PI2 / period * x),gpo->pulse_exponent);
break;
case PULSE_SHIFTTRI:
x += period / 4;
if(off_time) {
x %= on_time + off_time;
} else {
x %= period;
}
FALLTHROUGH;
case PULSE_TRI:
float inv;
if( x > period / 2) {
inv = -1;
} else {
inv = 1;
}
if( x > period / 4) {
pulse = gpo->pulse_bias + gpo->pulse_amplitude * inv * pow( 1.0f - ((x - period / 4.0f) * 4 / period) ,gpo->pulse_exponent);
} else {
pulse = gpo->pulse_bias + gpo->pulse_amplitude * inv * pow( (x * 4.0f / period) ,gpo->pulse_exponent);
}
break;
}
return pulse;
}
void model_render_set_glow_points(const polymodel *pm, int objnum)
{
int time = timestamp();
glow_point_bank_override *gpo = nullptr;
bool override_all = false;
ship_info *sip = nullptr;
ship *shipp = nullptr;
if ( Glowpoint_override ) {
return;
}
if ( objnum > -1 ) {
object *objp = &Objects[objnum];
if ( objp != NULL && objp->type == OBJ_SHIP ) {
shipp = &Ships[Objects[objnum].instance];
sip = &Ship_info[shipp->ship_info_index];
auto gpoi = sip->glowpoint_bank_override_map.find(-1);
if (gpoi != sip->glowpoint_bank_override_map.end()) {
override_all = true;
gpo = &glowpoint_bank_overrides[gpoi->second];
}
}
}
for ( int i = 0; i < pm->n_glow_point_banks; i++ ) { //glow point blink code -Bobboau
glow_point_bank *bank = &pm->glow_point_banks[i];
if ( !override_all && sip ) {
auto gpoi = sip->glowpoint_bank_override_map.find(i);
if ( gpoi != sip->glowpoint_bank_override_map.end() ) {
gpo = &glowpoint_bank_overrides[gpoi->second];
} else {
gpo = nullptr;
}
}
if ( bank->glow_timestamp == 0 ) {
bank->glow_timestamp=time;
}
int on_time = (gpo && gpo->on_time_override) ? gpo->on_time : bank->on_time;
int off_time = (gpo && gpo->off_time_override) ? gpo->off_time : bank->off_time;
int disp_time = (gpo && gpo->disp_time_override) ? gpo->disp_time : bank->disp_time;
if (off_time) {
bool glow_state = ((time - disp_time) % (on_time + off_time)) < on_time;
if ( glow_state != bank->is_on )
bank->glow_timestamp = time;
bank->is_on = glow_state;
}
}
}
//Handle all the glow points of a model being rendered.
void model_render_glow_points(const polymodel *pm, const polymodel_instance *pmi, const ship *shipp, const matrix *orient, const vec3d *pos, bool use_depth_buffer = true, bool render_sprites = true, bool add_lights= true)
{
Assert(pmi == nullptr || pm->id == pmi->model_num);
int i, j;
int cull = gr_set_cull(0);
glow_point_bank_override *gpo = nullptr;
bool override_all = false;
ship_info *sip = nullptr;
if ( shipp ) {
sip = &Ship_info[shipp->ship_info_index];
auto gpoi = sip->glowpoint_bank_override_map.find(-1);
if(gpoi != sip->glowpoint_bank_override_map.end()) {
override_all = true;
gpo = &glowpoint_bank_overrides[gpoi->second];
}
}
for (i = 0; i < pm->n_glow_point_banks; i++ ) {
glow_point_bank *bank = &pm->glow_point_banks[i];
if(!override_all && sip) {
auto gpoi = sip->glowpoint_bank_override_map.find(i);
if(gpoi != sip->glowpoint_bank_override_map.end()) {
gpo = &glowpoint_bank_overrides[gpoi->second];
} else {
gpo = nullptr;
}
}
//Only continue if there actually is a glowpoint bitmap available
if (bank->glow_bitmap == -1)
continue;
if ((pmi != nullptr) && (bank->submodel_parent > -1)) {
auto smi = &pmi->submodel[bank->submodel_parent];
if (smi->blown_off) {
continue;
}
}
if ((gpo && gpo->off_time_override && !gpo->off_time)?gpo->is_on:bank->is_on) {
if ( (shipp != nullptr) && !(shipp->glow_point_bank_active[i]) )
continue;
for (j = 0; j < bank->num_points; j++) {
Assert( bank->points != nullptr );
int flick;
if (pmi != nullptr && !pmi->submodel[pm->detail[0]].electrical_arcs.empty()) {
flick = static_rand( timestamp() % 20 ) % (pmi->submodel[pm->detail[0]].electrical_arcs.size() + j); //the more damage, the more arcs, the more likely the lights will fail
} else {
flick = 1;
}
if (flick == 1) {
//In some cases we want to be able to render only the bitmaps or only the lights
if(render_sprites)
model_render_glowpoint_bitmap(j, pos, orient, bank, gpo, pm, pmi, shipp, use_depth_buffer);
if(add_lights)
model_render_glowpoint_add_light(j, pos, orient, bank, gpo, pm, pmi, shipp);
} // flick
} // for slot
} // bank is on
} // for bank
gr_set_cull(cull);
}
// These scaling functions were adapted from Elecman's code.
// https://forum.unity.com/threads/this-script-gives-you-objects-screen-size-in-pixels.48966/#post-2107126
float convert_pixel_size_and_distance_to_diameter(float pixelsize, float distance, float field_of_view, int screen_width)
{
float diameter = (pixelsize * distance * tanf(field_of_view)) / (screen_width);
return diameter;
}
// These scaling functions were adapted from Elecman's code.
// https://forum.unity.com/threads/this-script-gives-you-objects-screen-size-in-pixels.48966/#post-2107126
float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view, int screen_width)
{
float pixel_size = (diameter * screen_width) / (distance * tanf(field_of_view));
return pixel_size;
}
float model_render_get_diameter_clamped_to_min_pixel_size(const vec3d* pos, float diameter, float min_pixel_size)
{
// Don't do any scaling math if the pixel size is set to zero.
if (fl_near_zero(min_pixel_size))
return diameter;
float distance_to_eye = vm_vec_dist(&Eye_position, pos);
float current_pixel_size = convert_distance_and_diameter_to_pixel_size(
distance_to_eye,
diameter,
g3_get_hfov(Eye_fov),
gr_screen.max_w);
float scaled_diameter = diameter;
if (current_pixel_size < min_pixel_size) {
scaled_diameter = convert_pixel_size_and_distance_to_diameter(
min_pixel_size,
distance_to_eye,
g3_get_hfov(Eye_fov),
gr_screen.max_w);
}
return scaled_diameter;
}
void model_queue_render_thrusters(const model_render_params *interp, const polymodel *pm, int objnum, const ship *shipp, const matrix *orient, const vec3d *pos)
{
int i, j;
int n_q = 0;
size_t k;
vec3d norm, norm2, fvec, pnt, npnt;
thruster_bank *bank = nullptr;
polymodel_instance *pmi = nullptr;
vertex p;
bool do_render = false;
if ( pm == NULL ) {
Int3();
return;
}
if ( !(interp->get_model_flags() & MR_SHOW_THRUSTERS) ) {
return;
}
// get an initial count to figure out how man geo batchers we need allocated
for (i = 0; i < pm->n_thrusters; i++ ) {
bank = &pm->thrusters[i];
n_q += bank->num_points;
}
if (n_q <= 0) {
return;
}
const mst_info& thruster_info = interp->get_thruster_info();
// primary_thruster_batcher
if (thruster_info.primary_glow_bitmap >= 0) {
do_render = true;
}
// secondary_thruster_batcher
if (thruster_info.secondary_glow_bitmap >= 0) {
do_render = true;
}
// tertiary_thruster_batcher
if (thruster_info.tertiary_glow_bitmap >= 0) {
do_render = true;
}
if (do_render == false) {
return;
}
// this is used for the secondary thruster glows
// it only needs to be calculated once so I'm doing it here -Bobboau
norm.xyz.z = -1.0f;
norm.xyz.x = 1.0f;
norm.xyz.y = -1.0f;
norm.xyz.x *= thruster_info.rotvel.xyz.y/2;
norm.xyz.y *= thruster_info.rotvel.xyz.x/2;
vm_vec_normalize(&norm);
for (i = 0; i < pm->n_thrusters; i++ ) {
vec3d submodel_static_offset; // The associated submodel's static offset in the ship's frame of reference
bool submodel_rotation = false;
bank = &pm->thrusters[i];
// don't draw this thruster if the engine is destroyed or just not on
if ( !model_should_render_engine_glow(objnum, bank->obj_num) )
continue;
// If bank is attached to a submodel, prepare to account for rotations
//
// TODO: This won't work in the ship lab, because the lab code doesn't
// set the the necessary submodel instance info needed here. The second
// condition is thus a hack to disable the feature while in the lab, and
// can be removed if the lab is re-structured accordingly. -zookeeper
if ( bank->submodel_num > -1 && pm->submodel[bank->submodel_num].flags[Model::Submodel_flags::Can_move] && (gameseq_get_state_idx(GS_STATE_LAB) == -1) ) {
model_find_submodel_offset(&submodel_static_offset, pm, bank->submodel_num);
submodel_rotation = true;
}
for (j = 0; j < bank->num_points; j++) {
Assert( bank->points != NULL );
float d;
vec3d tempv;
glow_point *gpt = &bank->points[j];
vec3d loc_offset = gpt->pnt;
vec3d loc_norm = gpt->norm;
vec3d world_pnt = loc_offset;
vec3d world_norm;
if ( submodel_rotation ) {
vm_vec_sub(&loc_offset, &gpt->pnt, &submodel_static_offset);
if (pmi == nullptr)
pmi = model_get_instance(shipp->model_instance_num);
if (IS_VEC_NULL(&loc_norm)) { // zero vectors are allowed for glowpoint norms
model_instance_local_to_global_point(&world_pnt, &loc_offset, pm, pmi, bank->submodel_num);
} else {
vec3d tempn = loc_norm;
model_instance_local_to_global_point_dir(&world_pnt, &loc_norm, &loc_offset, &tempn, pm, pmi, bank->submodel_num);
}
}
tempv = world_pnt;
vm_vec_unrotate(&world_pnt, &tempv, orient);
vm_vec_add2(&world_pnt, pos);
if (shipp) {
// if ship is warping out, check position of the engine glow to the warp plane
WarpEffect* warp_effect = nullptr;
if ((shipp->is_arriving()) && (shipp->warpin_effect != nullptr)
&& Warp_params[shipp->warpin_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpin_effect.get();
}
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr)
&& Warp_params[shipp->warpout_params_index].warp_type != WT_HYPERSPACE) {
warp_effect = shipp->warpout_effect.get();
}
if (warp_effect != nullptr && point_is_clipped_by_warp(&world_pnt, warp_effect))
continue;
}
vm_vec_sub(&tempv, &View_position, &world_pnt);
vm_vec_normalize(&tempv);
vm_vec_unrotate(&world_norm, &loc_norm, orient);
d = vm_vec_dot(&tempv, &world_norm);
// ADAM: Min throttle draws rad*MIN_SCALE, max uses max.
#define NOISE_SCALE 0.5f
#define MIN_SCALE 3.4f
#define MAX_SCALE 4.7f
float magnitude;
vec3d scale_vec = { { { 1.0f, 0.0f, 0.0f } } };
// normalize banks, in case of incredibly big normals
if ( !IS_VEC_NULL_SQ_SAFE(&loc_norm) )
vm_vec_copy_normalize(&scale_vec, &loc_norm);
// adjust for thrust
(scale_vec.xyz.x *= thruster_info.length.xyz.x) -= 0.1f;
(scale_vec.xyz.y *= thruster_info.length.xyz.y) -= 0.1f;
(scale_vec.xyz.z *= thruster_info.length.xyz.z) -= 0.1f;
// get magnitude, which we will use as the scaling reference
magnitude = vm_vec_normalize(&scale_vec);
// get absolute value
if (magnitude < 0.0f)
magnitude *= -1.0f;
float scale = magnitude * (MAX_SCALE - MIN_SCALE) + MIN_SCALE;
if (d > 0.0f){
// Make glow bitmap fade in/out quicker from sides.
d *= 3.0f;
if (d > 1.0f)
d = 1.0f;
}
float fog_int = 1.0f;
// fade them in the nebula as well
vm_vec_unrotate(&npnt, &gpt->pnt, orient);
vm_vec_add2(&npnt, pos);
nebula_handle_alpha(fog_int, &npnt, Neb2_fog_visibility_thruster);
d *= fog_int;
// Scale the thrusters so they always appears at least some configured amount of pixels wide.
float scaled_thruster_radius = model_render_get_diameter_clamped_to_min_pixel_size(
&world_pnt,
gpt->radius * 2.0f,
Min_pixel_size_thruster);
scaled_thruster_radius /= 2.0f;
float w = scaled_thruster_radius * (scale + thruster_info.glow_noise * NOISE_SCALE);
// these lines are used by the tertiary glows, thus we will need to project this all of the time
g3_transfer_vertex( &p, &world_pnt );
// these values are not used, but just appease the linters and future-proof
p.screen.xyw.x = 0.0f; p.screen.xyw.y = 0.0f; p.screen.xyw.w = 0.0f;
// start primary thruster glows
if ( (thruster_info.primary_glow_bitmap >= 0) && (d > 0.0f) ) {
p.r = p.g = p.b = p.a = (ubyte)(255.0f * d);
batching_add_volume_bitmap(thruster_info.primary_glow_bitmap, &p, 0, (w * 0.5f * thruster_info.glow_rad_factor), d, w * 0.325f);
}
// start tertiary thruster glows
if (thruster_info.tertiary_glow_bitmap >= 0) {
p.screen.xyw.w -= w;
p.r = p.g = p.b = p.a = (ubyte)(255.0f * fog_int);
batching_add_volume_bitmap_rotated(thruster_info.tertiary_glow_bitmap, &p, magnitude * 4, w * 0.6f * thruster_info.tertiary_glow_rad_factor, fog_int, -w*0.5f);
}
// begin secondary glows
if (thruster_info.secondary_glow_bitmap >= 0) {
pnt = world_pnt;
scale = magnitude * (MAX_SCALE - (MIN_SCALE / 2)) + (MIN_SCALE / 2);
vm_vec_unrotate(&world_norm, &norm, orient);
d = vm_vec_dot(&tempv, &world_norm);
d += 0.75f;
d *= 3.0f;
if (d > 1.0f)
d = 1.0f;
if (d > 0.0f) {
vm_vec_add(&norm2, &world_norm, &pnt);
vm_vec_sub(&fvec, &norm2, &pnt);
vm_vec_normalize(&fvec);
float wVal = gpt->radius * scale * 2;
vm_vec_scale_add(&norm2, &pnt, &fvec, wVal * 2 * thruster_info.glow_length_factor);
if (The_mission.flags[Mission::Mission_Flags::Fullneb]) {
vm_vec_add(&npnt, &pnt, pos);
}
d *= fog_int;
batching_add_beam(thruster_info.secondary_glow_bitmap, &pnt, &norm2, wVal*thruster_info.secondary_glow_rad_factor*0.5f, d);
if (Scene_framebuffer_in_frame && thruster_info.draw_distortion &&
Gr_framebuffer_effects[FramebufferEffects::Thrusters]) {
vm_vec_scale_add(&norm2, &pnt, &fvec, wVal * 2 * thruster_info.distortion_length_factor);
int dist_bitmap;
if (thruster_info.distortion_bitmap > 0) {
dist_bitmap = thruster_info.distortion_bitmap;
}
else {
dist_bitmap = thruster_info.secondary_glow_bitmap;
}
float mag = vm_vec_mag(&gpt->pnt);
mag -= (float)((int)mag);//Valathil - Get a fairly random but constant number to offset the distortion texture
batching_add_distortion_beam(dist_bitmap, &pnt, &norm2, wVal*thruster_info.distortion_rad_factor*0.5f, 1.0f, mag);
}
}
}
// begin particles
if (shipp) {
ship_info *sip = &Ship_info[shipp->ship_info_index];
thruster_particles *tp;
size_t num_particles = 0;
if (thruster_info.use_ab)
num_particles = sip->afterburner_thruster_particles.size();
else
num_particles = sip->normal_thruster_particles.size();
for (k = 0; k < num_particles; k++) {
if (thruster_info.use_ab)
tp = &sip->afterburner_thruster_particles[k];
else
tp = &sip->normal_thruster_particles[k];
auto source = particle::ParticleManager::get()->createSource(tp->particle_handle);
//This is a 180° flip around y matrix
matrix orientParticle = { { { { { { -1.0f, 0.0f, 0.0f } } }, { { { 0.0f, 1.0f, 0.0f } } }, { { { 0.0f, 0.0f, -1.0f } } } } } };
std::unique_ptr<EffectHost> host;
if (bank->submodel_num < 0)
host = std::make_unique<EffectHostObject>(&Objects[shipp->objnum], loc_offset, orientParticle);
else
host = std::make_unique<EffectHostSubmodel>(&Objects[shipp->objnum], bank->submodel_num, loc_offset, orientParticle);
source->setHost(std::move(host));
source->setTriggerRadius(gpt->radius);
source->setTriggerVelocity(vm_vec_mag_quick(&Objects[shipp->objnum].phys_info.desired_vel));
source->finishCreation();
}
}
}
}
}
void model_render_insignias(const insignia_draw_data *insignia_data)
{
auto pm = insignia_data->pm;
int detail_level = insignia_data->detail_level;
int bitmap_num = insignia_data->bitmap_num;
// if the model has no insignias, or we don't have a texture, then bail
if ( (pm->num_ins <= 0) || (bitmap_num < 0) )
return;
int idx, s_idx;
vertex vecs[3];
vec3d t1, t2, t3;
int i1, i2, i3;
material insignia_material;
insignia_material.set_depth_bias(1);
// set the proper texture
material_set_unlit(&insignia_material, bitmap_num, 0.65f, true, true);
if ( insignia_data->clip ) {
insignia_material.set_clip_plane(insignia_data->clip_normal, insignia_data->clip_position);
}
// otherwise render them
for(idx=0; idx<pm->num_ins; idx++){
// skip insignias not on our detail level
if(pm->ins[idx].detail_level != detail_level){
continue;
}
for(s_idx=0; s_idx<pm->ins[idx].num_faces; s_idx++){
// get vertex indices
i1 = pm->ins[idx].faces[s_idx][0];
i2 = pm->ins[idx].faces[s_idx][1];
i3 = pm->ins[idx].faces[s_idx][2];
// transform vecs and setup vertices
vm_vec_add(&t1, &pm->ins[idx].vecs[i1], &pm->ins[idx].offset);
vm_vec_add(&t2, &pm->ins[idx].vecs[i2], &pm->ins[idx].offset);
vm_vec_add(&t3, &pm->ins[idx].vecs[i3], &pm->ins[idx].offset);
g3_transfer_vertex(&vecs[0], &t1);
g3_transfer_vertex(&vecs[1], &t2);
g3_transfer_vertex(&vecs[2], &t3);
// setup texture coords
vecs[0].texture_position.u = pm->ins[idx].u[s_idx][0];
vecs[0].texture_position.v = pm->ins[idx].v[s_idx][0];
vecs[1].texture_position.u = pm->ins[idx].u[s_idx][1];
vecs[1].texture_position.v = pm->ins[idx].v[s_idx][1];
vecs[2].texture_position.u = pm->ins[idx].u[s_idx][2];
vecs[2].texture_position.v = pm->ins[idx].v[s_idx][2];
light_apply_rgb( &vecs[0].r, &vecs[0].g, &vecs[0].b, &pm->ins[idx].vecs[i1], &pm->ins[idx].norm[i1], 1.5f );
light_apply_rgb( &vecs[1].r, &vecs[1].g, &vecs[1].b, &pm->ins[idx].vecs[i2], &pm->ins[idx].norm[i2], 1.5f );
light_apply_rgb( &vecs[2].r, &vecs[2].g, &vecs[2].b, &pm->ins[idx].vecs[i3], &pm->ins[idx].norm[i3], 1.5f );
vecs[0].a = vecs[1].a = vecs[2].a = 255;
// draw the polygon
g3_render_primitives_colored_textured(&insignia_material, vecs, 3, PRIM_TYPE_TRIFAN, false);
}
}
}
SCP_vector<vec3d> Arc_segment_points;
void model_render_arc(const vec3d *v1, const vec3d *v2, const SCP_vector<vec3d> *persistent_arc_points, const color *primary, const color *secondary, float arc_width, ubyte depth_limit)
{
int size;
const vec3d *pvecs;
if (persistent_arc_points) {
size = static_cast<int>(persistent_arc_points->size());
pvecs = persistent_arc_points->data();
} else {
Arc_segment_points.clear();
// need to add the first point
Arc_segment_points.push_back(*v1);
// this should fill in all of the middle, and the last, points
interp_generate_arc_segment(Arc_segment_points, v1, v2, depth_limit, 0);
size = static_cast<int>(Arc_segment_points.size());
pvecs = Arc_segment_points.data();
}
// use primary color for fist pass
Assert( primary );
g3_render_rod(primary, size, pvecs, arc_width);
if (secondary) {
g3_render_rod(secondary, size, pvecs, arc_width * 0.33f);
}
}
void model_render_debug_children(const polymodel *pm, int mn, int detail_level, uint64_t debug_flags)
{
int i;
if ( (mn < 0) || (mn >= pm->n_models) ) {
Int3();
return;
}
bsp_info *model = &pm->submodel[mn];
// Get submodel rotation data and use submodel orientation matrix
// to put together a matrix describing the final orientation of
// the submodel relative to its parent
// (Not needed here because we're not using model instances)
matrix submodel_orient = vmd_identity_matrix;
g3_start_instance_matrix(&model->offset, &submodel_orient, true);
if ( debug_flags & MR_DEBUG_PIVOTS ) {
model_draw_debug_points( pm, &pm->submodel[mn], debug_flags );
}
i = model->first_child;
while ( i >= 0 ) {
model_render_debug_children( pm, i, detail_level, debug_flags );
i = pm->submodel[i].next_sibling;
}
g3_done_instance(true);
}
void model_render_debug(int model_num, const matrix *orient, const vec3d *pos, uint64_t flags, uint debug_flags, int objnum, int detail_level_locked )
{
polymodel *pm = model_get(model_num);
g3_start_instance_matrix(pos, orient, true);
if ( debug_flags & MR_DEBUG_RADIUS ) {
if ( !( flags & MR_SHOW_OUTLINE_PRESET ) ) {
gr_set_color(0,64,0);
g3_draw_sphere_ez(&vmd_zero_vector,pm->rad);
}
}
float depth = model_render_determine_depth(objnum, model_num, orient, pos, detail_level_locked);
int detail_level = model_render_determine_detail(depth, model_num, detail_level_locked);
vec3d auto_back = ZERO_VECTOR;
bool set_autocen = model_render_determine_autocenter(&auto_back, pm, detail_level, flags);
if ( set_autocen ) {
g3_start_instance_matrix(&auto_back, NULL, true);
}
uint save_gr_zbuffering_mode = gr_zbuffer_set(GR_ZBUFF_READ);
int i = pm->submodel[pm->detail[detail_level]].first_child;
while ( i >= 0 ) {
model_render_debug_children( pm, i, detail_level, flags );
i = pm->submodel[i].next_sibling;
}
if ( debug_flags & MR_DEBUG_PIVOTS ) {
model_draw_debug_points( pm, NULL, flags );
model_draw_debug_points( pm, &pm->submodel[pm->detail[detail_level]], flags );
if ( pm->flags & PM_FLAG_AUTOCEN ) {
gr_set_color(255, 255, 255);
g3_draw_sphere_ez(&pm->autocenter, pm->rad / 4.5f);
}
}
if ( debug_flags & MR_DEBUG_SHIELDS ) {
model_render_shields(pm, flags);
}
if ( debug_flags & MR_DEBUG_PATHS ) {
model_draw_paths_htl(model_num, flags);
}
if ( debug_flags & MR_DEBUG_BAY_PATHS ) {
model_draw_bay_paths_htl(model_num);
}
if ( debug_flags & MR_DEBUG_DOCK_POINTS ) {
model_draw_dock_points_htl(model_num);
}
if ( (flags & MR_AUTOCENTER) && (set_autocen) ) {
g3_done_instance(true);
}
g3_done_instance(true);
gr_zbuffer_set(save_gr_zbuffering_mode);
}
void model_render_immediate(const model_render_params* render_info, int model_num, const matrix* orient, const vec3d* pos, int render, bool sort)
{
model_render_immediate(render_info, model_num, -1, orient, pos, render, sort);
}
void model_render_immediate(const model_render_params* render_info, int model_num, int model_instance_num, const matrix* orient, const vec3d* pos, int render, bool sort)
{
model_draw_list model_list;
model_list.init();
model_render_queue(render_info, &model_list, model_num, model_instance_num, orient, pos);
model_list.init_render(sort);
switch ( render ) {
case MODEL_RENDER_OPAQUE:
model_list.render_all(ZBUFFER_TYPE_FULL);
break;
case MODEL_RENDER_TRANS:
model_list.render_all(ZBUFFER_TYPE_READ);
model_list.render_all(ZBUFFER_TYPE_NONE);
break;
case MODEL_RENDER_ALL:
model_list.render_all();
break;
}
model_list.render_outlines();
model_list.render_insignias();
model_list.render_arcs();
gr_zbias(0);
gr_set_cull(0);
gr_zbuffer_set(ZBUFFER_TYPE_READ);
gr_set_fill_mode(GR_FILL_MODE_SOLID);
gr_clear_states();
gr_reset_lighting();
if ( render_info->get_debug_flags() ) {
model_render_debug(model_num, orient, pos, render_info->get_model_flags(), render_info->get_debug_flags(), render_info->get_object_number(), render_info->get_detail_level_lock());
}
}
void model_render_queue(const model_render_params* interp, model_draw_list* scene, int model_num, const matrix* orient, const vec3d* pos)
{
model_render_queue(interp, scene, model_num, -1, orient, pos);
}
void model_render_queue(const model_render_params* interp, model_draw_list* scene, int model_num, int model_instance_num, const matrix* orient, const vec3d* pos)
{
int i;
const int objnum = interp->get_object_number();
const uint64_t model_flags = interp->get_model_flags();
model_material rendering_material;
polymodel *pm = model_get(model_num);
polymodel_instance *pmi = NULL;
float light_factor = model_render_determine_light_factor(interp, pos, model_flags);
if ( light_factor < (1.0f/32.0f) ) {
// If it is too far, exit
return;
}
rendering_material.set_light_factor(light_factor);
if ( interp->is_clip_plane_set() ) {
rendering_material.set_clip_plane(interp->get_clip_plane_normal(), interp->get_clip_plane_pos());
}
if ( interp->is_team_color_set() ) {
rendering_material.set_team_color(interp->get_team_color());
}
if ( model_flags & MR_FORCE_CLAMP ) {
rendering_material.set_texture_addressing(TMAP_ADDRESS_CLAMP);
} else {
rendering_material.set_texture_addressing(TMAP_ADDRESS_WRAP);
}
model_render_set_glow_points(pm, objnum);
if ( !(model_flags & MR_NO_LIGHTING) ) {
scene->set_light_filter(pos, pm->rad);
}
ship *shipp = nullptr;
object *objp = nullptr;
if (objnum >= 0) {
objp = &Objects[objnum];
int tentative_num = -1;
if (objp->type == OBJ_SHIP) {
shipp = &Ships[objp->instance];
tentative_num = shipp->model_instance_num;
}
else {
tentative_num = object_get_model_instance_num(objp);
}
if (tentative_num >= 0) {
model_instance_num = tentative_num;
}
}
// is this a skybox with a rotating submodel?
if (model_num == Nmodel_num && Nmodel_instance_num >= 0) {
model_instance_num = Nmodel_instance_num;
}
if (model_instance_num >= 0) {
pmi = model_get_instance(model_instance_num);
// This can happen if we supply a HUD target model for a real ship.
// The passed parameter was -1 but it was assigned an instance from
// the actual object. Set it back to -1 if there is a mismatch.
if (pmi->model_num != pm->id) {
model_instance_num = -1;
pmi = nullptr;
}
}
// Set the flags we will pass to the tmapper
uint tmap_flags = TMAP_FLAG_GOURAUD | TMAP_FLAG_RGB;
if ( !(model_flags & MR_NO_TEXTURING) ) {
tmap_flags |= TMAP_FLAG_TEXTURED;
if ( (pm->flags & PM_FLAG_ALLOW_TILING) && tiling)
tmap_flags |= TMAP_FLAG_TILED;
if ( !(model_flags & MR_NO_CORRECT) ) {
tmap_flags |= TMAP_FLAG_CORRECT;
}
}
if ( model_flags & MR_DESATURATED ) {
rendering_material.set_desaturation(true);
}
if ( interp->get_animated_effect_num() >= 0 ) {
rendering_material.set_animated_effect(interp->get_animated_effect_num(), interp->get_animated_effect_timer());
}
bool is_outlines_only = (model_flags & MR_NO_POLYS) && ((model_flags & MR_SHOW_OUTLINE_PRESET) || (model_flags & MR_SHOW_OUTLINE));
bool is_outlines_only_htl = (model_flags & MR_NO_POLYS) && (model_flags & MR_SHOW_OUTLINE_HTL);
float depth;
if (interp->get_attached_model_draw_distance() >= 0.0f) {
// this is an attached weapon model, rendered inside its parent's pushed transform, so pos
// is in the parent's frame; measure the depth from the eye position in that same frame
vec3d local_eye = scene->get_view_position();
depth = model_render_determine_depth(objnum, model_num, orient, pos, interp->get_detail_level_lock(), &local_eye);
// don't render weapon models beyond the ship's tabled Weapon Model Draw Distance (which defaults to 200)
if (depth > interp->get_attached_model_draw_distance()) {
return;
}
} else {
depth = model_render_determine_depth(objnum, model_num, orient, pos, interp->get_detail_level_lock());
}
scene->push_transform(pos, orient);
int detail_level = model_render_determine_detail(depth, model_num, interp->get_detail_level_lock());
// Send the detail level to the lab for displaying
if (gameseq_get_state() == GS_STATE_LAB) {
Lab_object_detail_level = detail_level;
}
// #ifndef NDEBUG
// if ( Interp_detail_level == 0 ) {
// MONITOR_INC( NumHiModelsRend, 1 );
// } else if ( Interp_detail_level == pm->n_detail_levels-1 ) {
// MONITOR_INC( NumLowModelsRend, 1 );
// } else {
// MONITOR_INC( NumMedModelsRend, 1 );
// }
// #endif
vec3d auto_back = ZERO_VECTOR;
bool set_autocen = model_render_determine_autocenter(&auto_back, pm, detail_level, model_flags);
if ( set_autocen ) {
scene->push_transform(&auto_back, NULL);
}
// if we're in nebula mode, fog everything except for the warp holes and other non-fogged models
if ( (The_mission.flags[Mission::Mission_Flags::Fullneb]) && (Neb2_render_mode != NEB2_RENDER_NONE) && !(model_flags & MR_NO_FOGGING) ) {
unsigned char r, g, b;
neb2_get_fog_color(&r, &g, &b);
rendering_material.set_fog(r, g, b);
} else {
rendering_material.set_fog();
}
if (interp->is_alpha_mult_set()) {
rendering_material.set_alpha_mult(interp->get_alpha_mult());
}
if ( is_outlines_only_htl ) {
rendering_material.set_fill_mode(GR_FILL_MODE_WIRE);
tmap_flags &= ~TMAP_FLAG_RGB;
} else {
rendering_material.set_fill_mode(GR_FILL_MODE_SOLID);
}
color clr = interp->get_color();
rendering_material.set_color(clr);
if ( model_flags & MR_EDGE_ALPHA ) {
rendering_material.set_center_alpha(-1);
} else if ( model_flags & MR_CENTER_ALPHA ) {
rendering_material.set_center_alpha(1);
} else {
rendering_material.set_center_alpha(0);
}
if ( interp->uses_thick_outlines() ) {
rendering_material.set_outline_thickness(interp->get_outline_thickness());
}
if ( ( model_flags & MR_NO_CULL ) || ( model_flags & MR_ALL_XPARENT ) || ( interp->get_warp_bitmap() >= 0 ) ) {
rendering_material.set_cull_mode(false);
} else {
rendering_material.set_cull_mode(true);
}
if ( !(model_flags & MR_NO_LIGHTING) ) {
rendering_material.set_lighting(true);
} else {
rendering_material.set_lighting(false);
}
Assertion(!(model_flags & MR_STENCIL_READ && model_flags & MR_STENCIL_WRITE),
"Enabling stencil read and write at the same time is not supported!");
if (model_flags & MR_STENCIL_READ) {
rendering_material.set_stencil_test(true);
rendering_material.set_stencil_func(ComparisionFunction::NotEqual, 1, 0xFFFF);
rendering_material.set_stencil_op(StencilOperation::Keep, StencilOperation::Keep, StencilOperation::Keep);
} else if (model_flags & MR_STENCIL_WRITE) {
rendering_material.set_stencil_test(true);
rendering_material.set_stencil_func(ComparisionFunction::Always, 1, 0xFFFF);
rendering_material.set_stencil_op(StencilOperation::Keep, StencilOperation::Keep, StencilOperation::Replace);
} else {
rendering_material.set_stencil_test(false);
rendering_material.set_stencil_func(ComparisionFunction::Never, 1, 0xFFFF);
rendering_material.set_stencil_op(StencilOperation::Keep, StencilOperation::Keep, StencilOperation::Keep);
}
if (model_flags & MR_NO_COLOR_WRITES) {
rendering_material.set_color_mask(false, false, false, false);
} else {
rendering_material.set_color_mask(true, true, true, true);
}
rendering_material.set_depth_bias(0);
if ( !(model_flags & MR_NO_BATCH) && pm->flags & PM_FLAG_BATCHED
&& !(is_outlines_only || is_outlines_only_htl) ) {
// always set batched rendering on if supported
tmap_flags |= TMAP_FLAG_BATCH_TRANSFORMS;
}
if ( (tmap_flags & TMAP_FLAG_BATCH_TRANSFORMS) ) {
scene->start_model_batch(pm->n_models);
model_render_buffers(scene, &rendering_material, interp, &pm->detail_buffers[detail_level], pm, -1, detail_level, tmap_flags);
}
// Draw the subobjects
bool draw_thrusters = false;
bool trans_buffer = false;
i = pm->submodel[pm->detail[detail_level]].first_child;
while( i >= 0 ) {
if ( !pm->submodel[i].flags[Model::Submodel_flags::Is_thruster] ) {
model_render_children_buffers( scene, &rendering_material, interp, pm, pmi, i, detail_level, tmap_flags, trans_buffer );
} else {
draw_thrusters = true;
}
i = pm->submodel[i].next_sibling;
}
model_radius = pm->submodel[pm->detail[detail_level]].rad;
//*************************** draw the hull of the ship *********************************************
vec3d view_pos = scene->get_view_position();
if ( model_render_check_detail_box(&view_pos, pm, pm->detail[detail_level], model_flags) ) {
int detail_model_num = pm->detail[detail_level];
if ( (is_outlines_only || is_outlines_only_htl) && pm->submodel[detail_model_num].outline_buffer != NULL ) {
color outline_color = interp->get_color();
scene->add_outline(pm->submodel[detail_model_num].outline_buffer.get(), pm->submodel[detail_model_num].n_verts_outline, &outline_color);
} else {
model_render_buffers(scene, &rendering_material, interp, &pm->submodel[detail_model_num].buffer, pm, detail_model_num, detail_level, tmap_flags);
if ( pmi != nullptr && !pmi->submodel[detail_model_num].electrical_arcs.empty() ) {
model_render_add_lightning( scene, interp, pm, &pmi->submodel[detail_model_num] );
}
}
}
// make sure batch rendering is unconditionally off.
tmap_flags &= ~TMAP_FLAG_BATCH_TRANSFORMS;
if ( pm->flags & PM_FLAG_TRANS_BUFFER && !(is_outlines_only || is_outlines_only_htl) ) {
trans_buffer = true;
i = pm->submodel[pm->detail[detail_level]].first_child;
while( i >= 0 ) {
if ( !pm->submodel[i].flags[Model::Submodel_flags::Is_thruster] ) {
model_render_children_buffers( scene, &rendering_material, interp, pm, pmi, i, detail_level, tmap_flags, trans_buffer );
}
i = pm->submodel[i].next_sibling;
}
view_pos = scene->get_view_position();
if ( model_render_check_detail_box(&view_pos, pm, pm->detail[detail_level], model_flags) ) {
int detail_model_num = pm->detail[detail_level];
model_render_buffers(scene, &rendering_material, interp, &pm->submodel[detail_model_num].trans_buffer, pm, detail_model_num, detail_level, tmap_flags);
}
}
// Draw the thruster subobjects
if ( draw_thrusters && !(is_outlines_only || is_outlines_only_htl) ) {
i = pm->submodel[pm->detail[detail_level]].first_child;
trans_buffer = false;
while( i >= 0 ) {
if (pm->submodel[i].flags[Model::Submodel_flags::Is_thruster]) {
model_render_children_buffers( scene, &rendering_material, interp, pm, pmi, i, detail_level, tmap_flags, trans_buffer );
}
i = pm->submodel[i].next_sibling;
}
}
// MARKED!
if ( !( model_flags & MR_NO_TEXTURING ) && !( model_flags & MR_NO_INSIGNIA) ) {
int bitmap_num = interp->get_insignia_bitmap();
if ( Render_insignias_as_decals && objnum >= 0 && (pm->num_ins > 0) && (bitmap_num >= 0) ) {
for (int ins_idx = 0; ins_idx < pm->num_ins; ins_idx++) {
const insignia& ins = pm->ins[ins_idx];
// skip insignias not on our detail level
if (ins.detail_level != detail_level) {
continue;
}
decals::Decal decal;
decal.object = &Objects[objnum];
decal.position = ins.position;
decal.submodel = -1;
decal.scale = vec3d{{{ins.diameter, ins.diameter, ins.diameter}}};
decal.orig_obj_type = OBJ_SHIP;
decal.creation_time = f2fl(Missiontime);
decal.lifetime = 1.0f;
decal.orientation = ins.orientation;
decal.definition_handle = std::make_tuple(bitmap_num, -1, -1);
decals::addSingleFrameDecal(std::move(decal));
}
} else {
scene->add_insignia(interp, pm, detail_level, bitmap_num);
}
}
if ( (model_flags & MR_AUTOCENTER) && (set_autocen) ) {
scene->pop_transform();
}
scene->pop_transform();
// start rendering glow points -Bobboau
if ( (pm->n_glow_point_banks) && !is_outlines_only && !is_outlines_only_htl && !Glowpoint_override ) {
model_render_glow_points(pm, pmi, shipp, orient, pos, Glowpoint_use_depth_buffer);
}
// Draw the thruster glow
if ( !is_outlines_only && !is_outlines_only_htl ) {
if ( ( model_flags & MR_AUTOCENTER ) && set_autocen ) {
vec3d autoback_rotated;
vm_vec_unrotate(&autoback_rotated, &auto_back, orient);
vm_vec_add2(&autoback_rotated, pos);
model_queue_render_thrusters( interp, pm, objnum, shipp, orient, &autoback_rotated );
} else {
model_queue_render_thrusters( interp, pm, objnum, shipp, orient, pos );
}
}
}
//Renders the glowpoint light sources of a model without rendering anything else of the model.
void model_render_only_glowpoint_lights(const model_render_params* interp, int model_num, int model_instance_num, const matrix* orient, const vec3d* pos)
{
const int objnum = interp->get_object_number();
const uint64_t model_flags = interp->get_model_flags();
polymodel *pm = model_get(model_num);
polymodel_instance *pmi = nullptr;
model_render_set_glow_points(pm, objnum);
ship *shipp = nullptr;
object *objp = nullptr;
if (objnum >= 0) {
objp = &Objects[objnum];
int tentative_num = -1;
// TODO: Add Prop support here
if (objp->type == OBJ_SHIP) {
shipp = &Ships[objp->instance];
tentative_num = shipp->model_instance_num;
}
else {
tentative_num = object_get_model_instance_num(objp);
}
if (tentative_num >= 0) {
model_instance_num = tentative_num;
}
}
// is this a skybox with a rotating submodel?
if (model_num == Nmodel_num && Nmodel_instance_num >= 0) {
model_instance_num = Nmodel_instance_num;
}
if (model_instance_num >= 0) {
pmi = model_get_instance(model_instance_num);
// This can happen if we supply a HUD target model for a real ship.
// The passed parameter was -1 but it was assigned an instance from
// the actual object. Set it back to -1 if there is a mismatch.
if (pmi->model_num != pm->id) {
model_instance_num = -1;
pmi = nullptr;
}
}
bool is_outlines_only = (model_flags & MR_NO_POLYS) && ((model_flags & MR_SHOW_OUTLINE_PRESET) || (model_flags & MR_SHOW_OUTLINE));
bool is_outlines_only_htl = (model_flags & MR_NO_POLYS) && (model_flags & MR_SHOW_OUTLINE_HTL);
// start rendering glow points -Bobboau
if ( (pm->n_glow_point_banks) && !is_outlines_only && !is_outlines_only_htl && !Glowpoint_override ) {
model_render_glow_points(pm, pmi, shipp, orient, pos, Glowpoint_use_depth_buffer,false,true);
}
}
void model_render_set_wireframe_color(const color* clr)
{
Wireframe_color = *clr;
}
void modelinstance_replace_active_texture(polymodel_instance* pmi, const char* old_name, const char* new_name)
{
Assert(pmi != nullptr);
if (pmi == nullptr)
return;
polymodel* pm = model_get(pmi->model_num);
int final_index = -1;
for (int i = 0; i < pm->n_textures; i++)
{
int tm_num = pm->maps[i].FindTexture(old_name);
if (tm_num > -1)
{
final_index = i * TM_NUM_TYPES + tm_num;
break;
}
}
if (final_index >= 0)
{
int texture;
if (!stricmp(new_name, "invisible"))
texture = REPLACE_WITH_INVISIBLE;
else
texture = bm_load_either(new_name);
if (pmi->texture_replace == nullptr) {
pmi->texture_replace = std::make_shared<model_texture_replace>();
}
(*pmi->texture_replace)[final_index] = texture;
} else
Warning(LOCATION, "Invalid texture '%s' used for replacement texture", old_name);
}
// renders a model as if in the tech room or briefing UI
// model_type 1 for ship class, 2 for weapon class, 3 for pof
bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string &pof_filename, float close_zoom, const vec3d *close_pos, const SCP_string& tcolor, const SCP_vector<SCP_string>& destroyed_subsystems)
{
lighting_profiles::set_non_mission_profile non_mission_lighting_profile;
model_render_params render_info;
const vec3d *closeup_pos;
float closeup_zoom;
int model_num;
bool model_lighting = true;
uint64_t render_flags = MR_AUTOCENTER | MR_NO_FOGGING;
switch (model_type) {
case TECH_SHIP:
ship_info* sip;
sip = &Ship_info[class_idx];
closeup_pos = &sip->closeup_pos;
closeup_zoom = sip->closeup_zoom;
if (sip->uses_team_colors) {
render_info.set_team_color(!tcolor.empty() ? tcolor : sip->default_team_name, "none", 0, 0);
}
if (sip->flags[Ship::Info_Flags::No_lighting]) {
model_lighting = false;
}
// Make sure model is loaded
model_num = model_load(sip, true);
render_info.set_replacement_textures(model_num, sip->replacement_textures);
break;
case TECH_PROP:
prop_info* pip;
pip = &Prop_info[class_idx];
closeup_pos = &pip->closeup_pos;
closeup_zoom = pip->closeup_zoom;
if (pip->flags[Prop::Info_Flags::No_lighting]) {
model_lighting = false;
}
// Make sure model is loaded
model_num = model_load(pip->pof_file.c_str());
break;
case TECH_WEAPON:
weapon_info* wip;
wip = &Weapon_info[class_idx];
closeup_pos = &wip->closeup_pos;
closeup_zoom = wip->closeup_zoom;
if (wip->wi_flags[Weapon::Info_Flags::Mr_no_lighting]) {
model_lighting = false;
}
// Make sure model is loaded
if (VALID_FNAME(wip->tech_model)) {
model_num = model_load(wip->tech_model, nullptr, ErrorType::WARNING);
} else {
// no tech model!!
return false;
}
break;
case TECH_POF:
closeup_pos = close_pos;
closeup_zoom = close_zoom;
model_num = model_load(pof_filename.c_str());
break;
case TECH_JUMP_NODE:
closeup_pos = close_pos;
closeup_zoom = close_zoom;
model_num = model_load(pof_filename.c_str());
render_info.set_color(HUD_color_red, HUD_color_green, HUD_color_blue);
render_flags |= MR_NO_POLYS | MR_SHOW_OUTLINE_HTL | MR_NO_TEXTURING;
model_lighting = false;
break;
default:
return false;
}
//check if the model was loaded
if (model_num < 0)
return false;
// Clip
gr_set_clip(x1, y1, x2 - x1, y2 - y1, GR_RESIZE_NONE);
// Handle 3D init stuff
g3_start_frame(1);
g3_set_view_matrix(closeup_pos, &vmd_identity_matrix, closeup_zoom * zoom);
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
// setup lights
common_setup_room_lights();
// Draw the model!!
model_clear_instance(model_num);
int model_instance = -1;
// Get a cached UI render instance for ships so repeated UI/Lua renders avoid per-call allocation churn
if (model_type == TECH_SHIP) {
auto sip = &Ship_info[class_idx];
const auto subsystem_hash = model_hash_subsystem_name_list_for_cache(destroyed_subsystems);
const auto cache_result = model_get_cached_ui_render_instance(model_num, &model_instance, subsystem_hash);
if (cache_result == TriStateBool::TRUE_) {
model_set_up_techroom_instance(sip, model_instance);
if (!destroyed_subsystems.empty()) {
auto pm = model_get(model_num);
auto pmi = model_get_instance(model_instance);
flagset<Ship::Subsystem_Flags> empty;
for (int idx = 0; idx < sip->n_subsystems; ++idx) {
auto& subsystem = sip->subsystems[idx];
if (subsystem.subobj_num < 0 || subsystem.subobj_num >= pm->n_models ||
subsystem.model_num != model_num) {
continue;
}
for (auto& destroyed_name : destroyed_subsystems) {
if (!stricmp(subsystem.subobj_name, destroyed_name.c_str()) ||
!stricmp(subsystem.name, destroyed_name.c_str())) {
pmi->submodel[subsystem.subobj_num].blown_off = true;
model_replicate_submodel_instance(pm, pmi, subsystem.subobj_num, empty);
break;
}
}
}
}
}
}
render_info.set_detail_level_lock(0);
if (!lighting || !model_lighting)
render_flags |= MR_NO_LIGHTING;
render_info.set_flags(render_flags);
bool s_save = Shadow_override;
Shadow_override = true;
if (model_type == TECH_SHIP) {
model_render_immediate(&render_info, model_num, model_instance, orient, &vmd_zero_vector);
} else {
model_render_immediate(&render_info, model_num, orient, &vmd_zero_vector);
}
Shadow_override = s_save;
// OK we're done
gr_end_view_matrix();
gr_end_proj_matrix();
// Bye!!
g3_end_frame();
gr_reset_clip();
return true;
}