Add Chromium-only Blender WebEngine parity work
This commit is contained in:
664
blender-5.2.0/intern/cycles/kernel/camera/camera.h
Normal file
664
blender-5.2.0/intern/cycles/kernel/camera/camera.h
Normal file
@@ -0,0 +1,664 @@
|
||||
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/globals.h"
|
||||
|
||||
#include "kernel/camera/projection.h"
|
||||
#include "kernel/sample/mapping.h"
|
||||
#include "kernel/util/differential.h"
|
||||
#include "kernel/util/lookup_table.h"
|
||||
|
||||
#ifdef WITH_OSL
|
||||
# include "kernel/osl/camera.h"
|
||||
#endif
|
||||
|
||||
CCL_NAMESPACE_BEGIN
|
||||
|
||||
/* Perspective Camera */
|
||||
|
||||
ccl_device float2 camera_sample_aperture(ccl_constant KernelCamera *cam, const float2 rand)
|
||||
{
|
||||
const float blades = cam->blades;
|
||||
float2 bokeh;
|
||||
|
||||
if (blades == 0.0f) {
|
||||
/* sample disk */
|
||||
bokeh = sample_uniform_disk(rand);
|
||||
}
|
||||
else {
|
||||
/* sample polygon */
|
||||
const float rotation = cam->bladesrotation;
|
||||
bokeh = regular_polygon_sample(blades, rotation, rand);
|
||||
}
|
||||
|
||||
/* anamorphic lens bokeh */
|
||||
bokeh.x *= cam->inv_aperture_ratio;
|
||||
|
||||
return bokeh;
|
||||
}
|
||||
|
||||
ccl_device Spectrum camera_sample_perspective(KernelGlobals kg,
|
||||
const float2 raster_xy,
|
||||
const float2 rand_lens,
|
||||
ccl_private Ray *ray)
|
||||
{
|
||||
/* create ray form raster position */
|
||||
const ProjectionTransform rastertocamera = kernel_data.cam.rastertocamera;
|
||||
const float3 raster = make_float3(raster_xy);
|
||||
float3 Pcamera = transform_perspective(&rastertocamera, raster);
|
||||
|
||||
if (kernel_data.cam.have_perspective_motion) {
|
||||
/* TODO(sergey): Currently we interpolate projected coordinate which
|
||||
* gives nice looking result and which is simple, but is in fact a bit
|
||||
* different comparing to constructing projective matrix from an
|
||||
* interpolated field of view.
|
||||
*/
|
||||
if (ray->time < 0.5f) {
|
||||
const ProjectionTransform rastertocamera_pre = kernel_data.cam.perspective_pre;
|
||||
const float3 Pcamera_pre = transform_perspective(&rastertocamera_pre, raster);
|
||||
Pcamera = interp(Pcamera_pre, Pcamera, ray->time * 2.0f);
|
||||
}
|
||||
else {
|
||||
const ProjectionTransform rastertocamera_post = kernel_data.cam.perspective_post;
|
||||
const float3 Pcamera_post = transform_perspective(&rastertocamera_post, raster);
|
||||
Pcamera = interp(Pcamera, Pcamera_post, (ray->time - 0.5f) * 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
float3 P = zero_float3();
|
||||
float3 D = Pcamera;
|
||||
|
||||
/* modify ray for depth of field */
|
||||
const float aperturesize = kernel_data.cam.aperturesize;
|
||||
|
||||
if (aperturesize > 0.0f) {
|
||||
/* sample point on aperture */
|
||||
const float2 lens_uv = camera_sample_aperture(&kernel_data.cam, rand_lens) * aperturesize;
|
||||
|
||||
/* compute point on plane of focus */
|
||||
const float ft = kernel_data.cam.focaldistance / D.z;
|
||||
const float3 Pfocus = D * ft;
|
||||
|
||||
/* update ray for effect of lens */
|
||||
P = make_float3(lens_uv);
|
||||
D = normalize(Pfocus - P);
|
||||
}
|
||||
|
||||
/* transform ray from camera to world */
|
||||
Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
|
||||
if (kernel_data.cam.num_motion_steps) {
|
||||
transform_motion_array_interpolate(&cameratoworld,
|
||||
kernel_data_array(camera_motion),
|
||||
kernel_data.cam.num_motion_steps,
|
||||
ray->time);
|
||||
}
|
||||
|
||||
P = transform_point(&cameratoworld, P);
|
||||
D = normalize(transform_direction(&cameratoworld, D));
|
||||
|
||||
const bool use_stereo = kernel_data.cam.interocular_offset != 0.0f;
|
||||
if (!use_stereo) {
|
||||
/* No stereo */
|
||||
ray->P = P;
|
||||
ray->D = D;
|
||||
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
const float3 Dcenter = transform_direction(&cameratoworld, Pcamera);
|
||||
const float3 Dcenter_normalized = normalize(Dcenter);
|
||||
|
||||
/* TODO: can this be optimized to give compact differentials directly? */
|
||||
ray->dP = differential_zero_compact();
|
||||
differential3 dD;
|
||||
dD.dx = normalize(Dcenter + make_float3(kernel_data.cam.dx)) - Dcenter_normalized;
|
||||
dD.dy = normalize(Dcenter + make_float3(kernel_data.cam.dy)) - Dcenter_normalized;
|
||||
ray->dD = differential_make_compact(dD) * kernel_data.cam.differential_scale;
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
/* Spherical stereo */
|
||||
spherical_stereo_transform(&kernel_data.cam, &P, &D);
|
||||
ray->P = P;
|
||||
ray->D = D;
|
||||
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
/* Ray differentials, computed from scratch using the raster coordinates
|
||||
* because we don't want to be affected by depth of field. We compute
|
||||
* ray origin and direction for the center and two neighboring pixels
|
||||
* and simply take their differences. */
|
||||
const float3 Pnostereo = transform_point(&cameratoworld, zero_float3());
|
||||
|
||||
float3 Pcenter = Pnostereo;
|
||||
float3 Dcenter = Pcamera;
|
||||
Dcenter = normalize(transform_direction(&cameratoworld, Dcenter));
|
||||
spherical_stereo_transform(&kernel_data.cam, &Pcenter, &Dcenter);
|
||||
|
||||
float3 Px = Pnostereo;
|
||||
float3 Dx = transform_perspective(&rastertocamera,
|
||||
make_float3(raster.x + 1.0f, raster.y, 0.0f));
|
||||
Dx = normalize(transform_direction(&cameratoworld, Dx));
|
||||
spherical_stereo_transform(&kernel_data.cam, &Px, &Dx);
|
||||
|
||||
differential3 dP;
|
||||
differential3 dD;
|
||||
|
||||
dP.dx = Px - Pcenter;
|
||||
dD.dx = Dx - Dcenter;
|
||||
|
||||
float3 Py = Pnostereo;
|
||||
float3 Dy = transform_perspective(&rastertocamera,
|
||||
make_float3(raster.x, raster.y + 1.0f, 0.0f));
|
||||
Dy = normalize(transform_direction(&cameratoworld, Dy));
|
||||
spherical_stereo_transform(&kernel_data.cam, &Py, &Dy);
|
||||
|
||||
dP.dy = Py - Pcenter;
|
||||
dD.dy = Dy - Dcenter;
|
||||
ray->dD = differential_make_compact(dD) * kernel_data.cam.differential_scale;
|
||||
ray->dP = differential_make_compact(dP) * kernel_data.cam.differential_scale;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* clipping */
|
||||
const float z_inv = 1.0f / normalize(Pcamera).z;
|
||||
const float nearclip = kernel_data.cam.nearclip * z_inv;
|
||||
ray->P += nearclip * ray->D;
|
||||
ray->dP += nearclip * ray->dD;
|
||||
ray->tmin = 0.0f;
|
||||
ray->tmax = kernel_data.cam.cliplength * z_inv;
|
||||
|
||||
return one_spectrum();
|
||||
}
|
||||
|
||||
/* Orthographic Camera */
|
||||
ccl_device Spectrum camera_sample_orthographic(KernelGlobals kg,
|
||||
const float2 raster_xy,
|
||||
const float2 rand_lens,
|
||||
ccl_private Ray *ray)
|
||||
{
|
||||
/* create ray form raster position */
|
||||
const ProjectionTransform rastertocamera = kernel_data.cam.rastertocamera;
|
||||
const float3 Pcamera = transform_perspective(&rastertocamera, make_float3(raster_xy));
|
||||
|
||||
float3 P;
|
||||
float3 D = make_float3(0.0f, 0.0f, 1.0f);
|
||||
|
||||
/* modify ray for depth of field */
|
||||
const float aperturesize = kernel_data.cam.aperturesize;
|
||||
|
||||
if (aperturesize > 0.0f) {
|
||||
/* sample point on aperture */
|
||||
const float2 lens_uv = camera_sample_aperture(&kernel_data.cam, rand_lens) * aperturesize;
|
||||
|
||||
/* compute point on plane of focus */
|
||||
const float3 Pfocus = D * kernel_data.cam.focaldistance;
|
||||
|
||||
/* Update ray for effect of lens */
|
||||
const float3 lens_uvw = make_float3(lens_uv);
|
||||
|
||||
D = normalize(Pfocus - lens_uvw);
|
||||
/* Compute position the ray will be if it traveled until it intersected the near clip plane.
|
||||
* This allows for correct DOF while allowing near clipping. */
|
||||
P = Pcamera + lens_uvw + (D * (kernel_data.cam.nearclip / D.z));
|
||||
}
|
||||
else {
|
||||
P = Pcamera + make_float3(0.0f, 0.0f, kernel_data.cam.nearclip);
|
||||
}
|
||||
/* transform ray from camera to world */
|
||||
Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
|
||||
if (kernel_data.cam.num_motion_steps) {
|
||||
transform_motion_array_interpolate(&cameratoworld,
|
||||
kernel_data_array(camera_motion),
|
||||
kernel_data.cam.num_motion_steps,
|
||||
ray->time);
|
||||
}
|
||||
|
||||
ray->P = transform_point(&cameratoworld, P);
|
||||
ray->D = normalize(transform_direction(&cameratoworld, D));
|
||||
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
/* ray differential */
|
||||
differential3 dP;
|
||||
dP.dx = make_float3(kernel_data.cam.dx);
|
||||
dP.dy = make_float3(kernel_data.cam.dy);
|
||||
|
||||
ray->dP = differential_make_compact(dP) * kernel_data.cam.differential_scale;
|
||||
ray->dD = differential_zero_compact();
|
||||
#endif
|
||||
|
||||
/* clipping */
|
||||
ray->tmin = 0.0f;
|
||||
ray->tmax = kernel_data.cam.cliplength;
|
||||
|
||||
return one_spectrum();
|
||||
}
|
||||
|
||||
/* Custom Camera */
|
||||
|
||||
ccl_device_inline void camera_sample_to_ray(ccl_constant KernelCamera *cam,
|
||||
const ccl_global DecomposedTransform *cam_motion,
|
||||
float3 P,
|
||||
float3 D,
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
float3 Pcenter,
|
||||
float3 Dcenter,
|
||||
float3 Px,
|
||||
float3 Dx,
|
||||
float3 Py,
|
||||
float3 Dy,
|
||||
#endif
|
||||
ccl_private Ray *ray)
|
||||
{
|
||||
/* Transform the ray from camera to world. */
|
||||
Transform cameratoworld = cam->cameratoworld;
|
||||
|
||||
if (cam->num_motion_steps) {
|
||||
transform_motion_array_interpolate(
|
||||
&cameratoworld, cam_motion, cam->num_motion_steps, ray->time);
|
||||
}
|
||||
|
||||
/* Stereo transform */
|
||||
const bool use_stereo = cam->interocular_offset != 0.0f;
|
||||
if (use_stereo) {
|
||||
spherical_stereo_transform(cam, &P, &D);
|
||||
}
|
||||
|
||||
P = transform_point(&cameratoworld, P);
|
||||
D = normalize(transform_direction(&cameratoworld, D));
|
||||
|
||||
ray->P = P;
|
||||
ray->D = D;
|
||||
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
if (use_stereo) {
|
||||
spherical_stereo_transform(cam, &Pcenter, &Dcenter);
|
||||
spherical_stereo_transform(cam, &Px, &Dx);
|
||||
spherical_stereo_transform(cam, &Py, &Dy);
|
||||
|
||||
differential3 dP;
|
||||
Pcenter = transform_point(&cameratoworld, Pcenter);
|
||||
dP.dx = transform_point(&cameratoworld, Px) - Pcenter;
|
||||
dP.dy = transform_point(&cameratoworld, Py) - Pcenter;
|
||||
ray->dP = differential_make_compact(dP) * cam->differential_scale;
|
||||
}
|
||||
else {
|
||||
ray->dP = differential_zero_compact();
|
||||
}
|
||||
|
||||
differential3 dD;
|
||||
Dcenter = normalize(transform_direction(&cameratoworld, Dcenter));
|
||||
dD.dx = normalize(transform_direction(&cameratoworld, Dx)) - Dcenter;
|
||||
dD.dy = normalize(transform_direction(&cameratoworld, Dy)) - Dcenter;
|
||||
ray->dD = differential_make_compact(dD) * cam->differential_scale;
|
||||
#endif
|
||||
|
||||
/* clipping */
|
||||
const float nearclip = cam->nearclip;
|
||||
ray->P += nearclip * ray->D;
|
||||
ray->dP += nearclip * ray->dD;
|
||||
ray->tmin = 0.0f;
|
||||
ray->tmax = cam->cliplength;
|
||||
}
|
||||
|
||||
ccl_device_inline Spectrum camera_sample_custom(KernelGlobals kg,
|
||||
ccl_constant KernelCamera *cam,
|
||||
const ccl_global DecomposedTransform *cam_motion,
|
||||
const float2 raster,
|
||||
const float2 rand_lens,
|
||||
ccl_private Ray *ray,
|
||||
ccl_private int &r_cache_miss)
|
||||
{
|
||||
#ifdef WITH_OSL
|
||||
/* Transform raster position to camera space. */
|
||||
const ProjectionTransform rastertocamera = cam->rastertocamera;
|
||||
float3 sensor = transform_perspective(&rastertocamera, make_float3(raster.x, raster.y, 0.0f));
|
||||
float3 dSdx = transform_perspective_direction(&rastertocamera, make_float3(1.0f, 0.0f, 0.0f));
|
||||
float3 dSdy = transform_perspective_direction(&rastertocamera, make_float3(0.0f, 1.0f, 0.0f));
|
||||
|
||||
ShaderDataTinyStorage sd_storage = {};
|
||||
ccl_private ShaderData *sd = AS_SHADER_DATA(&sd_storage);
|
||||
sd->object = OBJECT_NONE;
|
||||
sd->prim = PRIM_NONE;
|
||||
sd->shader = SHADER_NONE;
|
||||
sd->type = PRIMITIVE_NONE;
|
||||
sd->flag = 0;
|
||||
|
||||
/* Execute OSL shader to sample position, direction and transmission. */
|
||||
packed_float3 P, dPdx, dPdy, D, dDdx, dDdy, throughput;
|
||||
r_cache_miss = false;
|
||||
throughput = osl_eval_camera(
|
||||
kg, sd, sensor, dSdx, dSdy, rand_lens, P, dPdx, dPdy, D, dDdx, dDdy);
|
||||
if (sd->flag & SD_CACHE_MISS) {
|
||||
r_cache_miss = true;
|
||||
return zero_spectrum();
|
||||
}
|
||||
/* Zero throughput indicates failed sampling. */
|
||||
if (is_zero(throughput)) {
|
||||
return zero_spectrum();
|
||||
}
|
||||
|
||||
camera_sample_to_ray(cam,
|
||||
cam_motion,
|
||||
P,
|
||||
D,
|
||||
# ifdef __RAY_DIFFERENTIALS__
|
||||
P,
|
||||
D,
|
||||
P + dPdx,
|
||||
D + dDdx,
|
||||
P + dPdy,
|
||||
D + dDdy,
|
||||
# endif
|
||||
ray);
|
||||
|
||||
return throughput;
|
||||
#else
|
||||
(void)kg;
|
||||
(void)cam;
|
||||
(void)cam_motion;
|
||||
(void)raster;
|
||||
(void)rand_lens;
|
||||
(void)ray;
|
||||
(void)r_cache_miss;
|
||||
return zero_spectrum();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Panorama Camera */
|
||||
|
||||
ccl_device_inline float3 camera_panorama_direction(ccl_constant KernelCamera *cam,
|
||||
const float x,
|
||||
const float y)
|
||||
{
|
||||
const ProjectionTransform rastertocamera = cam->rastertocamera;
|
||||
const float3 Pcamera = transform_perspective(&rastertocamera, make_float3(x, y, 0.0f));
|
||||
return panorama_to_direction(cam, Pcamera.x, Pcamera.y);
|
||||
}
|
||||
|
||||
ccl_device_inline Spectrum camera_sample_panorama(ccl_constant KernelCamera *cam,
|
||||
const ccl_global DecomposedTransform *cam_motion,
|
||||
const float2 raster,
|
||||
const float2 rand_lens,
|
||||
ccl_private Ray *ray)
|
||||
{
|
||||
/* Create ray from raster position. */
|
||||
float3 P = zero_float3();
|
||||
float3 D = camera_panorama_direction(cam, raster.x, raster.y);
|
||||
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
/* Ray differentials, computed from scratch using the raster coordinates
|
||||
* because we don't want to be affected by depth of field. We compute
|
||||
* ray origin and direction for the center and two neighboring pixels
|
||||
* and simply take their differences. */
|
||||
float3 Dcenter = D;
|
||||
float3 Dx = camera_panorama_direction(cam, raster.x + 1.0f, raster.y);
|
||||
float3 Dy = camera_panorama_direction(cam, raster.x, raster.y + 1.0f);
|
||||
#endif
|
||||
|
||||
/* Here, zero indicates failed sampling, e.g. when the raster position is outside
|
||||
* the fisheye lens. */
|
||||
if (is_zero(D)) {
|
||||
return zero_spectrum();
|
||||
}
|
||||
|
||||
/* Perform depth-of-field sampling. */
|
||||
const float aperturesize = cam->aperturesize;
|
||||
|
||||
if (aperturesize > 0.0f) {
|
||||
/* Sample a point on the aperture. */
|
||||
const float2 lens_uv = camera_sample_aperture(cam, rand_lens) * aperturesize;
|
||||
|
||||
/* Compute the intersection of the original ray with the focal plane. */
|
||||
const float3 Dfocus = normalize(D);
|
||||
const float3 Pfocus = Dfocus * cam->focaldistance;
|
||||
|
||||
/* Calculate orthonormal coordinate system perpendicular to Dfocus. */
|
||||
const float3 U = normalize(make_float3(1.0f, 0.0f, 0.0f) - Dfocus.x * Dfocus);
|
||||
const float3 V = normalize(cross(Dfocus, U));
|
||||
|
||||
/* Compute new ray by shifting its origin (to account for aperture position) and
|
||||
* setting its direction to meet the original ray at the focal plane. */
|
||||
P = U * lens_uv.x + V * lens_uv.y;
|
||||
D = normalize(Pfocus - P);
|
||||
}
|
||||
|
||||
camera_sample_to_ray(cam,
|
||||
cam_motion,
|
||||
P,
|
||||
D,
|
||||
#ifdef __RAY_DIFFERENTIALS__
|
||||
zero_float3(),
|
||||
Dcenter,
|
||||
zero_float3(),
|
||||
Dx,
|
||||
zero_float3(),
|
||||
Dy,
|
||||
#endif
|
||||
ray);
|
||||
|
||||
return one_spectrum();
|
||||
}
|
||||
|
||||
/* Common */
|
||||
|
||||
/* Generates an outgoing camera ray for the given raster position and random inputs.
|
||||
* Returns camera sensitivity (used to initialize path throughput). */
|
||||
ccl_device_inline Spectrum camera_sample(KernelGlobals kg,
|
||||
const int x,
|
||||
const int y,
|
||||
const float2 filter_uv,
|
||||
const float time,
|
||||
const float2 lens_uv,
|
||||
ccl_private Ray *ray,
|
||||
ccl_private int &r_cache_miss)
|
||||
{
|
||||
float2 raster = make_float2(x, y);
|
||||
|
||||
/* pixel filter */
|
||||
if (kernel_data.integrator.pixel_jitter.x == FLT_MAX) {
|
||||
const int filter_table_offset = kernel_data.tables.filter_table_offset;
|
||||
raster.x += lookup_table_read(kg, filter_uv.x, filter_table_offset, FILTER_TABLE_SIZE);
|
||||
raster.y += lookup_table_read(kg, filter_uv.y, filter_table_offset, FILTER_TABLE_SIZE);
|
||||
}
|
||||
else {
|
||||
raster += -kernel_data.integrator.pixel_jitter;
|
||||
}
|
||||
|
||||
/* motion blur */
|
||||
if (kernel_data.cam.shuttertime == -1.0f) {
|
||||
ray->time = 0.5f;
|
||||
}
|
||||
else {
|
||||
/* TODO(sergey): Such lookup is unneeded when there's rolling shutter
|
||||
* effect in use but rolling shutter duration is set to 0.0.
|
||||
*/
|
||||
const int shutter_table_offset = kernel_data.cam.shutter_table_offset;
|
||||
ray->time = lookup_table_read(kg, time, shutter_table_offset, SHUTTER_TABLE_SIZE);
|
||||
/* TODO(sergey): Currently single rolling shutter effect type only
|
||||
* where scan-lines are acquired from top to bottom and whole scan-line
|
||||
* is acquired at once (no delay in acquisition happens between pixels
|
||||
* of single scan-line).
|
||||
*
|
||||
* Might want to support more models in the future.
|
||||
*/
|
||||
if (kernel_data.cam.rolling_shutter_type) {
|
||||
/* Time corresponding to a fully rolling shutter only effect:
|
||||
* top of the frame is time 0.0, bottom of the frame is time 1.0.
|
||||
*/
|
||||
const float time = 1.0f - (float)y / kernel_data.cam.height;
|
||||
const float duration = kernel_data.cam.rolling_shutter_duration;
|
||||
if (duration != 0.0f) {
|
||||
/* This isn't fully physical correct, but lets us to have simple
|
||||
* controls in the interface. The idea here is basically sort of
|
||||
* linear interpolation between how much rolling shutter effect
|
||||
* exist on the frame and how much of it is a motion blur effect.
|
||||
*/
|
||||
ray->time = (ray->time - 0.5f) * duration;
|
||||
ray->time += (time - 0.5f) * (1.0f - duration) + 0.5f;
|
||||
}
|
||||
else {
|
||||
ray->time = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* sample */
|
||||
r_cache_miss = false;
|
||||
if (kernel_data.cam.type == CAMERA_PERSPECTIVE) {
|
||||
return camera_sample_perspective(kg, raster, lens_uv, ray);
|
||||
}
|
||||
if (kernel_data.cam.type == CAMERA_ORTHOGRAPHIC) {
|
||||
return camera_sample_orthographic(kg, raster, lens_uv, ray);
|
||||
}
|
||||
if (kernel_data.cam.type == CAMERA_PANORAMA) {
|
||||
const ccl_global DecomposedTransform *cam_motion = kernel_data_array(camera_motion);
|
||||
return camera_sample_panorama(&kernel_data.cam, cam_motion, raster, lens_uv, ray);
|
||||
}
|
||||
if (kernel_data.cam.type == CAMERA_CUSTOM) {
|
||||
const ccl_global DecomposedTransform *cam_motion = kernel_data_array(camera_motion);
|
||||
return camera_sample_custom(
|
||||
kg, &kernel_data.cam, cam_motion, raster, lens_uv, ray, r_cache_miss);
|
||||
}
|
||||
kernel_assert(false);
|
||||
return zero_spectrum();
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
|
||||
ccl_device_inline float3 camera_position(KernelGlobals kg)
|
||||
{
|
||||
const Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
return make_float3(cameratoworld.x.w, cameratoworld.y.w, cameratoworld.z.w);
|
||||
}
|
||||
|
||||
ccl_device_inline float camera_distance(KernelGlobals kg, const float3 P)
|
||||
{
|
||||
const Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
const float3 camP = make_float3(cameratoworld.x.w, cameratoworld.y.w, cameratoworld.z.w);
|
||||
|
||||
if (kernel_data.cam.type == CAMERA_ORTHOGRAPHIC) {
|
||||
const float3 camD = make_float3(cameratoworld.x.z, cameratoworld.y.z, cameratoworld.z.z);
|
||||
return fabsf(dot((P - camP), camD));
|
||||
}
|
||||
return len(P - camP);
|
||||
}
|
||||
|
||||
ccl_device_inline float camera_z_depth(KernelGlobals kg, const float3 P)
|
||||
{
|
||||
if (kernel_data.cam.type == CAMERA_PERSPECTIVE || kernel_data.cam.type == CAMERA_ORTHOGRAPHIC) {
|
||||
const Transform worldtocamera = kernel_data.cam.worldtocamera;
|
||||
return transform_point(&worldtocamera, P).z;
|
||||
}
|
||||
const Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
const float3 camP = make_float3(cameratoworld.x.w, cameratoworld.y.w, cameratoworld.z.w);
|
||||
return len(P - camP);
|
||||
}
|
||||
|
||||
ccl_device_inline float3 camera_direction_from_point(KernelGlobals kg, const float3 P)
|
||||
{
|
||||
const Transform cameratoworld = kernel_data.cam.cameratoworld;
|
||||
|
||||
if (kernel_data.cam.type == CAMERA_ORTHOGRAPHIC) {
|
||||
const float3 camD = make_float3(cameratoworld.x.z, cameratoworld.y.z, cameratoworld.z.z);
|
||||
return -camD;
|
||||
}
|
||||
const float3 camP = make_float3(cameratoworld.x.w, cameratoworld.y.w, cameratoworld.z.w);
|
||||
return normalize(camP - P);
|
||||
}
|
||||
|
||||
ccl_device_inline float3 camera_world_to_ndc(KernelGlobals kg,
|
||||
ccl_private ShaderData *sd,
|
||||
float3 P)
|
||||
{
|
||||
if (kernel_data.cam.type == CAMERA_PERSPECTIVE || kernel_data.cam.type == CAMERA_ORTHOGRAPHIC) {
|
||||
/* perspective / ortho */
|
||||
if (sd->object == PRIM_NONE && kernel_data.cam.type == CAMERA_PERSPECTIVE) {
|
||||
P += camera_position(kg);
|
||||
}
|
||||
|
||||
const ProjectionTransform tfm = kernel_data.cam.worldtondc;
|
||||
return transform_perspective(&tfm, P);
|
||||
}
|
||||
/* panorama or custom */
|
||||
const Transform tfm = kernel_data.cam.worldtocamera;
|
||||
|
||||
if (sd->object != OBJECT_NONE) {
|
||||
P = normalize(transform_point(&tfm, P));
|
||||
}
|
||||
else {
|
||||
P = normalize(transform_direction(&tfm, P));
|
||||
}
|
||||
|
||||
if (kernel_data.cam.type == CAMERA_PANORAMA) {
|
||||
return make_float3(direction_to_panorama(&kernel_data.cam, P));
|
||||
}
|
||||
/* TODO: Fall back to camera coordinates until we have inverse mappings for custom cameras. */
|
||||
return P;
|
||||
}
|
||||
|
||||
/* Motion vector for motion pass */
|
||||
|
||||
ccl_device_forceinline float4 camera_motion_vector_direction(KernelGlobals kg, const float3 D)
|
||||
{
|
||||
Transform tfm;
|
||||
float3 motion_center;
|
||||
float3 motion_pre;
|
||||
float3 motion_post;
|
||||
|
||||
/* Camera motion, for perspective/orthographic motion.pre/post will be a
|
||||
* world-to-raster matrix, for panorama it's world-to-camera, for custom
|
||||
* we fall back to the world position until we have inverse mapping for it */
|
||||
if (kernel_data.cam.type == CAMERA_CUSTOM) {
|
||||
/* TODO: Custom cameras don't have inverse mappings yet, so we fall back to
|
||||
* camera-space vectors here for now. */
|
||||
tfm = kernel_data.cam.worldtocamera;
|
||||
motion_center = normalize(transform_direction(&tfm, D));
|
||||
|
||||
tfm = kernel_data.cam.motion_pass_pre;
|
||||
motion_pre = normalize(transform_direction(&tfm, D));
|
||||
|
||||
tfm = kernel_data.cam.motion_pass_post;
|
||||
motion_post = normalize(transform_direction(&tfm, D));
|
||||
}
|
||||
else if (kernel_data.cam.type != CAMERA_PANORAMA) {
|
||||
/* Perspective and orthographics camera use the world-to-raster matrix. */
|
||||
ProjectionTransform projection = kernel_data.cam.worldtoraster;
|
||||
motion_center = transform_perspective_direction(&projection, D);
|
||||
|
||||
projection = kernel_data.cam.perspective_pre;
|
||||
motion_pre = transform_perspective_direction(&projection, D);
|
||||
|
||||
projection = kernel_data.cam.perspective_post;
|
||||
motion_post = transform_perspective_direction(&projection, D);
|
||||
}
|
||||
else {
|
||||
/* Panorama cameras have their own inverse mappings. */
|
||||
tfm = kernel_data.cam.worldtocamera;
|
||||
motion_center = normalize(transform_direction(&tfm, D));
|
||||
motion_center = make_float3(direction_to_panorama(&kernel_data.cam, motion_center));
|
||||
motion_center.x *= kernel_data.cam.width;
|
||||
motion_center.y *= kernel_data.cam.height;
|
||||
|
||||
tfm = kernel_data.cam.motion_pass_pre;
|
||||
motion_pre = normalize(transform_direction(&tfm, D));
|
||||
motion_pre = make_float3(direction_to_panorama(&kernel_data.cam, motion_pre));
|
||||
motion_pre.x *= kernel_data.cam.width;
|
||||
motion_pre.y *= kernel_data.cam.height;
|
||||
|
||||
tfm = kernel_data.cam.motion_pass_post;
|
||||
motion_post = normalize(transform_direction(&tfm, D));
|
||||
motion_post = make_float3(direction_to_panorama(&kernel_data.cam, motion_post));
|
||||
motion_post.x *= kernel_data.cam.width;
|
||||
motion_post.y *= kernel_data.cam.height;
|
||||
}
|
||||
|
||||
motion_pre = motion_pre - motion_center;
|
||||
motion_post = motion_center - motion_post;
|
||||
|
||||
return make_float4(motion_pre.x, motion_pre.y, motion_post.x, motion_post.y);
|
||||
}
|
||||
|
||||
CCL_NAMESPACE_END
|
||||
382
blender-5.2.0/intern/cycles/kernel/camera/projection.h
Normal file
382
blender-5.2.0/intern/cycles/kernel/camera/projection.h
Normal file
@@ -0,0 +1,382 @@
|
||||
/* SPDX-FileCopyrightText: 2009-2010 Sony Pictures Imageworks Inc., et al.
|
||||
* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*
|
||||
* Adapted code from Open Shading Language. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/types.h"
|
||||
|
||||
#include "util/math.h"
|
||||
#include "util/types.h"
|
||||
|
||||
CCL_NAMESPACE_BEGIN
|
||||
|
||||
/* Equirectangular coordinates <-> Cartesian direction */
|
||||
|
||||
ccl_device float2 direction_to_equirectangular_range(const float3 dir, const float4 range)
|
||||
{
|
||||
if (is_zero(dir)) {
|
||||
return zero_float2();
|
||||
}
|
||||
|
||||
const float u = (atan2f(dir.y, dir.x) - range.y) / range.x;
|
||||
const float v = (acosf(dir.z / len(dir)) - range.w) / range.z;
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
ccl_device dual2 direction_to_equirectangular_range(const dual3 dir, const float4 range)
|
||||
{
|
||||
if (is_zero(dir)) {
|
||||
return make_zero<dual2>();
|
||||
}
|
||||
|
||||
const dual1 u = (atan2(dir.y(), dir.x()) - range.y) / range.x;
|
||||
const dual1 v = (acos(dir.z() / len(dir)) - range.w) / range.z;
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
ccl_device float3 equirectangular_range_to_direction(const float u,
|
||||
const float v,
|
||||
const float4 range)
|
||||
{
|
||||
const float phi = range.x * u + range.y;
|
||||
const float theta = range.z * v + range.w;
|
||||
return spherical_to_direction(theta, phi);
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_equirectangular(const float3 dir)
|
||||
{
|
||||
return direction_to_equirectangular_range(dir, make_float4(-M_2PI_F, M_PI_F, -M_PI_F, M_PI_F));
|
||||
}
|
||||
|
||||
ccl_device dual2 direction_to_equirectangular(const dual3 dir)
|
||||
{
|
||||
return direction_to_equirectangular_range(dir, make_float4(-M_2PI_F, M_PI_F, -M_PI_F, M_PI_F));
|
||||
}
|
||||
|
||||
ccl_device float3 equirectangular_to_direction(const float u, const float v)
|
||||
{
|
||||
return equirectangular_range_to_direction(u, v, make_float4(-M_2PI_F, M_PI_F, -M_PI_F, M_PI_F));
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_central_cylindrical(const float3 dir, const float4 range)
|
||||
{
|
||||
const float z = dir.z / len(make_float2(dir.x, dir.y));
|
||||
const float theta = atan2f(dir.y, dir.x);
|
||||
const float u = inverse_lerp(range.x, range.y, theta);
|
||||
const float v = inverse_lerp(range.z, range.w, z);
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
ccl_device float3 central_cylindrical_to_direction(const float u,
|
||||
const float v,
|
||||
const float4 range)
|
||||
{
|
||||
const float theta = mix(range.x, range.y, u);
|
||||
const float z = mix(range.z, range.w, v);
|
||||
return make_float3(cosf(theta), sinf(theta), z);
|
||||
}
|
||||
|
||||
/* Fisheye <-> Cartesian direction */
|
||||
|
||||
ccl_device_inline float3 fisheye_to_direction(const float theta,
|
||||
const float u,
|
||||
float v,
|
||||
const float r)
|
||||
{
|
||||
float phi = safe_acosf(safe_divide(u, r));
|
||||
if (v < 0.0f) {
|
||||
phi = -phi;
|
||||
}
|
||||
|
||||
return make_float3(cosf(theta), -cosf(phi) * sinf(theta), sinf(phi) * sinf(theta));
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_fisheye_equidistant(const float3 dir, const float fov)
|
||||
{
|
||||
const float r = atan2f(len(make_float2(dir.y, dir.z)), dir.x) / fov;
|
||||
const float2 uv = r * safe_normalize(make_float2(dir.y, dir.z));
|
||||
return make_float2(0.5f - uv.x, uv.y + 0.5f);
|
||||
}
|
||||
|
||||
ccl_device float3 fisheye_equidistant_to_direction(float u, float v, float fov)
|
||||
{
|
||||
u = (u - 0.5f) * 2.0f;
|
||||
v = (v - 0.5f) * 2.0f;
|
||||
|
||||
const float r = sqrtf(u * u + v * v);
|
||||
|
||||
if (r > 1.0f) {
|
||||
return zero_float3();
|
||||
}
|
||||
|
||||
const float theta = r * fov * 0.5f;
|
||||
|
||||
return fisheye_to_direction(theta, u, v, r);
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_fisheye_equisolid(const float3 dir,
|
||||
const float lens,
|
||||
const float width,
|
||||
const float height)
|
||||
{
|
||||
const float theta = safe_acosf(dir.x);
|
||||
const float r = 2.0f * lens * sinf(theta * 0.5f);
|
||||
|
||||
const float2 uv = r * safe_normalize(make_float2(dir.y, dir.z));
|
||||
return make_float2(0.5f - uv.x / width, uv.y / height + 0.5f);
|
||||
}
|
||||
|
||||
ccl_device_inline float3 fisheye_equisolid_to_direction(
|
||||
float u, float v, float lens, const float fov, const float width, const float height)
|
||||
{
|
||||
u = (u - 0.5f) * width;
|
||||
v = (v - 0.5f) * height;
|
||||
|
||||
const float rmax = 2.0f * lens * sinf(fov * 0.25f);
|
||||
const float r = sqrtf(u * u + v * v);
|
||||
|
||||
if (r > rmax) {
|
||||
return zero_float3();
|
||||
}
|
||||
|
||||
const float theta = 2.0f * asinf(r / (2.0f * lens));
|
||||
|
||||
return fisheye_to_direction(theta, u, v, r);
|
||||
}
|
||||
|
||||
ccl_device_inline float3 fisheye_lens_polynomial_to_direction(float u,
|
||||
float v,
|
||||
float coeff0,
|
||||
const float4 coeffs,
|
||||
const float fov,
|
||||
const float width,
|
||||
const float height)
|
||||
{
|
||||
u = (u - 0.5f) * width;
|
||||
v = (v - 0.5f) * height;
|
||||
|
||||
const float r = sqrtf(u * u + v * v);
|
||||
const float r2 = r * r;
|
||||
const float4 rr = make_float4(r, r2, r2 * r, r2 * r2);
|
||||
const float theta = -(coeff0 + dot(coeffs, rr));
|
||||
|
||||
if (fabsf(theta) > 0.5f * fov) {
|
||||
return zero_float3();
|
||||
}
|
||||
|
||||
return fisheye_to_direction(theta, u, v, r);
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_fisheye_lens_polynomial(
|
||||
float3 dir, const float coeff0, const float4 coeffs, const float width, const float height)
|
||||
{
|
||||
const float theta = -safe_acosf(dir.x);
|
||||
|
||||
/* Initialize r with the closed-form solution for the special case
|
||||
* coeffs.y = coeffs.z = coeffs.w = 0 */
|
||||
float r = (theta - coeff0) / coeffs.x;
|
||||
|
||||
const float4 diff_coeffs = make_float4(1.0f, 2.0f, 3.0f, 4.0f) * coeffs;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
/** \name Newton's Method for Finding Roots
|
||||
*
|
||||
* Given is the result theta = distortion_model(r),
|
||||
* we need to find r.
|
||||
* Let F(r) := theta - distortion_model(r).
|
||||
* Then F(r) = 0 <=> distortion_model(r) = theta
|
||||
* Therefore we apply Newton's method for finding a root of F(r).
|
||||
* Newton step for the function F:
|
||||
* r_n+1 = r_n - F(r_n) / F'(r_n)
|
||||
* The addition in the implementation is due to canceling of signs.
|
||||
* \{ */
|
||||
const float old_r = r;
|
||||
const float r2 = r * r;
|
||||
const float F_r = theta - (coeff0 + dot(coeffs, make_float4(r, r2, r2 * r, r2 * r2)));
|
||||
const float dF_r = dot(diff_coeffs, make_float4(1.0f, r, r2, r2 * r));
|
||||
r += F_r / dF_r;
|
||||
|
||||
/* Early termination if the change is below the threshold */
|
||||
if (fabsf(r - old_r) < 1e-6f) {
|
||||
break;
|
||||
}
|
||||
/** \} */
|
||||
}
|
||||
|
||||
const float2 uv = r * safe_normalize(make_float2(dir.y, dir.z));
|
||||
return make_float2(0.5f - uv.x / width, uv.y / height + 0.5f);
|
||||
}
|
||||
|
||||
/* Mirror Ball <-> Cartesian direction. */
|
||||
|
||||
ccl_device float3 mirrorball_to_direction(const float u, const float v)
|
||||
{
|
||||
/* point on sphere */
|
||||
float3 dir;
|
||||
|
||||
dir.x = 2.0f * u - 1.0f;
|
||||
dir.z = 2.0f * v - 1.0f;
|
||||
|
||||
if (dir.x * dir.x + dir.z * dir.z > 1.0f) {
|
||||
return zero_float3();
|
||||
}
|
||||
|
||||
dir.y = -sqrtf(max(1.0f - dir.x * dir.x - dir.z * dir.z, 0.0f));
|
||||
|
||||
/* reflection */
|
||||
const float3 I = make_float3(0.0f, -1.0f, 0.0f);
|
||||
|
||||
return 2.0f * dot(dir, I) * dir - I;
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_mirrorball(float3 dir)
|
||||
{
|
||||
/* inverse of mirrorball_to_direction */
|
||||
dir.y -= 1.0f;
|
||||
|
||||
const float div = 2.0f * sqrtf(max(-0.5f * dir.y, 0.0f));
|
||||
if (div > 0.0f) {
|
||||
dir /= div;
|
||||
}
|
||||
|
||||
const float u = 0.5f * (dir.x + 1.0f);
|
||||
const float v = 0.5f * (dir.z + 1.0f);
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
ccl_device dual2 direction_to_mirrorball(dual3 dir)
|
||||
{
|
||||
/* inverse of mirrorball_to_direction */
|
||||
dir.val.y -= 1.0f;
|
||||
|
||||
dir = dir * 0.5f * inversesqrt(-0.5f * dir.y());
|
||||
|
||||
const dual1 u = 0.5f * (dir.x() + 1.0f);
|
||||
const dual1 v = 0.5f * (dir.z() + 1.0f);
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
/* Single face of a equiangular cube map projection as described in
|
||||
* https://blog.google/products/google-ar-vr/bringing-pixels-front-and-center-vr-video/ */
|
||||
ccl_device float3 equiangular_cubemap_face_to_direction(float u, float v)
|
||||
{
|
||||
u = tanf((0.5f - u) * M_PI_2_F);
|
||||
v = tanf((v - 0.5f) * M_PI_2_F);
|
||||
|
||||
return normalize(make_float3(1.0f, u, v));
|
||||
}
|
||||
|
||||
ccl_device float2 direction_to_equiangular_cubemap_face(const float3 dir)
|
||||
{
|
||||
const float u = 0.5f - atan2f(dir.y, dir.x) * 2.0f / M_PI_F;
|
||||
const float v = atan2f(dir.z, dir.x) * 2.0f / M_PI_F + 0.5f;
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
ccl_device_inline float3 panorama_to_direction(ccl_constant KernelCamera *cam,
|
||||
const float u,
|
||||
float v)
|
||||
{
|
||||
switch (cam->panorama_type) {
|
||||
case PANORAMA_EQUIRECTANGULAR:
|
||||
return equirectangular_range_to_direction(u, v, cam->equirectangular_range);
|
||||
case PANORAMA_EQUIANGULAR_CUBEMAP_FACE:
|
||||
return equiangular_cubemap_face_to_direction(u, v);
|
||||
case PANORAMA_MIRRORBALL:
|
||||
return mirrorball_to_direction(u, v);
|
||||
case PANORAMA_FISHEYE_EQUIDISTANT:
|
||||
return fisheye_equidistant_to_direction(u, v, cam->fisheye_fov);
|
||||
case PANORAMA_FISHEYE_LENS_POLYNOMIAL:
|
||||
return fisheye_lens_polynomial_to_direction(u,
|
||||
v,
|
||||
cam->fisheye_lens_polynomial_bias,
|
||||
cam->fisheye_lens_polynomial_coefficients,
|
||||
cam->fisheye_fov,
|
||||
cam->sensorwidth,
|
||||
cam->sensorheight);
|
||||
case PANORAMA_CENTRAL_CYLINDRICAL:
|
||||
return central_cylindrical_to_direction(u, v, cam->central_cylindrical_range);
|
||||
case PANORAMA_FISHEYE_EQUISOLID:
|
||||
default:
|
||||
return fisheye_equisolid_to_direction(
|
||||
u, v, cam->fisheye_lens, cam->fisheye_fov, cam->sensorwidth, cam->sensorheight);
|
||||
}
|
||||
}
|
||||
|
||||
ccl_device_inline float2 direction_to_panorama(ccl_constant KernelCamera *cam, const float3 dir)
|
||||
{
|
||||
switch (cam->panorama_type) {
|
||||
case PANORAMA_EQUIRECTANGULAR:
|
||||
return direction_to_equirectangular_range(dir, cam->equirectangular_range);
|
||||
case PANORAMA_EQUIANGULAR_CUBEMAP_FACE:
|
||||
return direction_to_equiangular_cubemap_face(dir);
|
||||
case PANORAMA_MIRRORBALL:
|
||||
return direction_to_mirrorball(dir);
|
||||
case PANORAMA_FISHEYE_EQUIDISTANT:
|
||||
return direction_to_fisheye_equidistant(dir, cam->fisheye_fov);
|
||||
case PANORAMA_FISHEYE_LENS_POLYNOMIAL:
|
||||
return direction_to_fisheye_lens_polynomial(dir,
|
||||
cam->fisheye_lens_polynomial_bias,
|
||||
cam->fisheye_lens_polynomial_coefficients,
|
||||
cam->sensorwidth,
|
||||
cam->sensorheight);
|
||||
case PANORAMA_CENTRAL_CYLINDRICAL:
|
||||
return direction_to_central_cylindrical(dir, cam->central_cylindrical_range);
|
||||
case PANORAMA_FISHEYE_EQUISOLID:
|
||||
default:
|
||||
return direction_to_fisheye_equisolid(
|
||||
dir, cam->fisheye_lens, cam->sensorwidth, cam->sensorheight);
|
||||
}
|
||||
}
|
||||
|
||||
ccl_device_inline void spherical_stereo_transform(ccl_constant KernelCamera *cam,
|
||||
ccl_private float3 *P,
|
||||
ccl_private float3 *D)
|
||||
{
|
||||
float interocular_offset = cam->interocular_offset;
|
||||
|
||||
/* Interocular offset of zero means either non stereo, or stereo without
|
||||
* spherical stereo. */
|
||||
kernel_assert(interocular_offset != 0.0f);
|
||||
|
||||
if (cam->pole_merge_angle_to > 0.0f) {
|
||||
const float pole_merge_angle_from = cam->pole_merge_angle_from;
|
||||
const float pole_merge_angle_to = cam->pole_merge_angle_to;
|
||||
const float altitude = fabsf(safe_asinf((*D).z));
|
||||
if (altitude > pole_merge_angle_to) {
|
||||
interocular_offset = 0.0f;
|
||||
}
|
||||
else if (altitude > pole_merge_angle_from) {
|
||||
const float fac = (altitude - pole_merge_angle_from) /
|
||||
(pole_merge_angle_to - pole_merge_angle_from);
|
||||
const float fade = cosf(fac * M_PI_2_F);
|
||||
interocular_offset *= fade;
|
||||
}
|
||||
}
|
||||
|
||||
const float3 up = make_float3(0.0f, 0.0f, 1.0f);
|
||||
const float3 side = normalize(cross(*D, up));
|
||||
const float3 stereo_offset = side * interocular_offset;
|
||||
|
||||
*P += stereo_offset;
|
||||
|
||||
/* Convergence distance is FLT_MAX in the case of parallel convergence mode,
|
||||
* no need to modify direction in this case either. */
|
||||
const float convergence_distance = cam->convergence_distance;
|
||||
|
||||
if (convergence_distance != FLT_MAX) {
|
||||
const float3 screen_offset = convergence_distance * (*D);
|
||||
*D = normalize(screen_offset - stereo_offset);
|
||||
}
|
||||
}
|
||||
|
||||
CCL_NAMESPACE_END
|
||||
Reference in New Issue
Block a user