Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,860 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstring>
#include <string>
#include "BLI_listbase.h"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "BLI_threads.h"
#include "BLI_vector.hh"
#include "MEM_guardedalloc.h"
#include "DNA_node_types.h"
#include "BKE_cryptomatte.hh"
#include "BKE_global.hh"
#include "BKE_image.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_scene.hh"
#include "BKE_scene_runtime.hh"
#include "DRW_engine.hh"
#include "DRW_render.hh"
#include "IMB_imbuf.hh"
#include "COM_context.hh"
#include "COM_conversion_operation.hh"
#include "COM_domain.hh"
#include "COM_node_group_operation.hh"
#include "COM_realize_on_domain_operation.hh"
#include "COM_render_context.hh"
#include "COM_result.hh"
#include "NOD_eval_log.hh"
#include "RE_compositor.hh"
#include "RE_pipeline.h"
#include "WM_api.hh"
#include "GPU_context.hh"
#include "GPU_state.hh"
#include "GPU_texture_pool.hh"
#include "render_types.h"
namespace blender {
namespace render {
/**
* Render Context Data
*
* Stored separately from the context so we can update it without losing any cached
* data from the context.
*/
class ContextInputData {
public:
const Render *render;
const Main *main;
const Scene *scene;
const RenderData *render_data;
const bNodeTree *node_tree;
std::string view_name;
compositor::RenderContext *render_context;
compositor::NodeGroupOutputTypes needed_outputs;
ContextInputData(const Render *render,
const Main &main,
const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const char *view_name,
compositor::RenderContext *render_context,
compositor::NodeGroupOutputTypes needed_outputs)
: render(render),
main(&main),
scene(&scene),
render_data(&render_data),
node_tree(&node_tree),
view_name(view_name),
render_context(render_context),
needed_outputs(needed_outputs)
{
}
};
/* Render Context Data */
class Context : public compositor::Context {
private:
/* Input data. */
ContextInputData input_data_;
/* Cached GPU and CPU passes that the compositor took ownership of. Those had their reference
* count incremented when accessed and need to be freed/have their reference count decremented
* when destroying the context. */
Vector<gpu::Texture *> cached_gpu_passes_;
Vector<ImBuf *> cached_cpu_passes_;
/* True if GPU compute is supported and can be used, if false, we fallback to CPU. */
bool gpu_supported_ = true;
public:
Context(compositor::StaticCacheManager &cache_manager, const ContextInputData &input_data)
: compositor::Context(cache_manager), input_data_(input_data)
{
}
virtual ~Context()
{
for (gpu::Texture *pass : cached_gpu_passes_) {
GPU_texture_free(pass);
}
for (ImBuf *pass : cached_cpu_passes_) {
IMB_freeImBuf(pass);
}
}
const Main &get_main() const override
{
return *input_data_.main;
}
const Scene &get_scene() const override
{
return *input_data_.scene;
}
void set_gpu_supported(const bool supported)
{
gpu_supported_ = supported;
}
bool use_gpu() const override
{
return gpu_supported_ &&
this->get_render_data().compositor_device == SCE_COMPOSITOR_DEVICE_GPU;
}
compositor::NodeGroupOutputTypes needed_outputs() const
{
return input_data_.needed_outputs;
}
const RenderData &get_render_data() const override
{
return *(input_data_.render_data);
}
int2 get_render_size() const
{
Render *render = RE_GetSceneRender(input_data_.scene);
RenderResult *render_result = RE_AcquireResultRead(render);
/* If a render result already exist, use its size, since the compositor operates on the render
* settings at which the render happened. Otherwise, use the size from the render data. */
int2 size;
if (render_result) {
size = int2(render_result->rectx, render_result->recty);
}
else {
BKE_render_resolution(input_data_.render_data, true, &size.x, &size.y);
}
RE_ReleaseResult(render);
return size;
}
compositor::Domain get_compositing_domain() const override
{
return compositor::Domain(this->get_render_size());
}
void write_output(const compositor::Result &result)
{
Render *render = RE_GetSceneRender(input_data_.scene);
RenderResult *render_result = RE_AcquireResultWrite(render);
if (render_result) {
RenderView *render_view = RE_RenderViewGetByName(render_result,
input_data_.view_name.c_str());
ImBuf *image_buffer = RE_RenderViewEnsureImBuf(render_result, render_view);
render_result->have_combined = true;
if (result.is_single_value()) {
float *data = MEM_new_array_uninitialized<float>(
4 * size_t(render_result->rectx) * size_t(render_result->recty), __func__);
image_buffer->assign_float_data(data);
IMB_rectfill(image_buffer, result.get_single_value<compositor::Color>());
}
else if (this->use_gpu()) {
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
float *output_buffer = static_cast<float *>(GPU_texture_read(result, GPU_DATA_FLOAT, 0));
image_buffer->assign_float_data(output_buffer);
}
else {
if (result.sharing_info()) {
image_buffer->float_buffer = ImBufFloatBuffer{
.data = static_cast<const float *>(result.cpu_data().data()),
.sharing_info = result.sharing_info(),
.colorspace = nullptr};
}
else {
float *data = MEM_new_array_uninitialized<float>(
4 * size_t(render_result->rectx) * size_t(render_result->recty), __func__);
image_buffer->assign_float_data(data);
std::memcpy(image_buffer->float_data_for_write(),
result.cpu_data().data(),
render_result->rectx * render_result->recty * 4 * sizeof(float));
}
}
}
RE_ReleaseResult(render);
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_R_RESULT, "Render Result");
BKE_image_partial_update_mark_full_update(image);
BLI_thread_lock(LOCK_DRAW_IMAGE);
BKE_image_signal(G.main, image, nullptr, IMA_SIGNAL_FREE);
BLI_thread_unlock(LOCK_DRAW_IMAGE);
}
void write_viewer_image(const compositor::Result &viewer_result)
{
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_COMPOSITE, "Viewer Node");
if (viewer_result.meta_data.is_non_color_data) {
image->flag &= ~IMA_VIEW_AS_RENDER;
}
else {
image->flag |= IMA_VIEW_AS_RENDER;
}
ImageUser image_user = {nullptr};
image_user.multi_index = BKE_scene_multiview_view_id_get(input_data_.render_data,
input_data_.view_name.c_str());
if (BKE_scene_multiview_is_render_view_first(input_data_.render_data,
input_data_.view_name.c_str()))
{
BKE_image_ensure_viewer_views(input_data_.render_data, image, &image_user);
}
BLI_thread_lock(LOCK_DRAW_IMAGE);
void *lock;
ImBuf *image_buffer = BKE_image_acquire_ibuf_gpu(image, &image_user, &lock);
const int2 size = viewer_result.is_single_value() ? this->get_render_size() :
viewer_result.domain().data_size;
/* The image buffer has a different size than the viewer result, set the new size and free all
* data to be reallocated later. */
if (int2(image_buffer->x, image_buffer->y) != size) {
IMB_free_byte_pixels(image_buffer);
IMB_free_float_pixels(image_buffer);
IMB_free_gpu_textures(image_buffer);
image_buffer->x = size.x;
image_buffer->y = size.y;
}
if (this->use_gpu()) {
/* If using GPU, free any potential previous CPU data. */
IMB_free_float_pixels(image_buffer);
/* Allocate a GPU texture if using GPU and no texture exists or one exists but with a
* different format. */
if (!image_buffer->gpu.texture ||
GPU_texture_format(image_buffer->gpu.texture) != viewer_result.get_gpu_texture_format())
{
gpu::TextureFormat format = viewer_result.get_gpu_texture_format();
gpu::Texture *texture = GPU_texture_create_2d(
__func__, size.x, size.y, 1, format, GPU_TEXTURE_USAGE_GENERAL, nullptr);
IMB_assign_gpu_texture(image_buffer, texture);
}
}
else {
/* If not using GPU, free any potential previous GPU data. */
IMB_free_gpu_textures(image_buffer);
}
if (this->use_gpu()) {
if (viewer_result.is_single_value()) {
GPU_texture_clear(image_buffer->gpu.texture,
GPU_DATA_FLOAT,
viewer_result.get_single_value<compositor::Color>());
}
else {
GPU_texture_copy(image_buffer->gpu.texture, viewer_result);
}
image_buffer->userflags |= IB_HOST_BUFFER_INVALID;
}
else {
if (viewer_result.is_single_value()) {
IMB_alloc_float_pixels(image_buffer, 4, false);
IMB_rectfill(image_buffer, viewer_result.get_single_value<compositor::Color>());
}
else if (viewer_result.sharing_info()) {
image_buffer->channels = 4;
image_buffer->float_buffer = ImBufFloatBuffer{
.data = static_cast<const float *>(viewer_result.cpu_data().data()),
.sharing_info = viewer_result.sharing_info(),
.colorspace = nullptr};
}
else if (viewer_result.cpu_data().data() != image_buffer->float_data()) {
IMB_alloc_float_pixels(image_buffer, 4, false);
std::memcpy(image_buffer->float_data_for_write(),
viewer_result.cpu_data().data(),
size.x * size.y * 4 * sizeof(float));
}
image_buffer->userflags |= IB_DISPLAY_BUFFER_INVALID;
}
if (!viewer_result.is_single_value()) {
image_buffer->flags |= ImBufFlags::HasDisplayWindow;
const int2 display_offset = int2(viewer_result.domain().transformation.location());
copy_v2_v2_int(image_buffer->display_size, viewer_result.domain().display_size);
copy_v2_v2_int(image_buffer->display_offset, display_offset);
copy_v2_v2_int(image_buffer->data_offset, viewer_result.domain().data_offset);
}
else {
image_buffer->flags &= ~ImBufFlags::HasDisplayWindow;
}
BKE_image_partial_update_mark_full_update(image);
BKE_image_release_ibuf(image, image_buffer, lock);
BLI_thread_unlock(LOCK_DRAW_IMAGE);
}
void write_viewer(compositor::Result &viewer_result) override
{
using namespace compositor;
/* Realize the transforms if needed. */
const InputDescriptor input_descriptor = {ResultType::Color,
InputRealizationMode::OperationDomain};
SimpleOperation *realization_operation = RealizeOnDomainOperation::construct_if_needed(
*this, viewer_result, input_descriptor, viewer_result.domain());
if (realization_operation) {
Result realize_input = this->create_result(ResultType::Color, viewer_result.precision());
realize_input.share_data(viewer_result);
realization_operation->map_input_to_result(&realize_input);
realization_operation->evaluate();
Result &realized_viewer_result = realization_operation->get_result();
this->write_viewer_image(realized_viewer_result);
realized_viewer_result.release();
delete realization_operation;
return;
}
this->write_viewer_image(viewer_result);
}
compositor::ResultType get_pass_data_type(const RenderPass *pass)
{
switch (pass->channels) {
case 1:
return compositor::ResultType::Float;
case 2:
return compositor::ResultType::Float2;
case 3:
return compositor::ResultType::Float3;
case 4:
if (StringRef(pass->chan_id) == "XYZW") {
return compositor::ResultType::Float4;
}
else {
return compositor::ResultType::Color;
}
default:
break;
}
BLI_assert_unreachable();
return compositor::ResultType::Float;
}
compositor::ResultType get_pass_type(const RenderPass *pass)
{
switch (pass->channels) {
case 1:
return compositor::ResultType::Float;
case 2:
return compositor::ResultType::Float2;
case 3:
if (StringRef(pass->chan_id) == "RGB") {
return compositor::ResultType::Color;
}
else {
return compositor::ResultType::Float3;
}
case 4:
if (StringRef(pass->chan_id) == "XYZW") {
return compositor::ResultType::Float4;
}
else {
return compositor::ResultType::Color;
}
default:
break;
}
BLI_assert_unreachable();
return compositor::ResultType::Float;
}
compositor::Result get_invalid_pass()
{
compositor::Result invalid_pass = this->create_result(compositor::ResultType::Color);
invalid_pass.allocate_invalid();
return invalid_pass;
}
compositor::Result get_pass(const Scene *scene, int view_layer_id, const char *name) override
{
/* Blender aliases the Image pass name to be the Combined pass, so we return the combined pass
* in that case. */
const char *pass_name = StringRef(name) == "Image" ? "Combined" : name;
if (!scene) {
return this->get_invalid_pass();
}
ViewLayer *view_layer = static_cast<ViewLayer *>(
BLI_findlink(&scene->view_layers, view_layer_id));
if (!view_layer) {
return this->get_invalid_pass();
}
Render *render = RE_GetSceneRender(scene);
if (!render) {
return this->get_invalid_pass();
}
BLI_SCOPED_DEFER([&]() { RE_ReleaseResult(render); });
RenderResult *render_result = RE_AcquireResultRead(render);
if (!render_result) {
return this->get_invalid_pass();
}
RenderLayer *render_layer = RE_GetRenderLayer(render_result, view_layer->name);
if (!render_layer) {
return this->get_invalid_pass();
}
RenderPass *render_pass = RE_pass_find_by_name(
render_layer, pass_name, this->get_view_name().data());
if (!render_pass) {
return this->get_invalid_pass();
}
if (!render_pass || !render_pass->ibuf || !render_pass->ibuf->float_data()) {
return this->get_invalid_pass();
}
compositor::Result pass_data = compositor::Result(
*this, this->get_pass_data_type(render_pass), compositor::ResultPrecision::Full);
if (this->use_gpu()) {
gpu::Texture *pass_texture = RE_pass_ensure_gpu_texture_cache(render, render_pass);
/* Don't assume render will keep pass data stored, add our own reference. */
GPU_texture_ref(pass_texture);
pass_data.share_data(pass_texture);
cached_gpu_passes_.append(pass_texture);
}
else {
/* Don't assume render will keep pass data stored, add our own reference. */
IMB_refImBuf(render_pass->ibuf);
pass_data.share_data(render_pass->ibuf->float_buffer.data,
int2(render_pass->ibuf->x, render_pass->ibuf->y),
render_pass->ibuf->float_buffer.sharing_info);
cached_cpu_passes_.append(render_pass->ibuf);
}
compositor::Result pass = compositor::Result(
*this, this->get_pass_type(render_pass), compositor::ResultPrecision::Full);
if (pass.type() != pass_data.type()) {
compositor::ConversionOperation conversion_operation(*this, pass_data.type(), pass.type());
conversion_operation.map_input_to_result(&pass_data);
conversion_operation.evaluate();
pass.share_data(conversion_operation.get_result());
conversion_operation.get_result().release();
}
else {
pass.share_data(pass_data);
pass_data.release();
}
/* We assume the given pass is a Cryptomatte pass and retrieve its layer name. If it wasn't a
* Cryptomatte pass, the checks below will fail anyway. */
const std::string combined_pass_name = std::string(view_layer->name) + "." + pass_name;
StringRef cryptomatte_layer_name = bke::cryptomatte::BKE_cryptomatte_extract_layer_name(
combined_pass_name);
struct StampCallbackData {
std::string cryptomatte_layer_name;
compositor::MetaData *meta_data;
};
/* Go over the stamp data and add any Cryptomatte related meta data. */
StampCallbackData callback_data = {cryptomatte_layer_name, &pass.meta_data};
BKE_stamp_info_callback(
&callback_data,
render_result->stamp_data,
[](void *user_data, const char *key, char *value, int /*value_maxncpy*/) {
StampCallbackData *data = static_cast<StampCallbackData *>(user_data);
const std::string manifest_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "manifest");
if (key == manifest_key) {
data->meta_data->cryptomatte.manifest = value;
}
const std::string hash_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "hash");
if (key == hash_key) {
data->meta_data->cryptomatte.hash = value;
}
const std::string conversion_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "conversion");
if (key == conversion_key) {
data->meta_data->cryptomatte.conversion = value;
}
},
false);
return pass;
}
StringRef get_view_name() const override
{
return input_data_.view_name;
}
compositor::ResultPrecision get_precision() const override
{
switch (input_data_.scene->r.compositor_precision) {
case SCE_COMPOSITOR_PRECISION_AUTO:
/* Auto uses full precision for final renders and half precision otherwise. */
if (this->render_context()) {
return compositor::ResultPrecision::Full;
}
else {
return compositor::ResultPrecision::Half;
}
case SCE_COMPOSITOR_PRECISION_FULL:
return compositor::ResultPrecision::Full;
}
BLI_assert_unreachable();
return compositor::ResultPrecision::Full;
}
compositor::RenderContext *render_context() const override
{
return input_data_.render_context;
}
nodes::eval_log::NodesEvalLog *nodes_evaluation_log() const override
{
return this->get_scene().runtime->compositor.nodes_evaluation_log.get();
}
void evaluate_operation_post() const override
{
/* If no render context exist, that means this is an interactive compositor evaluation due to
* the user editing the node tree. In that case, we wait until the operation finishes executing
* on the GPU before we continue to improve interactivity. The improvement comes from the fact
* that the user might be rapidly changing values, so we need to cancel previous evaluations to
* make editing faster, but we can't do that if all operations are submitted to the GPU all at
* once, and we can't cancel work that was already submitted to the GPU. This does have a
* performance penalty, but in practice, the improved interactivity is worth it according to
* user feedback. */
if (this->use_gpu() && !this->render_context()) {
GPU_finish();
}
}
bool is_canceled() const override
{
return input_data_.render->display->test_break();
}
void evaluate()
{
/* Reset log before evaluation. */
this->get_scene().runtime->compositor.nodes_evaluation_log =
std::make_unique<nodes::eval_log::NodesEvalLog>();
using namespace compositor;
const NodeGroupOutputTypes needed_outputs = this->needed_outputs();
const bNodeTree &node_group = *input_data_.node_tree;
const bke::DataBlockComputeContext compute_context(nullptr, this->get_scene().id);
NodeGroupOperation node_group_operation(*this,
node_group,
needed_outputs,
node_group.active_viewer_key,
bke::NODE_INSTANCE_KEY_BASE,
compute_context);
/* Set the reference count for the outputs, only the first color output is actually needed,
* while the rest are ignored. */
const bool is_group_output_needed = flag_is_set(needed_outputs,
NodeGroupOutputTypes::GroupOutputNode);
node_group.ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *output_socket : node_group.interface_outputs()) {
const bool is_first_output = output_socket == node_group.interface_outputs().first();
Result &output_result = node_group_operation.get_result(output_socket->identifier);
const bool is_color = output_result.type() == ResultType::Color;
const bool is_needed = is_group_output_needed && is_first_output && is_color;
output_result.set_reference_count(is_needed ? 1 : 0);
}
/* Map the inputs to the operation. */
Vector<std::unique_ptr<Result>> inputs;
for (const bNodeTreeInterfaceSocket *input_socket : node_group.interface_inputs()) {
Result *input_result = new Result(
this->create_result(ResultType::Color, ResultPrecision::Full));
if (input_socket == node_group.interface_inputs()[0]) {
/* First socket is the combined pass. */
Result combined_pass = this->get_pass(&this->get_scene(), 0, "Image");
if (combined_pass.is_allocated()) {
input_result->share_data(combined_pass);
}
else {
input_result->allocate_invalid();
}
combined_pass.release();
}
else {
/* The rest of the sockets are not supported. */
input_result->allocate_invalid();
}
node_group_operation.map_input_to_result(input_socket->identifier, input_result);
inputs.append(std::unique_ptr<Result>(input_result));
}
node_group_operation.evaluate();
/* Write the outputs of the operation. */
for (const bNodeTreeInterfaceSocket *output_socket : node_group.interface_outputs()) {
Result &output_result = node_group_operation.get_result(output_socket->identifier);
if (!output_result.should_compute()) {
continue;
}
if (this->is_canceled()) {
output_result.release();
continue;
}
/* Realize the output on the compositing domain if needed. */
const Domain compositing_domain = this->get_compositing_domain();
const InputDescriptor input_descriptor = {ResultType::Color,
InputRealizationMode::OperationDomain};
SimpleOperation *realization_operation = RealizeOnDomainOperation::construct_if_needed(
*this, output_result, input_descriptor, compositing_domain);
if (realization_operation) {
realization_operation->map_input_to_result(&output_result);
realization_operation->evaluate();
Result &realized_output_result = realization_operation->get_result();
this->write_output(realized_output_result);
realized_output_result.release();
delete realization_operation;
continue;
}
this->write_output(output_result);
output_result.release();
}
}
};
/* Render Compositor */
class Compositor {
private:
/* Render instance for GPU context to run compositor in. */
Render &render_;
compositor::StaticCacheManager cache_manager_;
/* Stores the execution device and precision used in the last evaluation of the compositor. Those
* might be different from the current values returned by the context, since the user might have
* changed them since the last evaluation. See the needs_to_be_recreated method for more info on
* why those are needed. */
bool last_evaluation_used_gpu_ = false;
compositor::ResultPrecision last_evaluation_precision_ = compositor::ResultPrecision::Half;
public:
Compositor(Render &render) : render_(render) {}
~Compositor()
{
/* Use last_evaluation_used_gpu_ instead of the currently used device because we are freeing
* resources from the last evaluation. See last_evaluation_used_gpu_ for more information. */
if (last_evaluation_used_gpu_) {
/* Free resources with GPU context enabled. Cleanup may happen from the main thread, and we
* must use the main context there. */
if (BLI_thread_is_main()) {
DRW_gpu_context_enable();
}
else {
DRW_render_context_enable(&render_);
}
}
cache_manager_.free();
/* See comment above on context enabling. */
if (last_evaluation_used_gpu_) {
if (BLI_thread_is_main()) {
DRW_gpu_context_disable();
}
else {
DRW_render_context_disable(&render_);
}
}
}
void execute(const ContextInputData &input_data)
{
Context context(cache_manager_, input_data);
if (context.use_gpu()) {
/* For main thread rendering in background mode, blocking rendering, or when we do not have a
* render system GPU context, use the DRW context directly, while for threaded rendering when
* we have a render system GPU context, use the render's system GPU context to avoid blocking
* with the global DST. */
GHOST_IContext *re_system_gpu_context = RE_system_gpu_context_get(&render_);
if (BLI_thread_is_main() || re_system_gpu_context == nullptr) {
DRW_gpu_context_enable();
context.set_gpu_supported(DRW_gpu_context_is_enabled());
}
else if (re_system_gpu_context) {
WM_system_gpu_context_activate(re_system_gpu_context);
void *re_blender_gpu_context = RE_blender_gpu_context_ensure(&render_);
GPU_render_begin();
GPU_context_active_set(static_cast<GPUContext *>(re_blender_gpu_context));
}
else {
context.set_gpu_supported(false);
}
}
{
context.evaluate();
/* Reset the cache, but only if the evaluation did not get canceled, because in that case, we
* wouldn't want to invalidate the cache because not all operations that use cached resources
* got the chance to mark their used resources as still in use. So we wait until a full
* evaluation happen before we decide that some resources are no longer needed. */
if (!context.is_canceled()) {
context.cache_manager().reset();
}
last_evaluation_used_gpu_ = context.use_gpu();
last_evaluation_precision_ = context.get_precision();
}
if (context.use_gpu()) {
gpu::TexturePool::get().reset();
GHOST_IContext *re_system_gpu_context = RE_system_gpu_context_get(&render_);
if (BLI_thread_is_main() || re_system_gpu_context == nullptr) {
DRW_gpu_context_disable();
}
else {
GPU_context_active_set(nullptr);
GPU_render_end();
GHOST_IContext *re_system_gpu_context = RE_system_gpu_context_get(&render_);
WM_system_gpu_context_release(re_system_gpu_context);
}
}
}
/* Returns true if the compositor should be freed and reconstructed, which is needed when the
* compositor execution device or precision changed, because we either need to update all cached
* resources for the new execution device and precision, or we simply recreate the entire
* compositor, since it is much easier and safer. */
bool needs_to_be_recreated(const ContextInputData &input_data)
{
Context context(cache_manager_, input_data);
/* See last_evaluation_used_gpu_ and last_evaluation_precision_ for more information what how
* they are different from the ones returned from the context. */
return context.use_gpu() != last_evaluation_used_gpu_ ||
context.get_precision() != last_evaluation_precision_;
}
};
} // namespace render
void Render::compositor_execute(const Main &main,
const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const char *view_name,
compositor::RenderContext *render_context,
compositor::NodeGroupOutputTypes needed_outputs)
{
std::unique_lock lock(this->compositor_mutex);
render::ContextInputData input_data(
this, main, scene, render_data, node_tree, view_name, render_context, needed_outputs);
if (this->compositor && this->compositor->needs_to_be_recreated(input_data)) {
/* Free it here and it will be recreated in the check below. */
delete this->compositor;
this->compositor = nullptr;
}
if (!this->compositor) {
this->compositor = new render::Compositor(*this);
}
this->compositor->execute(input_data);
}
void Render::compositor_free()
{
std::unique_lock lock(this->compositor_mutex);
if (this->compositor != nullptr) {
delete this->compositor;
this->compositor = nullptr;
}
}
void RE_compositor_execute(Render &render,
const Main &main,
const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const char *view_name,
compositor::RenderContext *render_context,
compositor::NodeGroupOutputTypes needed_outputs)
{
render.compositor_execute(
main, scene, render_data, node_tree, view_name, render_context, needed_outputs);
}
void RE_compositor_free(Render &render)
{
render.compositor_free();
}
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
/* Global includes */
#include <cmath>
#include <cstdlib>
#include <cstring>
#include "BLI_math_base.h"
#include "BLI_math_matrix.h"
#include "BLI_rect.h"
#include "DNA_scene_types.h"
#include "BKE_camera.h"
/* this module */
#include "RE_pipeline.h"
#include "render_types.h"
namespace blender {
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
Object *RE_GetCamera(Render *re)
{
Object *camera = re->camera_override ? re->camera_override : re->scene->camera;
return BKE_camera_multiview_render(*re->main, re->scene, camera, re->viewname);
}
void RE_SetOverrideCamera(Render *re, Object *cam_ob)
{
re->camera_override = cam_ob;
}
void RE_SetCamera(Render *re, const Object *cam_ob)
{
CameraParams params;
/* setup parameters */
BKE_camera_params_init(&params);
BKE_camera_params_from_object(&params, cam_ob);
BKE_camera_multiview_params(&re->r, &params, cam_ob, re->viewname);
/* Compute matrix, view-plane, etc. */
BKE_camera_params_compute_viewplane(&params, re->winx, re->winy, re->r.xasp, re->r.yasp);
BKE_camera_params_compute_matrix(&params);
/* extract results */
copy_m4_m4(re->winmat, params.winmat);
re->clip_start = params.clip_start;
re->clip_end = params.clip_end;
re->viewplane = params.viewplane;
}
void RE_GetCameraWindow(Render *re, const Object *camera, float r_winmat[4][4])
{
RE_SetCamera(re, camera);
copy_m4_m4(r_winmat, re->winmat);
}
void RE_GetCameraWindowWithOverscan(const Render *re, float overscan, float r_winmat[4][4])
{
RE_GetWindowMatrixWithOverscan(
re->winmat[3][3] != 0.0f, re->clip_start, re->clip_end, re->viewplane, overscan, r_winmat);
}
void RE_GetCameraModelMatrix(const Render *re, const Object *camera, float r_modelmat[4][4])
{
BKE_camera_multiview_model_matrix(&re->r, camera, re->viewname, r_modelmat);
}
void RE_GetWindowMatrixWithOverscan(bool is_ortho,
float clip_start,
float clip_end,
rctf viewplane,
float overscan,
float r_winmat[4][4])
{
CameraParams params;
params.is_ortho = is_ortho;
params.clip_start = clip_start;
params.clip_end = clip_end;
params.viewplane = viewplane;
overscan *= max_ff(BLI_rctf_size_x(&params.viewplane), BLI_rctf_size_y(&params.viewplane));
params.viewplane.xmin -= overscan;
params.viewplane.xmax += overscan;
params.viewplane.ymin -= overscan;
params.viewplane.ymax += overscan;
BKE_camera_params_compute_matrix(&params);
copy_m4_m4(r_winmat, params.winmat);
}
void RE_GetViewPlane(Render *re, rctf *r_viewplane, rcti *r_disprect)
{
*r_viewplane = re->viewplane;
/* make disprect zero when no border render, is needed to detect changes in 3d view render */
if (re->r.mode & R_BORDER) {
*r_disprect = re->disprect;
}
else {
BLI_rcti_init(r_disprect, 0, 0, 0, 0);
}
}
} // namespace blender

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
/* SPDX-FileCopyrightText: 2006 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
namespace blender {
struct Render;
struct RenderData;
struct RenderLayer;
struct RenderResult;
RenderLayer *render_get_single_layer(Render *re, RenderResult *rr);
void render_copy_renderdata(RenderData *to, RenderData *from);
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
/* SPDX-FileCopyrightText: 2007 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
#include "BKE_global.hh" /* IWYU pragma: keep. Used in macro. */
namespace blender {
#define PASS_VECTOR_MAX 10000.0f
#define RR_ALL_LAYERS NULL
#define RR_ALL_VIEWS NULL
struct ColorManagedDisplaySettings;
struct ColorManagedViewSettings;
struct ExrHandle;
struct ImBuf;
struct Render;
struct RenderData;
struct RenderLayer;
struct RenderResult;
struct ReportList;
struct rcti;
/* New */
/**
* Called by main render as well for parts will read info from Render *re to define layers.
* \note Called in threads.
*
* `re->winx`, `re->winy` is coordinate space of entire image, `partrct` the part within.
*/
struct RenderResult *render_result_new(struct Render *re,
const struct rcti *partrct,
const char *layername,
const char *viewname);
void render_result_passes_allocated_ensure(struct RenderResult *rr);
/**
* From `imbuf`, if a handle was returned and
* it's not a single-layer multi-view we convert this to render result.
*/
struct RenderResult *render_result_new_from_exr(
ExrHandle *exrhandle, const char *colorspace, bool predivide, int rectx, int recty);
void render_result_view_new(struct RenderResult *rr, const char *viewname);
void render_result_views_new(struct RenderResult *rr, const struct RenderData *rd);
/* Merge */
/**
* Used when rendering to a full buffer, or when reading the EXR part-layer-pass file.
* no test happens here if it fits... we also assume layers are in sync.
* \note Is used within threads.
*/
void render_result_merge(struct RenderResult *rr, struct RenderResult *rrpart);
/* Add Passes */
void render_result_clone_passes(struct Render *re, struct RenderResult *rr, const char *viewname);
/* Free */
void render_result_free(struct RenderResult *rr);
/**
* Version that's compatible with full-sample buffers.
*/
void render_result_free_list(ListBaseT<RenderResult> *lb, struct RenderResult *rr);
/* Single Layer Render */
void render_result_single_layer_begin(struct Render *re);
/**
* If #RenderData.scemode is #R_SINGLE_LAYER, at end of rendering, merge the both render results.
*/
void render_result_single_layer_end(struct Render *re);
/**
* Render pass wrapper for grease-pencil.
*/
struct RenderPass *render_layer_add_pass(struct RenderResult *rr,
struct RenderLayer *rl,
int channels,
const char *name,
const char *viewname,
const char *chan_id,
bool allocate);
/**
* Called for reading temp files, and for external engines.
*/
bool render_result_exr_file_read_path(struct RenderResult *rr,
struct RenderLayer *rl_single,
struct ReportList *reports,
const char *filepath);
/* EXR cache */
void render_result_exr_file_cache_write(struct Render *re);
/**
* For cache, makes exact copy of render result.
*/
bool render_result_exr_file_cache_read(struct Render *re);
/* Combined Pixel Rect */
struct ImBuf *render_result_rect_to_ibuf(struct RenderResult *rr,
const struct RenderData *rd,
int view_id);
void render_result_rect_fill_zero(struct RenderResult *rr, int view_id);
void render_result_rect_get_pixels(struct RenderResult *rr,
uint8_t *rect,
int rectx,
int recty,
const struct ColorManagedViewSettings *view_settings,
const struct ColorManagedDisplaySettings *display_settings,
int view_id);
/**
* Create a new views #ListBaseT in rr without duplicating the memory pointers.
*/
void render_result_views_shallowcopy(struct RenderResult *dst, struct RenderResult *src);
/**
* Free the views created temporarily.
*/
void render_result_views_shallowdelete(struct RenderResult *rr);
/**
* Free GPU texture caches to reduce memory usage.
*/
void render_result_free_gpu_texture_caches(struct RenderResult *rr);
#define FOREACH_VIEW_LAYER_TO_RENDER_BEGIN(re_, iter_) \
{ \
ViewLayer *iter_; \
for (iter_ = static_cast<ViewLayer *>((re_)->scene->view_layers.first); iter_ != NULL; \
iter_ = iter_->next) \
{ \
if (!G.background && (re_)->r.scemode & R_SINGLE_LAYER) { \
if (!STREQ(iter_->name, re->single_view_layer)) { \
continue; \
} \
} \
else { \
if ((iter_->flag & VIEW_LAYER_RENDER) == 0) { \
continue; \
} \
}
#define FOREACH_VIEW_LAYER_TO_RENDER_END \
} \
} \
((void)0)
} // namespace blender

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#include "render_types.h"
#include <memory>
#include "BKE_colortools.hh"
#include "BLI_assert.h"
#include "RE_compositor.hh"
#include "RE_engine.h"
#include "render_result.h"
#include "GPU_context.hh"
#include "WM_api.hh"
#include "wm_window.hh"
namespace blender {
/* -------------------------------------------------------------------- */
/** \name Render
* \{ */
BaseRender::~BaseRender()
{
if (engine) {
RE_engine_free(engine);
}
render_result_free(result);
/* Free GPU context after engine, which may need context for cleanup. */
display.reset();
BLI_rw_mutex_end(&resultmutex);
BLI_mutex_end(&engine_draw_mutex);
}
Render::Render()
{
display = std::make_shared<RenderDisplay>();
}
Render::~Render()
{
RE_compositor_free(*this);
BKE_curvemapping_free_data(&r.mblur_shutter_curve);
render_result_free(pushedresult);
}
bool Render::prepare_viewlayer(ViewLayer *view_layer, Depsgraph *depsgraph)
{
if (!prepare_viewlayer_cb) {
return true;
}
return prepare_viewlayer_cb(prepare_vl_handle, view_layer, depsgraph);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name RenderDisplay
* \{ */
RenderDisplay::~RenderDisplay()
{
free_gpu_context();
display_update_cb = nullptr;
current_scene_update_cb = nullptr;
stats_draw_cb = nullptr;
progress_cb = nullptr;
draw_lock_cb = nullptr;
test_break_cb = nullptr;
}
void RenderDisplay::free_gpu_context()
{
if (blender_gpu_context) {
WM_system_gpu_context_activate(system_gpu_context);
GPU_context_active_set(static_cast<GPUContext *>(blender_gpu_context));
GPU_context_discard(static_cast<GPUContext *>(blender_gpu_context));
blender_gpu_context = nullptr;
}
if (system_gpu_context) {
WM_system_gpu_context_dispose(system_gpu_context);
system_gpu_context = nullptr;
/* If in main thread, reset window context. */
if (BLI_thread_is_main()) {
wm_window_reset_drawable();
}
}
}
void RenderDisplay::ensure_system_gpu_context()
{
BLI_assert(BLI_thread_is_main());
if (system_gpu_context == nullptr) {
/* Needs to be created in the main thread. */
system_gpu_context = WM_system_gpu_context_create();
/* The context is activated during creation, so release it here since the function should not
* have context activation as a side effect. Then activate the drawable's context below. */
if (system_gpu_context) {
WM_system_gpu_context_release(system_gpu_context);
}
wm_window_reset_drawable();
}
}
void *RenderDisplay::ensure_blender_gpu_context()
{
BLI_assert(system_gpu_context != nullptr);
if (blender_gpu_context == nullptr) {
blender_gpu_context = GPU_context_create(nullptr, system_gpu_context);
}
return blender_gpu_context;
}
void RenderDisplay::display_update(RenderResult *render_result, rcti *rect)
{
if (display_update_cb) {
display_update_cb(duh, render_result, rect);
}
}
void RenderDisplay::current_scene_update(Scene *scene)
{
if (current_scene_update_cb) {
current_scene_update_cb(suh, scene);
}
}
void RenderDisplay::stats_draw(RenderStats *render_stats)
{
if (stats_draw_cb) {
stats_draw_cb(sdh, render_stats);
}
}
void RenderDisplay::progress(float progress)
{
if (progress_cb) {
progress_cb(prh, progress);
}
}
void RenderDisplay::draw_lock()
{
if (draw_lock_cb) {
draw_lock_cb(dlh, true);
}
}
void RenderDisplay::draw_unlock()
{
if (draw_lock_cb) {
draw_lock_cb(dlh, false);
}
}
bool RenderDisplay::test_break()
{
if (!test_break_cb) {
return false;
}
return test_break_cb(tbh);
}
/** \} */
} // namespace blender

View File

@@ -0,0 +1,258 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
/* ------------------------------------------------------------------------- */
/* exposed internal in render module only! */
/* ------------------------------------------------------------------------- */
#include <memory>
#include "DNA_scene_types.h"
#include "BLI_mutex.hh"
#include "BLI_threads.h"
#include "RE_compositor.hh"
#include "RE_pipeline.h"
#include "tile_highlight.h"
class GHOST_IContext;
namespace blender {
namespace compositor {
class RenderContext;
enum class NodeGroupOutputTypes : uint8_t;
} // namespace compositor
struct bNodeTree;
struct Depsgraph;
struct Main;
struct Object;
struct RenderDisplay;
struct RenderEngine;
struct ReportList;
struct Scene;
struct BaseRender {
BaseRender() = default;
virtual ~BaseRender();
/* Get class which manages highlight of tiles.
* Note that it might not exist: for example, viewport render does not support the tile
* highlight. */
virtual render::TilesHighlight *get_tile_highlight() = 0;
virtual void compositor_execute(const Main &main,
const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const char *view_name,
compositor::RenderContext *render_context,
compositor::NodeGroupOutputTypes needed_outputs) = 0;
virtual void compositor_free() = 0;
/**
* Executed right before the initialization of the depsgraph, in order to modify some stuff in
* the viewlayer. The modified ids must be tagged in the depsgraph.
*
* If false is returned then rendering is aborted,
*/
virtual bool prepare_viewlayer(struct ViewLayer *view_layer, struct Depsgraph *depsgraph) = 0;
/* Result of rendering */
RenderResult *result = nullptr;
/* Read/write mutex, all internal code that writes to the `result` must use a
* write lock, all external code must use a read lock. Internal code is assumed
* to not conflict with writes, so no lock used for that. */
ThreadRWMutex resultmutex = BLI_RWLOCK_INITIALIZER;
/* Render engine. */
struct RenderEngine *engine = nullptr;
/* Guard for drawing render result using engine's `draw()` callback. */
ThreadMutex engine_draw_mutex = BLI_MUTEX_INITIALIZER;
/**
* GPU context and callbacks. This can be shared for recursive compositor and
* sequencer renders that we want to display in the same place.
*/
std::shared_ptr<RenderDisplay> display;
bool display_shared = false;
};
struct ViewRender : public BaseRender {
render::TilesHighlight *get_tile_highlight() override
{
return nullptr;
}
void compositor_execute(const Main & /*main*/,
const Scene & /*scene*/,
const RenderData & /*render_data*/,
const bNodeTree & /*node_tree*/,
const char * /*view_name*/,
compositor::RenderContext * /*render_context*/,
compositor::NodeGroupOutputTypes /*needed_outputs*/) override
{
}
void compositor_free() override {}
bool prepare_viewlayer(struct ViewLayer * /*view_layer*/,
struct Depsgraph * /*depsgraph*/) override
{
return true;
}
};
/** Controls state of render, everything that's read-only during render stage. */
struct Render : public BaseRender {
Render();
~Render() override;
render::TilesHighlight *get_tile_highlight() override
{
return &tile_highlight;
}
void compositor_execute(const Main &main,
const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const char *view_name,
compositor::RenderContext *render_context,
compositor::NodeGroupOutputTypes needed_outputs) override;
void compositor_free() override;
bool prepare_viewlayer(struct ViewLayer *view_layer, struct Depsgraph *depsgraph) override;
/* Owner pointer that uniquely identifiers the owner of this scene. */
const void *owner = nullptr;
/* state settings */
short flag = 0;
bool ok = false;
/* if render with single-layer option, other rendered layers are stored here */
RenderResult *pushedresult = nullptr;
/** A list of #RenderResults, for full-samples. */
ListBaseT<RenderResult> fullresult = {nullptr, nullptr};
/* True if result has GPU textures, to quickly skip cache clear. */
bool result_has_gpu_texture_caches = false;
/** Window size, display rect, viewplane.
* \note Buffer width and height with percentage applied
* without border & crop. convert to long before multiplying together to avoid overflow. */
int winx = 0, winy = 0;
rcti disprect = {0, 0, 0, 0}; /* part within winx winy */
rctf viewplane = {0, 0, 0, 0}; /* mapped on winx winy */
/* final picture width and height (within disprect) */
int rectx = 0, recty = 0;
/* Camera transform. Used by Freestyle, Eevee, and other draw manager engines.. */
float winmat[4][4] = {{0}};
/* Clipping. */
float clip_start = 0.0f;
float clip_end = 0.0f;
/* main, scene, and its full copy of renderdata and world */
struct Main *main = nullptr;
Scene *scene = nullptr;
RenderData r = {};
char single_view_layer[MAX_NAME] = "";
struct Object *camera_override = nullptr;
render::TilesHighlight tile_highlight;
/* NOTE: This is a minimal dependency graph and evaluated scene which is enough to access view
* layer visibility and use for postprocessing (compositor and sequencer). */
struct Depsgraph *pipeline_depsgraph = nullptr;
Scene *pipeline_scene_eval = nullptr;
/* Compositor.
* NOTE: Use bare pointer instead of smart pointer because the it is a fully opaque type. */
render::Compositor *compositor = nullptr;
Mutex compositor_mutex;
/* Callbacks for the corresponding base class method implementation. */
bool (*prepare_viewlayer_cb)(void *handle,
struct ViewLayer *vl,
struct Depsgraph *depsgraph) = nullptr;
void *prepare_vl_handle = nullptr;
RenderStats i = {};
/**
* Optional report list which may be null (borrowed memory).
* Callers to rendering functions are responsible for setting can clearing, see: #RE_SetReports.
*/
struct ReportList *reports = nullptr;
Vector<MovieWriter *> movie_writers;
char viewname[MAX_NAME] = "";
};
struct RenderDisplay {
~RenderDisplay();
void free_gpu_context();
void ensure_system_gpu_context();
void *ensure_blender_gpu_context();
void display_update(RenderResult *render_result, rcti *rect);
void current_scene_update(struct Scene *scene);
void stats_draw(RenderStats *render_stats);
void progress(float progress);
void draw_lock();
void draw_unlock();
bool test_break();
/* Callbacks */
void (*display_update_cb)(void *handle, RenderResult *rr, rcti *rect) = nullptr;
void *duh = nullptr;
void (*current_scene_update_cb)(void *handle, struct Scene *scene) = nullptr;
void *suh = nullptr;
void (*stats_draw_cb)(void *handle, RenderStats *ri) = nullptr;
void *sdh = nullptr;
void (*progress_cb)(void *handle, float i) = nullptr;
void *prh = nullptr;
void (*draw_lock_cb)(void *handle, bool lock) = nullptr;
void *dlh = nullptr;
bool (*test_break_cb)(void *handle) = nullptr;
void *tbh = nullptr;
/* GPU contexts.
* TODO: replace by a whole draw manager. */
GHOST_IContext *system_gpu_context = nullptr;
void *blender_gpu_context = nullptr;
};
/* **************** defines ********************* */
/** #R.flag */
#define R_ANIMATION 1 << 0
/* Indicates that the render pipeline should not write its render result. This happens for instance
* when the render pipeline uses the compositor, but the compositor node tree does not have a group
* output node or a render layer input, and consequently no render result. In that case, the output
* will be written from the File Output nodes, since the render pipeline will early fail if neither
* a File Output nor a Group Output node exist in the scene. */
#define R_SKIP_WRITE 1 << 1
} // namespace blender

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
#include "BLI_math_color.h" /* IWYU pragma: keep. Used in macros. */
namespace blender {
#define BRICONT \
texres->tin = (texres->tin - 0.5f) * tex->contrast + tex->bright - 0.5f; \
if (!(tex->flag & TEX_NO_CLAMP)) { \
if (texres->tin < 0.0f) { \
texres->tin = 0.0f; \
} \
else if (texres->tin > 1.0f) { \
texres->tin = 1.0f; \
} \
} \
((void)0)
#define BRICONTRGB \
texres->trgba[0] = tex->rfac * \
((texres->trgba[0] - 0.5f) * tex->contrast + tex->bright - 0.5f); \
texres->trgba[1] = tex->gfac * \
((texres->trgba[1] - 0.5f) * tex->contrast + tex->bright - 0.5f); \
texres->trgba[2] = tex->bfac * \
((texres->trgba[2] - 0.5f) * tex->contrast + tex->bright - 0.5f); \
if (!(tex->flag & TEX_NO_CLAMP)) { \
if (texres->trgba[0] < 0.0f) { \
texres->trgba[0] = 0.0f; \
} \
if (texres->trgba[1] < 0.0f) { \
texres->trgba[1] = 0.0f; \
} \
if (texres->trgba[2] < 0.0f) { \
texres->trgba[2] = 0.0f; \
} \
} \
if (tex->saturation != 1.0f) { \
float _hsv[3]; \
rgb_to_hsv(texres->trgba[0], texres->trgba[1], texres->trgba[2], _hsv, _hsv + 1, _hsv + 2); \
_hsv[1] *= tex->saturation; \
hsv_to_rgb( \
_hsv[0], _hsv[1], _hsv[2], &texres->trgba[0], &texres->trgba[1], &texres->trgba[2]); \
if ((tex->saturation > 1.0f) && !(tex->flag & TEX_NO_CLAMP)) { \
if (texres->trgba[0] < 0.0f) { \
texres->trgba[0] = 0.0f; \
} \
if (texres->trgba[1] < 0.0f) { \
texres->trgba[1] = 0.0f; \
} \
if (texres->trgba[2] < 0.0f) { \
texres->trgba[2] = 0.0f; \
} \
} \
} \
((void)0)
struct ImBuf;
struct Image;
struct ImagePool;
struct Tex;
struct TexResult;
/* `texture_image.cc` */
int imagewrap(struct Tex *tex,
struct Image *ima,
const float texvec[3],
struct TexResult *texres,
struct ImagePool *pool,
bool skip_load_image);
void image_sample(struct Image *ima,
float fx,
float fy,
float dx,
float dy,
float result[4],
struct ImagePool *pool);
} // namespace blender

View File

@@ -0,0 +1,661 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <cstring>
#include <fcntl.h>
#ifndef WIN32
# include <unistd.h>
#else
# include <io.h>
#endif
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "DNA_image_types.h"
#include "DNA_texture_types.h"
#include "BLI_math_vector.h"
#include "BLI_rect.h"
#include "BLI_threads.h"
#include "BLI_utildefines.h"
#include "BKE_image.hh"
#include "RE_texture.h"
#include "texture_common.h"
namespace blender {
static void boxsample(ImBuf *ibuf,
float minx,
float miny,
float maxx,
float maxy,
TexResult *texres,
const short imaprepeat,
const short imapextend);
/* *********** IMAGEWRAPPING ****************** */
/* x and y have to be checked for image size */
static void ibuf_get_color(float col[4], const ImBuf *ibuf, int x, int y)
{
const int64_t ofs = int64_t(y) * ibuf->x + x;
if (ibuf->float_data()) {
if (ibuf->channels == 4) {
const float *fp = ibuf->float_data() + 4 * ofs;
copy_v4_v4(col, fp);
}
else if (ibuf->channels == 3) {
const float *fp = ibuf->float_data() + 3 * ofs;
copy_v3_v3(col, fp);
col[3] = 1.0f;
}
else {
const float *fp = ibuf->float_data() + ofs;
col[0] = col[1] = col[2] = col[3] = *fp;
}
}
else {
const uchar *rect = ibuf->byte_data() + 4 * ofs;
col[0] = float(rect[0]) * (1.0f / 255.0f);
col[1] = float(rect[1]) * (1.0f / 255.0f);
col[2] = float(rect[2]) * (1.0f / 255.0f);
col[3] = float(rect[3]) * (1.0f / 255.0f);
/* Bytes are internally straight, however render pipeline seems to expect pre-multiplied. */
col[0] *= col[3];
col[1] *= col[3];
col[2] *= col[3];
}
}
int imagewrap(Tex *tex,
Image *ima,
const float texvec[3],
TexResult *texres,
ImagePool *pool,
const bool skip_load_image)
{
float fx, fy;
int x, y, retval;
int xi, yi; /* original values */
texres->tin = texres->trgba[3] = texres->trgba[0] = texres->trgba[1] = texres->trgba[2] = 0.0f;
retval = TEX_RGB;
/* quick tests */
if (ima == nullptr) {
return retval;
}
/* hack for icon render */
if (skip_load_image && !BKE_image_has_loaded_ibuf(ima)) {
return retval;
}
ImageUser *iuser = &tex->iuser;
ImageUser local_iuser;
if (ima->source == IMA_SRC_TILED) {
/* tex->iuser might be shared by threads, so create a local copy. */
local_iuser = tex->iuser;
iuser = &local_iuser;
float new_uv[2];
iuser->tile = BKE_image_get_tile_from_pos(ima, texvec, new_uv, nullptr);
fx = new_uv[0];
fy = new_uv[1];
}
else {
fx = texvec[0];
fy = texvec[1];
}
ImBuf *ibuf = BKE_image_pool_acquire_ibuf(ima, iuser, pool);
ima->flag |= IMA_USED_FOR_RENDER;
if (ibuf == nullptr || (ibuf->byte_data() == nullptr && ibuf->float_data() == nullptr)) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
return retval;
}
/* setup mapping */
if (tex->imaflag & TEX_IMAROT) {
std::swap(fx, fy);
}
if (tex->extend == TEX_CHECKER) {
int xs, ys;
xs = int(floor(fx));
ys = int(floor(fy));
fx -= xs;
fy -= ys;
if ((tex->flag & TEX_CHECKER_ODD) == 0) {
if ((xs + ys) & 1) {
/* pass */
}
else {
if (ima) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
return retval;
}
}
if ((tex->flag & TEX_CHECKER_EVEN) == 0) {
if ((xs + ys) & 1) {
if (ima) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
return retval;
}
}
/* scale around center, (0.5, 0.5) */
if (tex->checkerdist < 1.0f) {
fx = (fx - 0.5f) / (1.0f - tex->checkerdist) + 0.5f;
fy = (fy - 0.5f) / (1.0f - tex->checkerdist) + 0.5f;
}
}
x = xi = int(floorf(fx * ibuf->x));
y = yi = int(floorf(fy * ibuf->y));
if (tex->extend == TEX_CLIPCUBE) {
if (x < 0 || y < 0 || x >= ibuf->x || y >= ibuf->y || texvec[2] < -1.0f || texvec[2] > 1.0f) {
if (ima) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
return retval;
}
}
else if (ELEM(tex->extend, TEX_CLIP, TEX_CHECKER)) {
if (x < 0 || y < 0 || x >= ibuf->x || y >= ibuf->y) {
if (ima) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
return retval;
}
}
else {
if (tex->extend == TEX_EXTEND) {
if (x >= ibuf->x) {
x = ibuf->x - 1;
}
else if (x < 0) {
x = 0;
}
}
else {
x = x % ibuf->x;
if (x < 0) {
x += ibuf->x;
}
}
if (tex->extend == TEX_EXTEND) {
if (y >= ibuf->y) {
y = ibuf->y - 1;
}
else if (y < 0) {
y = 0;
}
}
else {
y = y % ibuf->y;
if (y < 0) {
y += ibuf->y;
}
}
}
/* Keep this before interpolation #29761. */
if (ima) {
if ((tex->imaflag & TEX_USEALPHA) && (ima->alpha_mode != IMA_ALPHA_IGNORE)) {
if ((tex->imaflag & TEX_CALCALPHA) == 0) {
texres->talpha = true;
}
}
}
/* interpolate */
if (tex->imaflag & TEX_INTERPOL) {
float filterx, filtery;
filterx = (0.5f * tex->filtersize) / ibuf->x;
filtery = (0.5f * tex->filtersize) / ibuf->y;
/* Important that this value is wrapped #27782.
* this applies the modifications made by the checks above,
* back to the floating point values */
fx -= float(xi - x) / float(ibuf->x);
fy -= float(yi - y) / float(ibuf->y);
boxsample(ibuf,
fx - filterx,
fy - filtery,
fx + filterx,
fy + filtery,
texres,
(tex->extend == TEX_REPEAT),
(tex->extend == TEX_EXTEND));
}
else { /* no filtering */
ibuf_get_color(texres->trgba, ibuf, x, y);
}
if (texres->talpha) {
texres->tin = texres->trgba[3];
}
else if (tex->imaflag & TEX_CALCALPHA) {
texres->trgba[3] = texres->tin = std::max(
{texres->trgba[0], texres->trgba[1], texres->trgba[2]});
}
else {
texres->trgba[3] = texres->tin = 1.0;
}
if (tex->flag & TEX_NEGALPHA) {
texres->trgba[3] = 1.0f - texres->trgba[3];
}
/* De-pre-multiply, this is being pre-multiplied in #shade_input_do_shade()
* do not de-pre-multiply for generated alpha, it is already in straight. */
if (texres->trgba[3] != 1.0f && texres->trgba[3] > 1e-4f && !(tex->imaflag & TEX_CALCALPHA)) {
fx = 1.0f / texres->trgba[3];
texres->trgba[0] *= fx;
texres->trgba[1] *= fx;
texres->trgba[2] *= fx;
}
if (ima) {
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
BRICONTRGB;
return retval;
}
static void clipx_rctf_swap(rctf *stack, short *count, float x1, float x2)
{
rctf *rf, *newrct;
short a;
a = *count;
rf = stack;
for (; a > 0; a--) {
if (rf->xmin < x1) {
if (rf->xmax < x1) {
rf->xmin += (x2 - x1);
rf->xmax += (x2 - x1);
}
else {
rf->xmax = std::min(rf->xmax, x2);
newrct = stack + *count;
(*count)++;
newrct->xmax = x2;
newrct->xmin = rf->xmin + (x2 - x1);
newrct->ymin = rf->ymin;
newrct->ymax = rf->ymax;
if (newrct->xmin == newrct->xmax) {
(*count)--;
}
rf->xmin = x1;
}
}
else if (rf->xmax > x2) {
if (rf->xmin > x2) {
rf->xmin -= (x2 - x1);
rf->xmax -= (x2 - x1);
}
else {
rf->xmin = std::max(rf->xmin, x1);
newrct = stack + *count;
(*count)++;
newrct->xmin = x1;
newrct->xmax = rf->xmax - (x2 - x1);
newrct->ymin = rf->ymin;
newrct->ymax = rf->ymax;
if (newrct->xmin == newrct->xmax) {
(*count)--;
}
rf->xmax = x2;
}
}
rf++;
}
}
static void clipy_rctf_swap(rctf *stack, short *count, float y1, float y2)
{
rctf *rf, *newrct;
short a;
a = *count;
rf = stack;
for (; a > 0; a--) {
if (rf->ymin < y1) {
if (rf->ymax < y1) {
rf->ymin += (y2 - y1);
rf->ymax += (y2 - y1);
}
else {
rf->ymax = std::min(rf->ymax, y2);
newrct = stack + *count;
(*count)++;
newrct->ymax = y2;
newrct->ymin = rf->ymin + (y2 - y1);
newrct->xmin = rf->xmin;
newrct->xmax = rf->xmax;
if (newrct->ymin == newrct->ymax) {
(*count)--;
}
rf->ymin = y1;
}
}
else if (rf->ymax > y2) {
if (rf->ymin > y2) {
rf->ymin -= (y2 - y1);
rf->ymax -= (y2 - y1);
}
else {
rf->ymin = std::max(rf->ymin, y1);
newrct = stack + *count;
(*count)++;
newrct->ymin = y1;
newrct->ymax = rf->ymax - (y2 - y1);
newrct->xmin = rf->xmin;
newrct->xmax = rf->xmax;
if (newrct->ymin == newrct->ymax) {
(*count)--;
}
rf->ymax = y2;
}
}
rf++;
}
}
static float square_rctf(const rctf *rf)
{
float x, y;
x = BLI_rctf_size_x(rf);
y = BLI_rctf_size_y(rf);
return x * y;
}
static float clipx_rctf(rctf *rf, float x1, float x2)
{
float size;
size = BLI_rctf_size_x(rf);
rf->xmin = std::max(rf->xmin, x1);
rf->xmax = std::min(rf->xmax, x2);
if (rf->xmin > rf->xmax) {
rf->xmin = rf->xmax;
return 0.0;
}
if (size != 0.0f) {
return BLI_rctf_size_x(rf) / size;
}
return 1.0;
}
static float clipy_rctf(rctf *rf, float y1, float y2)
{
float size;
size = BLI_rctf_size_y(rf);
rf->ymin = std::max(rf->ymin, y1);
rf->ymax = std::min(rf->ymax, y2);
if (rf->ymin > rf->ymax) {
rf->ymin = rf->ymax;
return 0.0;
}
if (size != 0.0f) {
return BLI_rctf_size_y(rf) / size;
}
return 1.0;
}
static void boxsampleclip(ImBuf *ibuf, const rctf *rf, TexResult *texres)
{
/* Sample box, is clipped already, and minx etc. have been set at ibuf size.
* Enlarge with anti-aliased edges of the pixels. */
float muly, mulx, div, col[4];
int x, y, startx, endx, starty, endy;
startx = int(floor(rf->xmin));
endx = int(floor(rf->xmax));
starty = int(floor(rf->ymin));
endy = int(floor(rf->ymax));
startx = std::max(startx, 0);
starty = std::max(starty, 0);
if (endx >= ibuf->x) {
endx = ibuf->x - 1;
}
if (endy >= ibuf->y) {
endy = ibuf->y - 1;
}
if (starty == endy && startx == endx) {
ibuf_get_color(texres->trgba, ibuf, startx, starty);
}
else {
div = texres->trgba[0] = texres->trgba[1] = texres->trgba[2] = texres->trgba[3] = 0.0;
for (y = starty; y <= endy; y++) {
muly = 1.0;
if (starty == endy) {
/* pass */
}
else {
if (y == starty) {
muly = 1.0f - (rf->ymin - y);
}
if (y == endy) {
muly = (rf->ymax - y);
}
}
if (startx == endx) {
mulx = muly;
ibuf_get_color(col, ibuf, startx, y);
madd_v4_v4fl(texres->trgba, col, mulx);
div += mulx;
}
else {
for (x = startx; x <= endx; x++) {
mulx = muly;
if (x == startx) {
mulx *= 1.0f - (rf->xmin - x);
}
if (x == endx) {
mulx *= (rf->xmax - x);
}
ibuf_get_color(col, ibuf, x, y);
/* TODO(jbakker): No need to do manual optimization. Branching is slower than multiplying
* with 1. */
if (mulx == 1.0f) {
add_v4_v4(texres->trgba, col);
div += 1.0f;
}
else {
madd_v4_v4fl(texres->trgba, col, mulx);
div += mulx;
}
}
}
}
if (div != 0.0f) {
div = 1.0f / div;
mul_v4_fl(texres->trgba, div);
}
else {
zero_v4(texres->trgba);
}
}
}
static void boxsample(ImBuf *ibuf,
float minx,
float miny,
float maxx,
float maxy,
TexResult *texres,
const short imaprepeat,
const short imapextend)
{
/* Sample box, performs clip. minx etc are in range 0.0 - 1.0 .
* Enlarge with anti-aliased edges of pixels.
* If variable 'imaprepeat' has been set, the
* clipped-away parts are sampled as well.
*/
/* NOTE: actually minx etc isn't in the proper range...
* this due to filter size and offset vectors for bump. */
/* NOTE: talpha must be initialized. */
/* NOTE: even when 'imaprepeat' is set, this can only repeat once in any direction.
* the point which min/max is derived from is assumed to be wrapped. */
TexResult texr;
rctf *rf, stack[8];
float opp, tot, alphaclip = 1.0;
short count = 1;
rf = stack;
rf->xmin = minx * (ibuf->x);
rf->xmax = maxx * (ibuf->x);
rf->ymin = miny * (ibuf->y);
rf->ymax = maxy * (ibuf->y);
texr.talpha = texres->talpha; /* is read by boxsample_clip */
if (imapextend) {
CLAMP(rf->xmin, 0.0f, ibuf->x - 1);
CLAMP(rf->xmax, 0.0f, ibuf->x - 1);
}
else if (imaprepeat) {
clipx_rctf_swap(stack, &count, 0.0, float(ibuf->x));
}
else {
alphaclip = clipx_rctf(rf, 0.0, float(ibuf->x));
if (alphaclip <= 0.0f) {
texres->trgba[0] = texres->trgba[2] = texres->trgba[1] = texres->trgba[3] = 0.0;
return;
}
}
if (imapextend) {
CLAMP(rf->ymin, 0.0f, ibuf->y - 1);
CLAMP(rf->ymax, 0.0f, ibuf->y - 1);
}
else if (imaprepeat) {
clipy_rctf_swap(stack, &count, 0.0, float(ibuf->y));
}
else {
alphaclip *= clipy_rctf(rf, 0.0, float(ibuf->y));
if (alphaclip <= 0.0f) {
texres->trgba[0] = texres->trgba[2] = texres->trgba[1] = texres->trgba[3] = 0.0;
return;
}
}
if (count > 1) {
tot = texres->trgba[0] = texres->trgba[2] = texres->trgba[1] = texres->trgba[3] = 0.0;
while (count--) {
boxsampleclip(ibuf, rf, &texr);
opp = square_rctf(rf);
tot += opp;
texres->trgba[0] += opp * texr.trgba[0];
texres->trgba[1] += opp * texr.trgba[1];
texres->trgba[2] += opp * texr.trgba[2];
if (texres->talpha) {
texres->trgba[3] += opp * texr.trgba[3];
}
rf++;
}
if (tot != 0.0f) {
texres->trgba[0] /= tot;
texres->trgba[1] /= tot;
texres->trgba[2] /= tot;
if (texres->talpha) {
texres->trgba[3] /= tot;
}
}
}
else {
boxsampleclip(ibuf, rf, texres);
}
if (texres->talpha == 0) {
texres->trgba[3] = 1.0;
}
if (alphaclip != 1.0f) {
/* Pre-multiply it all. */
texres->trgba[0] *= alphaclip;
texres->trgba[1] *= alphaclip;
texres->trgba[2] *= alphaclip;
texres->trgba[3] *= alphaclip;
}
}
void image_sample(
Image *ima, float fx, float fy, float dx, float dy, float result[4], ImagePool *pool)
{
TexResult texres;
ImBuf *ibuf = BKE_image_pool_acquire_ibuf(ima, nullptr, pool);
if (UNLIKELY(ibuf == nullptr)) {
zero_v4(result);
return;
}
texres.talpha = true; /* boxsample expects to be initialized */
boxsample(ibuf, fx, fy, fx + dx, fy + dy, &texres, 0, 1);
copy_v4_v4(result, texres.trgba);
ima->flag |= IMA_USED_FOR_RENDER;
BKE_image_pool_release_ibuf(ima, ibuf, pool);
}
} // namespace blender

View File

@@ -0,0 +1,595 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#include "BLI_assert.h"
#include "BLI_math_geom.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
#include "BKE_attribute.hh"
#include "BKE_customdata.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_mapping.hh"
#include "IMB_imbuf.hh"
#include "IMB_interp.hh"
#include "MEM_guardedalloc.h"
#include "zbuf.h" /* For rasterizer (#ZSpan and associated functions). */
#include "RE_texture_margin.h"
#include <algorithm>
#include <cmath>
namespace blender {
namespace render::texturemargin {
/**
* The map class contains both a pixel map which maps out face indices for all UV-polygons and
* adjacency tables.
*/
class TextureMarginMap {
static const int directions[8][2];
static const int distances[8];
/** Maps UV-edges to their corresponding UV-edge. */
Vector<int> loop_adjacency_map_;
/** Maps UV-edges to their corresponding face. */
Array<int> loop_to_face_map_;
int w_, h_;
float uv_offset_[2];
Vector<uint32_t> pixel_data_;
ZSpan zspan_;
uint32_t value_to_store_;
bool write_mask_;
char *mask_;
OffsetIndices<int> faces_;
Span<int> corner_edges_;
Span<float2> uv_map_;
int totedge_;
public:
TextureMarginMap(size_t w,
size_t h,
const float uv_offset[2],
const int totedge,
const OffsetIndices<int> faces,
const Span<int> corner_edges,
const Span<float2> uv_map)
: w_(w),
h_(h),
faces_(faces),
corner_edges_(corner_edges),
uv_map_(uv_map),
totedge_(totedge)
{
copy_v2_v2(uv_offset_, uv_offset);
pixel_data_.resize(w_ * h_, 0xFFFFFFFF);
zbuf_alloc_span(&zspan_, w_, h_);
build_tables();
}
~TextureMarginMap()
{
zbuf_free_span(&zspan_);
}
void set_pixel(int x, int y, uint32_t value)
{
BLI_assert(x < w_);
BLI_assert(x >= 0);
pixel_data_[y * w_ + x] = value;
}
uint32_t get_pixel(int x, int y) const
{
if (x < 0 || y < 0 || x >= w_ || y >= h_) {
return 0xFFFFFFFF;
}
return pixel_data_[y * w_ + x];
}
void rasterize_tri(float *v1, float *v2, float *v3, uint32_t value, char *mask, bool writemask)
{
/* NOTE: This is not thread safe, because the value to be written by the rasterizer is
* a class member. If this is ever made multi-threaded each thread needs to get its own. */
value_to_store_ = value;
mask_ = mask;
write_mask_ = writemask;
zspan_scanconvert(
&zspan_, this, &(v1[0]), &(v2[0]), &(v3[0]), TextureMarginMap::zscan_store_pixel);
}
static void zscan_store_pixel(
void *map, int x, int y, [[maybe_unused]] float u, [[maybe_unused]] float v)
{
/* NOTE: Not thread safe, see comment above. */
TextureMarginMap *m = static_cast<TextureMarginMap *>(map);
if (m->mask_) {
if (m->write_mask_) {
/* if there is a mask and write_mask_ is true, write to the mask */
m->mask_[y * m->w_ + x] = 1;
m->set_pixel(x, y, m->value_to_store_);
}
else {
/* if there is a mask and write_mask_ is false, read the mask
* to decide if the map needs to be written
*/
if (m->mask_[y * m->w_ + x] != 0) {
m->set_pixel(x, y, m->value_to_store_);
}
}
}
else {
m->set_pixel(x, y, m->value_to_store_);
}
}
/* The map contains 2 kinds of pixels: DijkstraPixels and face indices. The top bit determines
* what kind it is. With the top bit set, it is a 'dijkstra' pixel. The bottom 4 bits encode the
* direction of the shortest path and the remaining 27 bits are used to store the distance. If
* the top bit is not set, the rest of the bits is used to store the face index.
*/
#define PackDijkstraPixel(dist, dir) (0x80000000 + ((dist) << 4) + (dir))
#define DijkstraPixelGetDistance(dp) (((dp) ^ 0x80000000) >> 4)
#define DijkstraPixelGetDirection(dp) ((dp) & 0xF)
#define IsDijkstraPixel(dp) ((dp) & 0x80000000)
#define DijkstraPixelIsUnset(dp) ((dp) == 0xFFFFFFFF)
/**
* Use dijkstra's algorithm to 'grow' a border around the polygons marked in the map.
* For each pixel mark which direction is the shortest way to a face.
*/
void grow_dijkstra(int margin)
{
class DijkstraActivePixel {
public:
DijkstraActivePixel(int dist, int _x, int _y) : distance(dist), x(_x), y(_y) {}
int distance;
int x, y;
};
auto cmp_dijkstrapixel_fun = [](DijkstraActivePixel const &a1, DijkstraActivePixel const &a2) {
return a1.distance > a2.distance;
};
Vector<DijkstraActivePixel> active_pixels;
for (int y = 0; y < h_; y++) {
for (int x = 0; x < w_; x++) {
if (DijkstraPixelIsUnset(get_pixel(x, y))) {
for (int i = 0; i < 8; i++) {
int xx = x - directions[i][0];
int yy = y - directions[i][1];
if (xx >= 0 && xx < w_ && yy >= 0 && yy < w_ && !IsDijkstraPixel(get_pixel(xx, yy))) {
set_pixel(x, y, PackDijkstraPixel(distances[i], i));
active_pixels.append(DijkstraActivePixel(distances[i], x, y));
break;
}
}
}
}
}
/* Not strictly needed because at this point it already is a heap. */
#if 0
std::make_heap(active_pixels.begin(), active_pixels.end(), cmp_dijkstrapixel_fun);
#endif
while (active_pixels.size()) {
std::pop_heap(active_pixels.begin(), active_pixels.end(), cmp_dijkstrapixel_fun);
DijkstraActivePixel p = active_pixels.pop_last();
int dist = p.distance;
if (dist < 2 * (margin + 1)) {
for (int i = 0; i < 8; i++) {
int x = p.x + directions[i][0];
int y = p.y + directions[i][1];
if (x >= 0 && x < w_ && y >= 0 && y < h_) {
uint32_t dp = get_pixel(x, y);
if (IsDijkstraPixel(dp) && (DijkstraPixelGetDistance(dp) > dist + distances[i])) {
BLI_assert(DijkstraPixelGetDirection(dp) != i);
set_pixel(x, y, PackDijkstraPixel(dist + distances[i], i));
active_pixels.append(DijkstraActivePixel(dist + distances[i], x, y));
std::push_heap(active_pixels.begin(), active_pixels.end(), cmp_dijkstrapixel_fun);
}
}
}
}
}
}
/**
* Walk over the map and for margin pixels follow the direction stored in the bottom 3
* bits back to the face.
* Then look up the pixel from the next face.
*/
void lookup_pixels(ImBuf *ibuf, char *mask, int maxPolygonSteps)
{
float4 *ibuf_ptr_fl = reinterpret_cast<float4 *>(ibuf->float_data_for_write());
uchar4 *ibuf_ptr_ch = reinterpret_cast<uchar4 *>(ibuf->byte_data_for_write());
size_t pixel_index = 0;
for (int y = 0; y < h_; y++) {
for (int x = 0; x < w_; x++) {
uint32_t dp = pixel_data_[pixel_index];
if (IsDijkstraPixel(dp) && !DijkstraPixelIsUnset(dp)) {
int dist = DijkstraPixelGetDistance(dp);
int direction = DijkstraPixelGetDirection(dp);
int xx = x;
int yy = y;
/* Follow the dijkstra directions to find the face this margin pixels belongs to. */
while (dist > 0) {
xx -= directions[direction][0];
yy -= directions[direction][1];
dp = get_pixel(xx, yy);
dist -= distances[direction];
BLI_assert(!dist || (dist == DijkstraPixelGetDistance(dp)));
direction = DijkstraPixelGetDirection(dp);
}
uint32_t face = get_pixel(xx, yy);
BLI_assert(!IsDijkstraPixel(face));
float destX, destY;
int other_poly;
bool found_pixel_in_polygon = false;
if (lookup_pixel_polygon_neighborhood(x, y, &face, &destX, &destY, &other_poly)) {
for (int i = 0; i < maxPolygonSteps; i++) {
/* Force to pixel grid. */
int nx = int(round(destX));
int ny = int(round(destY));
uint32_t polygon_from_map = get_pixel(nx, ny);
if (other_poly == polygon_from_map) {
found_pixel_in_polygon = true;
break;
}
float dist_to_edge;
/* Look up again, but starting from the face we were expected to land in. */
if (!lookup_pixel(nx, ny, other_poly, &destX, &destY, &other_poly, &dist_to_edge)) {
found_pixel_in_polygon = false;
break;
}
}
if (found_pixel_in_polygon) {
if (ibuf_ptr_fl) {
ibuf_ptr_fl[pixel_index] = imbuf::interpolate_bilinear_border_fl(
ibuf, destX, destY);
}
if (ibuf_ptr_ch) {
ibuf_ptr_ch[pixel_index] = imbuf::interpolate_bilinear_border_byte(
ibuf, destX, destY);
}
/* Add our new pixels to the assigned pixel map. */
mask[pixel_index] = 1;
}
}
}
else if (DijkstraPixelIsUnset(dp) || !IsDijkstraPixel(dp)) {
/* These are not margin pixels, make sure the extend filter which is run after this step
* leaves them alone.
*/
mask[pixel_index] = 1;
}
pixel_index++;
}
}
}
private:
float2 uv_to_xy(const float2 &uv_map) const
{
float2 ret;
ret.x = (((uv_map[0] - uv_offset_[0]) * w_) - (0.5f + 0.001f));
ret.y = (((uv_map[1] - uv_offset_[1]) * h_) - (0.5f + 0.001f));
return ret;
}
void build_tables()
{
loop_to_face_map_ = bke::mesh::build_corner_to_face_map(faces_);
loop_adjacency_map_.resize(corner_edges_.size(), -1);
Vector<int> tmpmap;
tmpmap.resize(totedge_, -1);
for (const int64_t i : corner_edges_.index_range()) {
int edge = corner_edges_[i];
if (tmpmap[edge] == -1) {
loop_adjacency_map_[i] = -1;
tmpmap[edge] = i;
}
else {
BLI_assert(tmpmap[edge] >= 0);
loop_adjacency_map_[i] = tmpmap[edge];
loop_adjacency_map_[tmpmap[edge]] = i;
}
}
}
/**
* Call lookup_pixel for the start_poly. If that fails, try the adjacent polygons as well.
* Because the Dijkstra is not very exact in determining which face is the closest, the
* face we need can be the one next to the one the Dijkstra map provides. To prevent missing
* pixels also check the neighboring polygons.
*/
bool lookup_pixel_polygon_neighborhood(
float x, float y, uint32_t *r_start_poly, float *r_destx, float *r_desty, int *r_other_poly)
{
float found_dist;
if (lookup_pixel(x, y, *r_start_poly, r_destx, r_desty, r_other_poly, &found_dist)) {
return true;
}
int loopstart = faces_[*r_start_poly].start();
int totloop = faces_[*r_start_poly].size();
float destx, desty;
int foundpoly;
float mindist = -1.0f;
/* Loop over all adjacent polygons and determine which edge is closest.
* This could be optimized by only inspecting neighbors which are on the edge of an island.
* But it seems fast enough for now and that would add a lot of complexity. */
for (int i = 0; i < totloop; i++) {
int otherloop = loop_adjacency_map_[i + loopstart];
if (otherloop < 0) {
continue;
}
uint32_t face = loop_to_face_map_[otherloop];
if (lookup_pixel(x, y, face, &destx, &desty, &foundpoly, &found_dist)) {
if (mindist < 0.0f || found_dist < mindist) {
mindist = found_dist;
*r_other_poly = foundpoly;
*r_destx = destx;
*r_desty = desty;
*r_start_poly = face;
}
}
}
return mindist >= 0.0f;
}
/**
* Find which edge of the src_poly is closest to x,y. Look up its adjacent UV-edge and face.
* Then return the location of the equivalent pixel in the other face.
* Returns true if a new pixel location was found, false if it wasn't, which can happen if the
* margin pixel is on a corner, or the UV-edge doesn't have an adjacent face.
*/
bool lookup_pixel(float x,
float y,
int src_poly,
float *r_destx,
float *r_desty,
int *r_other_poly,
float *r_dist_to_edge)
{
float2 point(x, y);
*r_destx = *r_desty = 0;
int found_edge = -1;
float found_dist = -1;
float found_t = 0;
/* Find the closest edge on which the point x,y can be projected.
*/
for (size_t i = 0; i < faces_[src_poly].size(); i++) {
int l1 = faces_[src_poly].start() + i;
int l2 = l1 + 1;
if (l2 >= faces_[src_poly].start() + faces_[src_poly].size()) {
l2 = faces_[src_poly].start();
}
/* edge points */
float2 edgepoint1 = uv_to_xy(uv_map_[l1]);
float2 edgepoint2 = uv_to_xy(uv_map_[l2]);
/* Vector AB is the vector from the first edge point to the second edge point.
* Vector AP is the vector from the first edge point to our point under investigation. */
float2 ab = edgepoint2 - edgepoint1;
float2 ap = point - edgepoint1;
/* Project ap onto ab. */
float dotv = math::dot(ab, ap);
float ablensq = math::length_squared(ab);
float t = dotv / ablensq;
if (t >= 0.0 && t <= 1.0) {
/* Find the point on the edge closest to P */
float2 reflect_point = edgepoint1 + (t * ab);
/* This is the vector to P, so 90 degrees out from the edge. */
float2 reflect_vec = reflect_point - point;
float reflectLen = sqrt(reflect_vec[0] * reflect_vec[0] + reflect_vec[1] * reflect_vec[1]);
float cross = ab[0] * reflect_vec[1] - ab[1] * reflect_vec[0];
/* Only if P is on the outside of the edge, which means the cross product is positive,
* we consider this edge.
*/
bool valid = (cross > 0.0);
if (valid && (found_dist < 0 || reflectLen < found_dist)) {
/* Stother_ab the info of the closest edge so far. */
found_dist = reflectLen;
found_t = t;
found_edge = i + faces_[src_poly].start();
}
}
}
if (found_edge < 0) {
return false;
}
*r_dist_to_edge = found_dist;
/* Get the 'other' edge. I.E. the UV edge from the neighbor face. */
int other_edge = loop_adjacency_map_[found_edge];
if (other_edge < 0) {
return false;
}
int dst_poly = loop_to_face_map_[other_edge];
if (r_other_poly) {
*r_other_poly = dst_poly;
}
int other_edge2 = other_edge + 1;
if (other_edge2 >= faces_[dst_poly].start() + faces_[dst_poly].size()) {
other_edge2 = faces_[dst_poly].start();
}
float2 other_edgepoint1 = uv_to_xy(uv_map_[other_edge]);
float2 other_edgepoint2 = uv_to_xy(uv_map_[other_edge2]);
/* Calculate the vector from the order edges last point to its first point. */
float2 other_ab = other_edgepoint1 - other_edgepoint2;
float2 other_reflect_point = other_edgepoint2 + (found_t * other_ab);
float2 perpendicular_other_ab;
perpendicular_other_ab.x = other_ab.y;
perpendicular_other_ab.y = -other_ab.x;
/* The new point is dound_dist distance from other_reflect_point at a 90 degree angle to
* other_ab */
float2 new_point = other_reflect_point + (found_dist / math::length(perpendicular_other_ab)) *
perpendicular_other_ab;
*r_destx = new_point.x;
*r_desty = new_point.y;
return true;
}
}; // class TextureMarginMap
const int TextureMarginMap::directions[8][2] = {
{-1, 0}, {-1, -1}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}};
const int TextureMarginMap::distances[8] = {2, 3, 2, 3, 2, 3, 2, 3};
static void generate_margin(ImBuf *ibuf,
char *mask,
const int margin,
const Span<float3> vert_positions,
const int edges_num,
const OffsetIndices<int> faces,
const Span<int> corner_edges,
const Span<int> corner_verts,
const Span<float2> uv_map,
const float uv_offset[2])
{
Array<int3> corner_tris(poly_to_tri_count(faces.size(), corner_edges.size()));
bke::mesh::corner_tris_calc(vert_positions, faces, corner_verts, corner_tris);
Array<int> tri_faces(corner_tris.size());
bke::mesh::corner_tris_calc_face_indices(faces, tri_faces);
TextureMarginMap map(ibuf->x, ibuf->y, uv_offset, edges_num, faces, corner_edges, uv_map);
bool draw_new_mask = false;
/* Now the map contains 3 sorts of values: 0xFFFFFFFF for empty pixels, `0x80000000 + polyindex`
* for margin pixels, just `polyindex` for face pixels. */
if (mask) {
mask = MEM_dupalloc(mask);
}
else {
mask = MEM_new_array_zeroed<char>(size_t(ibuf->x) * size_t(ibuf->y), __func__);
draw_new_mask = true;
}
for (const int i : corner_tris.index_range()) {
const int3 tri = corner_tris[i];
float vec[3][2];
for (int a = 0; a < 3; a++) {
const float *uv = uv_map[tri[a]];
/* NOTE(@ideasman42): workaround for pixel aligned UVs which are common and can screw up
* our intersection tests where a pixel gets in between 2 faces or the middle of a quad,
* camera aligned quads also have this problem but they are less common.
* Add a small offset to the UVs, fixes bug #18685. */
vec[a][0] = (uv[0] - uv_offset[0]) * float(ibuf->x) - (0.5f + 0.001f);
vec[a][1] = (uv[1] - uv_offset[1]) * float(ibuf->y) - (0.5f + 0.002f);
}
/* NOTE: we need the top bit for the dijkstra distance map. */
BLI_assert(tri_faces[i] < 0x80000000);
map.rasterize_tri(vec[0], vec[1], vec[2], tri_faces[i], mask, draw_new_mask);
}
char *tmpmask = MEM_dupalloc(mask);
/* Extend (with averaging) by 2 pixels. Those will be overwritten, but it
* helps linear interpolations on the edges of polygons. */
IMB_filter_extend(ibuf, tmpmask, 2);
MEM_delete(tmpmask);
map.grow_dijkstra(margin);
/* Looking further than 3 polygons away leads to so much cumulative rounding
* that it isn't worth it. So hard-code it to 3. */
map.lookup_pixels(ibuf, mask, 3);
/* Use the extend filter to fill in the missing pixels at the corners, not strictly correct, but
* the visual difference seems very minimal. This also catches pixels we missed because of very
* narrow polygons.
*/
IMB_filter_extend(ibuf, mask, margin);
MEM_delete(mask);
}
} // namespace render::texturemargin
void RE_generate_texturemargin_adjacentfaces(ImBuf *ibuf,
char *mask,
const int margin,
const Mesh *mesh,
StringRef uv_layer,
const float uv_offset[2])
{
const StringRef name = uv_layer.is_empty() ? mesh->active_uv_map_name() : uv_layer;
const bke::AttributeAccessor attributes = mesh->attributes();
const VArraySpan<float2> uv_map = *attributes.lookup<float2>(name, bke::AttrDomain::Corner);
render::texturemargin::generate_margin(ibuf,
mask,
margin,
mesh->vert_positions(),
mesh->edges_num,
mesh->faces(),
mesh->corner_edges(),
mesh->corner_verts(),
uv_map,
uv_offset);
}
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#include "tile_highlight.h"
#include "BLI_hash.hh"
#include "BLI_rect.h"
#include "RE_pipeline.h"
namespace blender::render {
TilesHighlight::Tile::Tile(const RenderResult *result) : rect(result->tilerect) {}
TilesHighlight::Tile::Tile(const int x, const int y, const int width, const int height)
{
BLI_rcti_init(&rect, x, x + width, y, y + height);
}
uint64_t TilesHighlight::Tile::hash() const
{
return get_default_hash(rect.xmin, rect.xmax, rect.ymin, rect.ymax);
}
void TilesHighlight::highlight_tile_for_result(const RenderResult *result)
{
const Tile tile(result);
highlight_tile(tile);
}
void TilesHighlight::unhighlight_tile_for_result(const RenderResult *result)
{
const Tile tile(result);
unhighlight_tile(tile);
}
void TilesHighlight::highlight_tile(const int x, const int y, const int width, const int height)
{
const Tile tile(x, y, width, height);
highlight_tile(tile);
}
void TilesHighlight::unhighlight_tile(const int x, const int y, const int width, const int height)
{
const Tile tile(x, y, width, height);
unhighlight_tile(tile);
}
void TilesHighlight::highlight_tile(const Tile &tile)
{
std::unique_lock lock(mutex_);
highlighted_tiles_set_.add(tile);
did_tiles_change_ = true;
}
void TilesHighlight::unhighlight_tile(const Tile &tile)
{
std::unique_lock lock(mutex_);
highlighted_tiles_set_.remove(tile);
did_tiles_change_ = true;
}
void TilesHighlight::clear()
{
std::unique_lock lock(mutex_);
highlighted_tiles_set_.clear();
cached_highlighted_tiles_.clear_and_shrink();
}
Span<rcti> TilesHighlight::get_all_highlighted_tiles() const
{
std::unique_lock lock(mutex_);
/* Updated cached flat list if needed. */
if (did_tiles_change_) {
if (highlighted_tiles_set_.is_empty()) {
cached_highlighted_tiles_.clear_and_shrink();
}
else {
cached_highlighted_tiles_.reserve(highlighted_tiles_set_.size());
for (const Tile &tile : highlighted_tiles_set_) {
cached_highlighted_tiles_.append(tile.rect);
}
}
did_tiles_change_ = false;
}
return cached_highlighted_tiles_;
}
} // namespace blender::render

View File

@@ -0,0 +1,70 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
#include "DNA_vec_types.h"
#include "BLI_mutex.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
namespace blender {
struct RenderResult;
namespace render {
class TilesHighlight {
public:
TilesHighlight() = default;
~TilesHighlight() = default;
void highlight_tile_for_result(const RenderResult *result);
void unhighlight_tile_for_result(const RenderResult *result);
void highlight_tile(int x, int y, int width, int height);
void unhighlight_tile(int x, int y, int width, int height);
void clear();
Span<rcti> get_all_highlighted_tiles() const;
private:
struct Tile {
Tile() = default;
explicit Tile(const RenderResult *result);
explicit Tile(int x, int y, int width, int height);
uint64_t hash() const;
bool operator==(const Tile &other) const
{
return rect == other.rect;
}
bool operator!=(const Tile &other) const
{
return !(*this == other);
}
rcti rect = {0, 0, 0, 0};
};
void highlight_tile(const Tile &tile);
void unhighlight_tile(const Tile &tile);
mutable Mutex mutex_;
Set<Tile> highlighted_tiles_set_;
/* Cached flat list of currently highlighted tiles for a fast access via API. */
mutable bool did_tiles_change_ = false;
mutable Vector<rcti> cached_highlighted_tiles_;
};
} // namespace render
} // namespace blender

View File

@@ -0,0 +1,243 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*
* \note Some of this logic has been duplicated in `COM_VectorBlurOperation.cc`
* changes here may also apply also apply to that file.
*/
/*---------------------------------------------------------------------------*/
/* Common includes */
/*---------------------------------------------------------------------------*/
#include <algorithm>
#include <cstring>
#include "MEM_guardedalloc.h"
#include "BLI_math_base.h"
/* own includes */
#include "zbuf.h"
namespace blender {
/* could enable at some point but for now there are far too many conversions */
#ifdef __GNUC__
# pragma GCC diagnostic ignored "-Wdouble-promotion"
#endif
/* ****************** Spans ******************************* */
void zbuf_alloc_span(ZSpan *zspan, int rectx, int recty)
{
memset(zspan, 0, sizeof(ZSpan));
zspan->rectx = rectx;
zspan->recty = recty;
zspan->span1 = MEM_new_array_uninitialized<float>(recty, "zspan");
zspan->span2 = MEM_new_array_uninitialized<float>(recty, "zspan");
}
void zbuf_free_span(ZSpan *zspan)
{
if (zspan) {
MEM_SAFE_DELETE(zspan->span1);
MEM_SAFE_DELETE(zspan->span2);
}
}
/* reset range for clipping */
static void zbuf_init_span(ZSpan *zspan)
{
zspan->miny1 = zspan->miny2 = zspan->recty + 1;
zspan->maxy1 = zspan->maxy2 = -1;
zspan->minp1 = zspan->maxp1 = zspan->minp2 = zspan->maxp2 = nullptr;
}
static void zbuf_add_to_span(ZSpan *zspan, const float v1[2], const float v2[2])
{
const float *minv, *maxv;
float *span;
float xx1, dx0, xs0;
int y, my0, my2;
if (v1[1] < v2[1]) {
minv = v1;
maxv = v2;
}
else {
minv = v2;
maxv = v1;
}
my0 = ceil(minv[1]);
my2 = floor(maxv[1]);
if (my2 < 0 || my0 >= zspan->recty) {
return;
}
/* clip top */
if (my2 >= zspan->recty) {
my2 = zspan->recty - 1;
}
/* clip bottom */
my0 = std::max(my0, 0);
if (my0 > my2) {
return;
}
/* if (my0>my2) should still fill in, that way we get spans that skip nicely */
xx1 = maxv[1] - minv[1];
if (xx1 > FLT_EPSILON) {
dx0 = (minv[0] - maxv[0]) / xx1;
xs0 = dx0 * (minv[1] - my2) + minv[0];
}
else {
dx0 = 0.0f;
xs0 = min_ff(minv[0], maxv[0]);
}
/* empty span */
if (zspan->maxp1 == nullptr) {
span = zspan->span1;
}
else { /* does it complete left span? */
if (maxv == zspan->minp1 || minv == zspan->maxp1) {
span = zspan->span1;
}
else {
span = zspan->span2;
}
}
if (span == zspan->span1) {
// printf("left span my0 %d my2 %d\n", my0, my2);
if (zspan->minp1 == nullptr || zspan->minp1[1] > minv[1]) {
zspan->minp1 = minv;
}
if (zspan->maxp1 == nullptr || zspan->maxp1[1] < maxv[1]) {
zspan->maxp1 = maxv;
}
zspan->miny1 = std::min(my0, zspan->miny1);
zspan->maxy1 = std::max(my2, zspan->maxy1);
}
else {
// printf("right span my0 %d my2 %d\n", my0, my2);
if (zspan->minp2 == nullptr || zspan->minp2[1] > minv[1]) {
zspan->minp2 = minv;
}
if (zspan->maxp2 == nullptr || zspan->maxp2[1] < maxv[1]) {
zspan->maxp2 = maxv;
}
zspan->miny2 = std::min(my0, zspan->miny2);
zspan->maxy2 = std::max(my2, zspan->maxy2);
}
for (y = my2; y >= my0; y--, xs0 += dx0) {
/* xs0 is the X-coordinate! */
span[y] = xs0;
}
}
/*-----------------------------------------------------------*/
/* Functions */
/*-----------------------------------------------------------*/
void zspan_scanconvert(ZSpan *zspan,
void *handle,
float *v1,
float *v2,
float *v3,
void (*func)(void *, int, int, float, float))
{
float x0, y0, x1, y1, x2, y2, z0, z1, z2;
float u, v, uxd, uyd, vxd, vyd, uy0, vy0, xx1;
const float *span1, *span2;
int i, j, x, y, sn1, sn2, rectx = zspan->rectx, my0, my2;
/* init */
zbuf_init_span(zspan);
/* set spans */
zbuf_add_to_span(zspan, v1, v2);
zbuf_add_to_span(zspan, v2, v3);
zbuf_add_to_span(zspan, v3, v1);
/* clipped */
if (zspan->minp2 == nullptr || zspan->maxp2 == nullptr) {
return;
}
my0 = max_ii(zspan->miny1, zspan->miny2);
my2 = min_ii(zspan->maxy1, zspan->maxy2);
// printf("my %d %d\n", my0, my2);
if (my2 < my0) {
return;
}
/* ZBUF DX DY, in floats still */
x1 = v1[0] - v2[0];
x2 = v2[0] - v3[0];
y1 = v1[1] - v2[1];
y2 = v2[1] - v3[1];
z1 = 1.0f; /* (u1 - u2) */
z2 = 0.0f; /* (u2 - u3) */
x0 = y1 * z2 - z1 * y2;
y0 = z1 * x2 - x1 * z2;
z0 = x1 * y2 - y1 * x2;
if (z0 == 0.0f) {
return;
}
xx1 = (x0 * v1[0] + y0 * v1[1]) / z0 + 1.0f;
uxd = -double(x0) / double(z0);
uyd = -double(y0) / double(z0);
uy0 = double(my2) * uyd + double(xx1);
z1 = -1.0f; /* (v1 - v2) */
z2 = 1.0f; /* (v2 - v3) */
x0 = y1 * z2 - z1 * y2;
y0 = z1 * x2 - x1 * z2;
xx1 = (x0 * v1[0] + y0 * v1[1]) / z0;
vxd = -double(x0) / double(z0);
vyd = -double(y0) / double(z0);
vy0 = double(my2) * vyd + double(xx1);
/* correct span */
span1 = zspan->span1 + my2;
span2 = zspan->span2 + my2;
for (i = 0, y = my2; y >= my0; i++, y--, span1--, span2--) {
sn1 = floor(min_ff(*span1, *span2));
sn2 = floor(max_ff(*span1, *span2));
sn1++;
if (sn2 >= rectx) {
sn2 = rectx - 1;
}
sn1 = std::max(sn1, 0);
u = ((double(sn1) * uxd) + uy0) - (i * uyd);
v = ((double(sn1) * vxd) + vy0) - (i * vyd);
for (j = 0, x = sn1; x <= sn2; j++, x++) {
func(handle, x, y, u + (j * uxd), v + (j * vxd));
}
}
}
} // namespace blender

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup render
*/
#pragma once
namespace blender {
/** Span fill in method, is also used to localize data for Z-buffering. */
struct ZSpan {
int rectx, recty; /* range for clipping */
int miny1, maxy1, miny2, maxy2; /* actual filled in range */
const float *minp1, *maxp1, *minp2, *maxp2; /* vertex pointers detect min/max range in */
float *span1, *span2;
};
/**
* Each Z-buffer has coordinates transformed to local rect coordinates, so we can simply clip.
*/
void zbuf_alloc_span(struct ZSpan *zspan, int rectx, int recty);
void zbuf_free_span(struct ZSpan *zspan);
/**
* Scan-convert for strand triangles, calls function for each x, y coordinate
* and gives UV barycentrics and z.
*/
void zspan_scanconvert(struct ZSpan *zspan,
void *handle,
float *v1,
float *v2,
float *v3,
void (*func)(void *, int, int, float, float));
} // namespace blender